diff options
author | Jack He <siyuanh@google.com> | 2022-03-03 06:50:22 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-03-03 06:50:22 +0000 |
commit | 2b130e18935864a10ea554c96c217a07c09d295a (patch) | |
tree | bf0f0c8fd92d0a2d6ece056ea120e944ece9a25a | |
parent | 84e3f271b6248b9a2b646464d776640d92ee4b24 (diff) | |
parent | 43c9825758098f8a8effbabaf358ee36a15a5aab (diff) |
Merge "BT LE broadcast assistant implementation" am: 4024c5ee49 am: 627c75137f am: 43c9825758
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Bluetooth/+/1952637
Change-Id: I537e81d8bb6c9fd8306e367f9f599412d047ed40
29 files changed, 5043 insertions, 23 deletions
diff --git a/android/app/AndroidManifest.xml b/android/app/AndroidManifest.xml index 95e82a46fd..bf0f4d46fa 100644 --- a/android/app/AndroidManifest.xml +++ b/android/app/AndroidManifest.xml @@ -468,6 +468,15 @@ <action android:name="android.bluetooth.IBluetoothLeCallControl" /> </intent-filter> </service> + <service + android:process="@string/process" + android:name = ".bass_client.BassClientService" + android:enabled="@bool/profile_supported_bass_client" + android:exported = "true"> + <intent-filter> + <action android:name="android.bluetooth.IBluetoothLeBroadcastAssistant" /> + </intent-filter> + </service> <!-- Authenticator for PBAP account. --> <service android:process="@string/process" android:name=".pbapclient.AuthenticationService" diff --git a/android/app/res/values/config.xml b/android/app/res/values/config.xml index 3a709770c1..dddf9ad3e5 100644 --- a/android/app/res/values/config.xml +++ b/android/app/res/values/config.xml @@ -39,6 +39,7 @@ <bool name="profile_supported_csip_set_coordinator">true</bool> <bool name="profile_supported_le_call_control">true</bool> <bool name="profile_supported_hap_client">true</bool> + <bool name="profile_supported_bass_client">true</bool> <!-- If true, we will require location to be enabled on the device to fire Bluetooth LE scan result callbacks in addition to having one diff --git a/android/app/src/com/android/bluetooth/bass_client/BaseData.java b/android/app/src/com/android/bluetooth/bass_client/BaseData.java new file mode 100755 index 0000000000..ac547c3bc9 --- /dev/null +++ b/android/app/src/com/android/bluetooth/bass_client/BaseData.java @@ -0,0 +1,520 @@ +/* + * Copyright 2022 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. + */ + +package com.android.bluetooth.bass_client; + +import android.util.Log; +import android.util.Pair; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.Set; + +/** + * Helper class to parse the Broadcast Announcement BASE data + */ +class BaseData { + private static final String TAG = "Bassclient-BaseData"; + private static final byte UNKNOWN_CODEC = (byte) 0xFE; + private static final int METADATA_LEVEL1 = 1; + private static final int METADATA_LEVEL2 = 2; + private static final int METADATA_LEVEL3 = 3; + private static final int METADATA_PRESENTATIONDELAY_LENGTH = 3; + private static final int METADATA_CODEC_LENGTH = 5; + private static final int METADATA_UNKNOWN_CODEC_LENGTH = 1; + private static final int CODEC_CAPABILITIES_SAMPLE_RATE_TYPE = 1; + private static final int CODEC_CAPABILITIES_FRAME_DURATION_TYPE = 2; + private static final int CODEC_CAPABILITIES_CHANNEL_COUNT_TYPE = 3; + private static final int CODEC_CAPABILITIES_OCTETS_PER_FRAME_TYPE = 4; + private static final int CODEC_CAPABILITIES_MAX_FRAMES_PER_SDU_TYPE = 5; + private static final int CODEC_CONFIGURATION_SAMPLE_RATE_TYPE = 0x01; + private static final int CODEC_CONFIGURATION_FRAME_DURATION_TYPE = 0x02; + private static final int CODEC_CONFIGURATION_CHANNEL_ALLOCATION_TYPE = 0x03; + private static final int CODEC_CONFIGURATION_OCTETS_PER_FRAME_TYPE = 0x04; + private static final int CODEC_CONFIGURATION_BLOCKS_PER_SDU_TYPE = 0x05; + private static final int METADATA_PREFERRED_CONTEXTS_TYPE = 0x01; + private static final int METADATA_STREAMING_CONTEXTS_TYPE = 0x02; + private static final int METADATA_PROGRAM_INFO_TYPE = 0x03; + private static final int METADATA_LANGUAGE_TYPE = 0x04; + private static final int METADATA_CCID_LIST_TYPE = 0x05; + private static final int METADATA_PARENTAL_RATING_TYPE = 0x06; + private static final int METADATA_PROGRAM_INFO_URI_TYPE = 0x07; + private static final int METADATA_EXTENDED_TYPE = 0xFE; + private static final int METADATA_VENDOR_TYPE = 0xFF; + private static final int CODEC_AUDIO_LOCATION_FRONT_LEFT = 0x01000000; + private static final int CODEC_AUDIO_LOCATION_FRONT_RIGHT = 0x02000000; + private static final int CODEC_AUDIO_SAMPLE_RATE_8K = 0x01; + private static final int CODEC_AUDIO_SAMPLE_RATE_16K = 0x03; + private static final int CODEC_AUDIO_SAMPLE_RATE_24K = 0x05; + private static final int CODEC_AUDIO_SAMPLE_RATE_32K = 0x06; + private static final int CODEC_AUDIO_SAMPLE_RATE_44P1K = 0x07; + private static final int CODEC_AUDIO_SAMPLE_RATE_48K = 0x08; + private static final int CODEC_AUDIO_FRAME_DURATION_7P5MS = 0x00; + private static final int CODEC_AUDIO_FRAME_DURATION_10MS = 0x01; + + private final BaseInformation mLevelOne; + private final ArrayList<BaseInformation> mLevelTwo; + private final ArrayList<BaseInformation> mLevelThree; + + private int mNumBISIndices = 0; + + public static class BaseInformation { + public byte[] presentationDelay = new byte[3]; + public byte[] codecId = new byte[5]; + public byte codecConfigLength; + public byte[] codecConfigInfo; + public byte metaDataLength; + public byte[] metaData; + public byte numSubGroups; + public byte[] bisIndices; + public byte index; + public int subGroupId; + public int level; + public LinkedHashSet<String> keyCodecCfgDiff; + public LinkedHashSet<String> keyMetadataDiff; + public String diffText; + public String description; + public byte[] consolidatedCodecId; + public Set<String> consolidatedMetadata; + public Set<String> consolidatedCodecInfo; + public HashMap<Integer, String> consolidatedUniqueCodecInfo; + public HashMap<Integer, String> consolidatedUniqueMetadata; + + BaseInformation() { + presentationDelay = new byte[3]; + codecId = new byte[5]; + codecConfigLength = 0; + codecConfigInfo = null; + metaDataLength = 0; + metaData = null; + numSubGroups = 0; + bisIndices = null; + index = (byte) 0xFF; + level = 0; + keyCodecCfgDiff = new LinkedHashSet<String>(); + keyMetadataDiff = new LinkedHashSet<String>(); + consolidatedMetadata = new LinkedHashSet<String>(); + consolidatedCodecInfo = new LinkedHashSet<String>(); + consolidatedCodecId = new byte[5]; + consolidatedUniqueMetadata = new HashMap<Integer, String>(); + consolidatedUniqueCodecInfo = new HashMap<Integer, String>(); + diffText = new String(""); + description = new String(""); + log("BaseInformation is Initialized"); + } + + boolean isCodecIdUnknown() { + return (codecId != null && codecId[4] == (byte) UNKNOWN_CODEC); + } + + void print() { + log("**BEGIN: Base Information**"); + log("**Level: " + level + "***"); + if (level == 1) { + log("presentationDelay: " + Arrays.toString(presentationDelay)); + } + if (level == 2) { + log("codecId: " + Arrays.toString(codecId)); + } + if (level == 2 || level == 3) { + log("codecConfigLength: " + codecConfigLength); + log("subGroupId: " + subGroupId); + } + if (codecConfigLength != (byte) 0) { + log("codecConfigInfo: " + Arrays.toString(codecConfigInfo)); + } + if (level == 2) { + log("metaDataLength: " + metaDataLength); + if (metaDataLength != (byte) 0) { + log("metaData: " + Arrays.toString(metaData)); + } + if (level == 1 || level == 2) { + log("numSubGroups: " + numSubGroups); + } + } + if (level == 2) { + log("Level2: Key Metadata differentiators"); + if (keyMetadataDiff != null) { + Iterator<String> itr = keyMetadataDiff.iterator(); + for (int k = 0; itr.hasNext(); k++) { + log("keyMetadataDiff:[" + k + "]:" + + Arrays.toString(itr.next().getBytes())); + } + } + log("END: Level2: Key Metadata differentiators"); + log("Level2: Key CodecConfig differentiators"); + if (keyCodecCfgDiff != null) { + Iterator<String> itr = keyCodecCfgDiff.iterator(); + for (int k = 0; itr.hasNext(); k++) { + log("LEVEL2: keyCodecCfgDiff:[" + k + "]:" + + Arrays.toString(itr.next().getBytes())); + } + } + log("END: Level2: Key CodecConfig differentiators"); + log("LEVEL2: diffText: " + diffText); + } + if (level == 3) { + log("Level3: Key CodecConfig differentiators"); + if (keyCodecCfgDiff != null) { + Iterator<String> itr = keyCodecCfgDiff.iterator(); + for (int k = 0; itr.hasNext(); k++) { + log("LEVEL3: keyCodecCfgDiff:[" + k + "]:" + + Arrays.toString(itr.next().getBytes())); + } + } + log("END: Level3: Key CodecConfig differentiators"); + log("index: " + index); + log("LEVEL3: diffText: " + diffText); + } + log("**END: Base Information****"); + } + } + + BaseData(BaseInformation levelOne, ArrayList<BaseInformation> levelTwo, + ArrayList<BaseInformation> levelThree, int numOfBISIndices) { + mLevelOne = levelOne; + mLevelTwo = levelTwo; + mLevelThree = levelThree; + mNumBISIndices = numOfBISIndices; + } + + static BaseData parseBaseData(byte[] serviceData) { + if (serviceData == null) { + Log.e(TAG, "Invalid service data for BaseData construction"); + throw new IllegalArgumentException("Basedata: serviceData is null"); + } + BaseInformation levelOne = new BaseInformation(); + ArrayList<BaseInformation> levelTwo = new ArrayList<BaseInformation>(); + ArrayList<BaseInformation> levelThree = new ArrayList<BaseInformation>(); + int numOfBISIndices = 0; + log("BASE input" + Arrays.toString(serviceData)); + + // Parse Level 1 base + levelOne.level = METADATA_LEVEL1; + int offset = 0; + System.arraycopy(serviceData, offset, levelOne.presentationDelay, 0, 3); + offset += METADATA_PRESENTATIONDELAY_LENGTH; + levelOne.numSubGroups = serviceData[offset++]; + levelOne.print(); + log("levelOne subgroups" + levelOne.numSubGroups); + for (int i = 0; i < (int) levelOne.numSubGroups; i++) { + Pair<BaseInformation, Integer> pair1 = + parseLevelTwo(serviceData, i, offset); + BaseInformation node2 = pair1.first; + if (node2 == null) { + Log.e(TAG, "Error: parsing Level 2"); + return null; + } + numOfBISIndices += node2.numSubGroups; + levelTwo.add(node2); + node2.print(); + offset = pair1.second; + for (int k = 0; k < node2.numSubGroups; k++) { + Pair<BaseInformation, Integer> pair2 = + parseLevelThree(serviceData, offset); + BaseInformation node3 = pair2.first; + offset = pair2.second; + if (node3 == null) { + Log.e(TAG, "Error: parsing Level 3"); + return null; + } + levelThree.add(node3); + node3.print(); + } + } + consolidateBaseofLevelTwo(levelTwo, levelThree); + return new BaseData(levelOne, levelTwo, levelThree, numOfBISIndices); + } + + private static Pair<BaseInformation, Integer> + parseLevelTwo(byte[] serviceData, int groupIndex, int offset) { + log("Parsing Level 2"); + BaseInformation node = new BaseInformation(); + node.level = METADATA_LEVEL2; + node.subGroupId = groupIndex; + node.numSubGroups = serviceData[offset++]; + if (serviceData[offset] == (byte) UNKNOWN_CODEC) { + // Place It in the last byte of codecID + System.arraycopy(serviceData, offset, node.codecId, + METADATA_CODEC_LENGTH - 1, METADATA_UNKNOWN_CODEC_LENGTH); + offset += METADATA_UNKNOWN_CODEC_LENGTH; + log("codecId is FE"); + } else { + System.arraycopy(serviceData, offset, node.codecId, + 0, METADATA_CODEC_LENGTH); + offset += METADATA_CODEC_LENGTH; + } + node.codecConfigLength = serviceData[offset++]; + if (node.codecConfigLength != 0) { + node.codecConfigInfo = new byte[(int) node.codecConfigLength]; + System.arraycopy(serviceData, offset, node.codecConfigInfo, + 0, (int) node.codecConfigLength); + offset += node.codecConfigLength; + } + node.metaDataLength = serviceData[offset++]; + if (node.metaDataLength != 0) { + node.metaData = new byte[(int) node.metaDataLength]; + System.arraycopy(serviceData, offset, + node.metaData, 0, (int) node.metaDataLength); + offset += node.metaDataLength; + } + return new Pair<BaseInformation, Integer>(node, offset); + } + + private static Pair<BaseInformation, Integer> + parseLevelThree(byte[] serviceData, int offset) { + log("Parsing Level 3"); + BaseInformation node = new BaseInformation(); + node.level = METADATA_LEVEL3; + node.index = serviceData[offset++]; + node.codecConfigLength = serviceData[offset++]; + if (node.codecConfigLength != 0) { + node.codecConfigInfo = new byte[(int) node.codecConfigLength]; + System.arraycopy(serviceData, offset, + node.codecConfigInfo, 0, (int) node.codecConfigLength); + offset += node.codecConfigLength; + } + return new Pair<BaseInformation, Integer>(node, offset); + } + + static void consolidateBaseofLevelTwo(ArrayList<BaseInformation> levelTwo, + ArrayList<BaseInformation> levelThree) { + int startIdx = 0; + int children = 0; + for (int i = 0; i < levelTwo.size(); i++) { + startIdx = startIdx + children; + children = children + levelTwo.get(i).numSubGroups; + consolidateBaseofLevelThree(levelTwo, levelThree, + i, startIdx, levelTwo.get(i).numSubGroups); + } + // Eliminate Duplicates at Level 3 + for (int i = 0; i < levelThree.size(); i++) { + Map<Integer, String> uniqueMds = new HashMap<Integer, String>(); + Map<Integer, String> uniqueCcis = new HashMap<Integer, String>(); + Set<String> Csfs = levelThree.get(i).consolidatedCodecInfo; + if (Csfs.size() > 0) { + Iterator<String> itr = Csfs.iterator(); + for (int j = 0; itr.hasNext(); j++) { + byte[] ltvEntries = itr.next().getBytes(); + int k = 0; + byte length = ltvEntries[k++]; + byte[] ltv = new byte[length + 1]; + ltv[0] = length; + System.arraycopy(ltvEntries, k, ltv, 1, length); + int type = (int) ltv[1]; + String s = uniqueCcis.get(type); + String ltvS = new String(ltv); + if (s == null) { + uniqueCcis.put(type, ltvS); + } else { + // if same type exists, replace + uniqueCcis.replace(type, ltvS); + } + } + } + Set<String> Mds = levelThree.get(i).consolidatedMetadata; + if (Mds.size() > 0) { + Iterator<String> itr = Mds.iterator(); + for (int j = 0; itr.hasNext(); j++) { + byte[] ltvEntries = itr.next().getBytes(); + int k = 0; + byte length = ltvEntries[k++]; + byte[] ltv = new byte[length + 1]; + ltv[0] = length; + System.arraycopy(ltvEntries, k, ltv, 1, length); + int type = (int) ltv[1]; + String s = uniqueCcis.get(type); + String ltvS = new String(ltv); + if (s == null) { + uniqueMds.put(type, ltvS); + } else { + uniqueMds.replace(type, ltvS); + } + } + } + levelThree.get(i).consolidatedUniqueMetadata = new HashMap<Integer, String>(uniqueMds); + levelThree.get(i).consolidatedUniqueCodecInfo = + new HashMap<Integer, String>(uniqueCcis); + } + } + + static void consolidateBaseofLevelThree(ArrayList<BaseInformation> levelTwo, + ArrayList<BaseInformation> levelThree, int parentSubgroup, int startIdx, int numNodes) { + for (int i = startIdx; i < startIdx + numNodes || i < levelThree.size(); i++) { + levelThree.get(i).subGroupId = levelTwo.get(parentSubgroup).subGroupId; + log("Copy Codec Id from Level2 Parent" + parentSubgroup); + System.arraycopy( + levelTwo.get(parentSubgroup).consolidatedCodecId, + 0, levelThree.get(i).consolidatedCodecId, 0, 5); + // Metadata clone from Parent + levelThree.get(i).consolidatedMetadata = + new LinkedHashSet<String>(levelTwo.get(parentSubgroup).consolidatedMetadata); + // CCI clone from Parent + levelThree.get(i).consolidatedCodecInfo = + new LinkedHashSet<String>(levelTwo.get(parentSubgroup).consolidatedCodecInfo); + // Append Level 2 Codec Config + if (levelThree.get(i).codecConfigLength != 0) { + log("append level 3 cci to level 3 cons:" + i); + String s = new String(levelThree.get(i).codecConfigInfo); + levelThree.get(i).consolidatedCodecInfo.add(s); + } + } + } + + public int getNumberOfIndices() { + return mNumBISIndices; + } + + public BaseInformation getLevelOne() { + return mLevelOne; + } + + public ArrayList<BaseInformation> getLevelTwo() { + return mLevelTwo; + } + + public ArrayList<BaseInformation> getLevelThree() { + return mLevelThree; + } + + public byte getNumberOfSubgroupsofBIG() { + byte ret = 0; + if (mLevelOne != null) { + ret = mLevelOne.numSubGroups; + } + return ret; + } + + public ArrayList<BaseInformation> getBISIndexInfos() { + return mLevelThree; + } + + byte[] getMetadata(int subGroup) { + if (mLevelTwo != null) { + return mLevelTwo.get(subGroup).metaData; + } + return null; + } + + String getMetadataString(byte[] metadataBytes) { + String ret = ""; + switch (metadataBytes[1]) { + case METADATA_LANGUAGE_TYPE: + char[] lang = new char[3]; + System.arraycopy(metadataBytes, 1, lang, 0, 3); + Locale locale = new Locale(String.valueOf(lang)); + try { + ret = locale.getISO3Language(); + } catch (MissingResourceException e) { + ret = "UNKNOWN LANGUAGE"; + } + break; + default: + ret = "UNKNOWN METADATA TYPE"; + } + log("getMetadataString: " + ret); + return ret; + } + + String getCodecParamString(byte[] csiBytes) { + String ret = ""; + switch (csiBytes[1]) { + case CODEC_CONFIGURATION_CHANNEL_ALLOCATION_TYPE: + byte[] location = new byte[4]; + System.arraycopy(csiBytes, 2, location, 0, 4); + ByteBuffer wrapped = ByteBuffer.wrap(location); + int audioLocation = wrapped.getInt(); + log("audioLocation: " + audioLocation); + switch (audioLocation) { + case CODEC_AUDIO_LOCATION_FRONT_LEFT: + ret = "LEFT"; + break; + case CODEC_AUDIO_LOCATION_FRONT_RIGHT: + ret = "RIGHT"; + break; + case CODEC_AUDIO_LOCATION_FRONT_LEFT + | CODEC_AUDIO_LOCATION_FRONT_RIGHT: + ret = "LR"; + break; + } + break; + case CODEC_CONFIGURATION_SAMPLE_RATE_TYPE: + switch (csiBytes[2]) { + case CODEC_AUDIO_SAMPLE_RATE_8K: + ret = "8K"; + break; + case CODEC_AUDIO_SAMPLE_RATE_16K: + ret = "16K"; + break; + case CODEC_AUDIO_SAMPLE_RATE_24K: + ret = "24K"; + break; + case CODEC_AUDIO_SAMPLE_RATE_32K: + ret = "32K"; + break; + case CODEC_AUDIO_SAMPLE_RATE_44P1K: + ret = "44.1K"; + break; + case CODEC_AUDIO_SAMPLE_RATE_48K: + ret = "48K"; + break; + } + break; + case CODEC_CONFIGURATION_FRAME_DURATION_TYPE: + switch (csiBytes[2]) { + case CODEC_AUDIO_FRAME_DURATION_7P5MS: + ret = "7.5ms"; + break; + case CODEC_AUDIO_FRAME_DURATION_10MS: + ret = "10ms"; + break; + } + break; + case CODEC_CONFIGURATION_OCTETS_PER_FRAME_TYPE: + ret = "OPF_" + String.valueOf((int) csiBytes[2]); + break; + default: + ret = "UNKNOWN PARAMETER"; + } + log("getCodecParamString: " + ret); + return ret; + } + + void print() { + mLevelOne.print(); + log("----- Level TWO BASE ----"); + for (int i = 0; i < mLevelTwo.size(); i++) { + mLevelTwo.get(i).print(); + } + log("----- Level THREE BASE ----"); + for (int i = 0; i < mLevelThree.size(); i++) { + mLevelThree.get(i).print(); + } + } + + static void log(String msg) { + if (BassConstants.BASS_DBG) { + Log.d(TAG, msg); + } + } +} diff --git a/android/app/src/com/android/bluetooth/bass_client/BassClientService.java b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java new file mode 100755 index 0000000000..0a1c7a5b2a --- /dev/null +++ b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java @@ -0,0 +1,1261 @@ +/* + * Copyright 2022 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. + */ + +package com.android.bluetooth.bass_client; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; +import android.bluetooth.BluetoothUuid; +import android.bluetooth.IBluetoothLeBroadcastAssistant; +import android.bluetooth.IBluetoothLeBroadcastAssistantCallback; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanRecord; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelUuid; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.AdapterService; +import com.android.bluetooth.btservice.ProfileService; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Broacast Assistant Scan Service + */ +public class BassClientService extends ProfileService { + private static final boolean DBG = true; + private static final String TAG = BassClientService.class.getSimpleName(); + private static final int MAX_BASS_CLIENT_STATE_MACHINES = 10; + + private static BassClientService sService; + + private final Map<BluetoothDevice, BassClientStateMachine> mStateMachines = new HashMap<>(); + + private HandlerThread mStateMachinesThread; + private HandlerThread mCallbackHandlerThread; + private AdapterService mAdapterService; + private BluetoothAdapter mBluetoothAdapter = null; + private BassUtils mBassUtils = null; + private Map<BluetoothDevice, BluetoothDevice> mActiveSourceMap; + /* Caching the PeriodicAdvertisementResult from Broadcast source */ + /* This is stored at service so that each device state machine can access + and use it as needed. Once the periodic sync in cancelled, this data will bre + removed to ensure stable data won't used */ + /* broadcastSrcDevice, syncHandle */ + private Map<BluetoothDevice, Integer> mDeviceToSyncHandleMap; + /*syncHandle, parsed BaseData data*/ + private Map<Integer, BaseData> mSyncHandleToBaseDataMap; + private Map<Integer, BluetoothLeBroadcastMetadata> mBroadcastSources; + /*bcastSrcDevice, corresponding PeriodicAdvertisementResult*/ + private Map<BluetoothDevice, PeriodicAdvertisementResult> mPeriodicAdvertisementResultMap; + private ScanCallback mSearchScanCallback; + private Callbacks mCallbacks; + + void updatePeriodicAdvertisementResultMap( + BluetoothDevice device, + int addressType, + int syncHandle, + int advSid, + int advInterval, + int bId) { + log("updatePeriodicAdvertisementResultMap: device: " + device); + log("updatePeriodicAdvertisementResultMap: syncHandle: " + syncHandle); + log("updatePeriodicAdvertisementResultMap: advSid: " + advSid); + log("updatePeriodicAdvertisementResultMap: addressType: " + addressType); + log("updatePeriodicAdvertisementResultMap: advInterval: " + advInterval); + log("updatePeriodicAdvertisementResultMap: broadcastId: " + bId); + log("mDeviceToSyncHandleMap" + mDeviceToSyncHandleMap); + log("mPeriodicAdvertisementResultMap" + mPeriodicAdvertisementResultMap); + // Cache the SyncHandle + if (mDeviceToSyncHandleMap != null) { + mDeviceToSyncHandleMap.put(device, syncHandle); + } + if (mPeriodicAdvertisementResultMap != null) { + PeriodicAdvertisementResult paRes = mPeriodicAdvertisementResultMap.get(device); + if (paRes == null) { + log("PAResmap: add >>>"); + paRes = new PeriodicAdvertisementResult(device, + addressType, syncHandle, advSid, advInterval, bId); + if (paRes != null) { + paRes.print(); + mPeriodicAdvertisementResultMap.put(device, paRes); + } + } else { + if (advSid != BassConstants.INVALID_ADV_SID) { + paRes.updateAdvSid(advSid); + } + if (syncHandle != BassConstants.INVALID_SYNC_HANDLE) { + paRes.updateSyncHandle(syncHandle); + } + if (addressType != BassConstants.INVALID_ADV_ADDRESS_TYPE) { + paRes.updateAddressType(addressType); + } + if (advInterval != BassConstants.INVALID_ADV_INTERVAL) { + paRes.updateAdvInterval(advInterval); + } + if (bId != BassConstants.INVALID_BROADCAST_ID) { + paRes.updateBroadcastId(bId); + } + log("PAResmap: update >>>"); + paRes.print(); + mPeriodicAdvertisementResultMap.replace(device, paRes); + } + } + log(">>mPeriodicAdvertisementResultMap" + mPeriodicAdvertisementResultMap); + } + + PeriodicAdvertisementResult getPeriodicAdvertisementResult(BluetoothDevice device) { + if (mPeriodicAdvertisementResultMap == null) { + Log.e(TAG, "getPeriodicAdvertisementResult: mPeriodicAdvertisementResultMap is null"); + return null; + } + return mPeriodicAdvertisementResultMap.get(device); + } + + PeriodicAdvertisementResult clearPeriodicAdvertisementResult(BluetoothDevice device) { + if (mPeriodicAdvertisementResultMap == null) { + Log.e(TAG, "getPeriodicAdvertisementResult: mPeriodicAdvertisementResultMap is null"); + return null; + } + return mPeriodicAdvertisementResultMap.remove(device); + } + + void updateBase(int syncHandlemap, BaseData base) { + if (mSyncHandleToBaseDataMap == null) { + Log.e(TAG, "updateBase: mSyncHandleToBaseDataMap is null"); + return; + } + log("updateBase : mSyncHandleToBaseDataMap>>"); + mSyncHandleToBaseDataMap.put(syncHandlemap, base); + } + + BaseData getBase(int syncHandlemap) { + if (mSyncHandleToBaseDataMap == null) { + Log.e(TAG, "getBase: mSyncHandleToBaseDataMap is null"); + return null; + } + BaseData base = mSyncHandleToBaseDataMap.get(syncHandlemap); + log("getBase returns" + base); + return base; + } + + void updateSourceInternal(int sourceId, BluetoothLeBroadcastMetadata metaData) { + if (mBroadcastSources == null) { + return; + } + if (metaData != null) { + // This will replace old metadata with new one + mBroadcastSources.put(sourceId, metaData); + } else { + mBroadcastSources.remove(sourceId); + } + } + + BluetoothLeBroadcastMetadata getSourceInternal(int sourceId) { + if (mBroadcastSources != null) { + return mBroadcastSources.get(sourceId); + } + return null; + } + + void setActiveSyncedSource(BluetoothDevice scanDelegator, BluetoothDevice sourceDevice) { + log("setActiveSyncedSource: scanDelegator" + scanDelegator + + ":: sourceDevice:" + sourceDevice); + if (sourceDevice == null) { + mActiveSourceMap.remove(scanDelegator); + } else { + mActiveSourceMap.put(scanDelegator, sourceDevice); + } + } + + BluetoothDevice getActiveSyncedSource(BluetoothDevice scanDelegator) { + BluetoothDevice currentSource = mActiveSourceMap.get(scanDelegator); + log("getActiveSyncedSource: scanDelegator" + scanDelegator + + "returning " + currentSource); + return currentSource; + } + + public Callbacks getCallbacks() { + return mCallbacks; + } + + @Override + protected IProfileServiceBinder initBinder() { + return new BluetoothLeBroadcastAssistantBinder(this); + } + + @Override + protected boolean start() { + if (DBG) { + Log.d(TAG, "start()"); + } + if (sService != null) { + throw new IllegalStateException("start() called twice"); + } + mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(), + "AdapterService cannot be null when BassClientService starts"); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mStateMachines.clear(); + mStateMachinesThread = new HandlerThread("BassClientService.StateMachines"); + mStateMachinesThread.start(); + mCallbackHandlerThread = new HandlerThread(TAG); + mCallbackHandlerThread.start(); + mCallbacks = new Callbacks(mCallbackHandlerThread.getLooper()); + setBassClientService(this); + mBassUtils = new BassUtils(this); + // Saving PSync stuff for future addition + mDeviceToSyncHandleMap = new HashMap<BluetoothDevice, Integer>(); + mPeriodicAdvertisementResultMap = new HashMap<BluetoothDevice, + PeriodicAdvertisementResult>(); + mSyncHandleToBaseDataMap = new HashMap<Integer, BaseData>(); + mActiveSourceMap = new HashMap<BluetoothDevice, BluetoothDevice>(); + mSearchScanCallback = null; + return true; + } + + @Override + protected boolean stop() { + if (DBG) { + Log.d(TAG, "stop()"); + } + synchronized (mStateMachines) { + for (BassClientStateMachine sm : mStateMachines.values()) { + sm.doQuit(); + sm.cleanup(); + } + mStateMachines.clear(); + } + if (mCallbackHandlerThread != null) { + mCallbackHandlerThread.quitSafely(); + mCallbackHandlerThread = null; + } + if (mStateMachinesThread != null) { + mStateMachinesThread.quitSafely(); + mStateMachinesThread = null; + } + setBassClientService(null); + if (mDeviceToSyncHandleMap != null) { + mDeviceToSyncHandleMap.clear(); + mDeviceToSyncHandleMap = null; + } + if (mActiveSourceMap != null) { + mActiveSourceMap.clear(); + mActiveSourceMap = null; + } + if (mBroadcastSources != null) { + mBroadcastSources.clear(); + mBroadcastSources = null; + } + if (mBassUtils != null) { + mBassUtils.cleanUp(); + mBassUtils = null; + } + return true; + } + + @Override + public boolean onUnbind(Intent intent) { + Log.d(TAG, "Need to unregister app"); + return super.onUnbind(intent); + } + + /** + * getBassUtils + */ + public BassUtils getBassUtils() { + return mBassUtils; + } + + BluetoothDevice getDeviceForSyncHandle(int syncHandle) { + if (mDeviceToSyncHandleMap == null) { + return null; + } + BluetoothDevice device = null; + for (Map.Entry<BluetoothDevice, Integer> entry : mDeviceToSyncHandleMap.entrySet()) { + Integer value = entry.getValue(); + if (value == syncHandle) { + device = entry.getKey(); + break; + } + } + return device; + } + + private static synchronized void setBassClientService(BassClientService instance) { + if (DBG) { + Log.d(TAG, "setBassClientService(): set to: " + instance); + } + sService = instance; + } + + private boolean isValidBroadcastSourceAddition( + BluetoothDevice device, BluetoothLeBroadcastMetadata metaData) { + boolean retval = true; + List<BluetoothLeBroadcastReceiveState> currentAllSources = getAllSources(device); + for (int i = 0; i < currentAllSources.size(); i++) { + BluetoothLeBroadcastReceiveState state = currentAllSources.get(i); + if (metaData.getSourceDevice().equals(state.getSourceDevice()) + && metaData.getSourceAddressType() == state.getSourceAddressType() + && metaData.getSourceAdvertisingSid() == state.getSourceAdvertisingSid() + && metaData.getBroadcastId() == state.getBroadcastId()) { + retval = false; + Log.e(TAG, "isValidBroadcastSourceAddition: fail for " + device + + " metaData: " + metaData); + break; + } + } + return retval; + } + + private boolean hasRoomForBroadcastSourceAddition(BluetoothDevice device) { + List<BluetoothLeBroadcastReceiveState> currentAllSources = getAllSources(device); + return currentAllSources.size() < getMaximumSourceCapacity(device); + } + + private BassClientStateMachine getOrCreateStateMachine(BluetoothDevice device) { + if (device == null) { + Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null"); + return null; + } + synchronized (mStateMachines) { + BassClientStateMachine stateMachine = mStateMachines.get(device); + if (stateMachine != null) { + return stateMachine; + } + // Limit the maximum number of state machines to avoid DoS attack + if (mStateMachines.size() >= MAX_BASS_CLIENT_STATE_MACHINES) { + Log.e(TAG, "Maximum number of Bassclient state machines reached: " + + MAX_BASS_CLIENT_STATE_MACHINES); + return null; + } + log("Creating a new state machine for " + device); + stateMachine = BassClientStateMachine.make(device, + this, mStateMachinesThread.getLooper()); + mStateMachines.put(device, stateMachine); + return stateMachine; + } + } + + /** + * Get the BassClientService instance + * + * @return BassClientService instance + */ + public static synchronized BassClientService getBassClientService() { + if (sService == null) { + Log.w(TAG, "getBassClientService(): service is NULL"); + return null; + } + if (!sService.isAvailable()) { + Log.w(TAG, "getBassClientService(): service is not available"); + return null; + } + return sService; + } + + /** + * Connects the bass profile to the passed in device + * + * @param device is the device with which we will connect the Bass profile + * @return true if BAss profile successfully connected, false otherwise + */ + public boolean connect(BluetoothDevice device) { + if (DBG) { + Log.d(TAG, "connect(): " + device); + } + if (device == null) { + Log.e(TAG, "connect: device is null"); + return false; + } + if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_UNKNOWN) { + Log.e(TAG, "connect: unknown connection policy"); + return false; + } + synchronized (mStateMachines) { + BassClientStateMachine stateMachine = getOrCreateStateMachine(device); + stateMachine.sendMessage(BassClientStateMachine.CONNECT); + } + return true; + } + + /** + * Disconnects Bassclient profile for the passed in device + * + * @param device is the device with which we want to disconnected the BAss client profile + * @return true if Bass client profile successfully disconnected, false otherwise + */ + public boolean disconnect(BluetoothDevice device) { + if (DBG) { + Log.d(TAG, "disconnect(): " + device); + } + if (device == null) { + Log.e(TAG, "disconnect: device is null"); + return false; + } + synchronized (mStateMachines) { + BassClientStateMachine stateMachine = getOrCreateStateMachine(device); + stateMachine.sendMessage(BassClientStateMachine.DISCONNECT); + } + return true; + } + + /** + * Check whether can connect to a peer device. The check considers a number of factors during + * the evaluation. + * + * @param device the peer device to connect to + * @return true if connection is allowed, otherwise false + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public boolean okToConnect(BluetoothDevice device) { + // Check if this is an incoming connection in Quiet mode. + if (mAdapterService.isQuietModeEnabled()) { + Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled"); + return false; + } + // Check connection policy and accept or reject the connection. + int connectionPolicy = getConnectionPolicy(device); + int bondState = mAdapterService.getBondState(device); + // Allow this connection only if the device is bonded. Any attempt to connect while + // bonding would potentially lead to an unauthorized connection. + if (bondState != BluetoothDevice.BOND_BONDED) { + Log.w(TAG, "okToConnect: return false, bondState=" + bondState); + return false; + } else if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN + && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) { + // Otherwise, reject the connection if connectionPolicy is not valid. + Log.w(TAG, "okToConnect: return false, connectionPolicy=" + connectionPolicy); + return false; + } + return true; + } + + /** + * Get connection state of remote device + * + * @param sink the remote device + * @return connection state + */ + public int getConnectionState(BluetoothDevice sink) { + synchronized (mStateMachines) { + BassClientStateMachine sm = getOrCreateStateMachine(sink); + if (sm == null) { + log("getConnectionState returns STATE_DISC"); + return BluetoothProfile.STATE_DISCONNECTED; + } + return sm.getConnectionState(); + } + } + + /** + * Get a list of all LE Audio Broadcast Sinks with the specified connection states. + * @param states states array representing the connection states + * @return a list of devices that match the provided connection states + */ + List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { + ArrayList<BluetoothDevice> devices = new ArrayList<>(); + if (states == null) { + return devices; + } + final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); + if (bondedDevices == null) { + return devices; + } + synchronized (mStateMachines) { + for (BluetoothDevice device : bondedDevices) { + final ParcelUuid[] featureUuids = device.getUuids(); + if (!Utils.arrayContains( + featureUuids, BluetoothUuid.BASS)) { + continue; + } + int connectionState = BluetoothProfile.STATE_DISCONNECTED; + BassClientStateMachine sm = getOrCreateStateMachine(device); + if (sm != null) { + connectionState = sm.getConnectionState(); + } + for (int state : states) { + if (connectionState == state) { + devices.add(device); + break; + } + } + } + return devices; + } + } + + /** + * Get a list of all LE Audio Broadcast Sinks connected with the LE Audio Broadcast Assistant. + * @return list of connected devices + */ + List<BluetoothDevice> getConnectedDevices() { + synchronized (mStateMachines) { + List<BluetoothDevice> devices = new ArrayList<>(); + for (BassClientStateMachine sm : mStateMachines.values()) { + if (sm.isConnected()) { + devices.add(sm.getDevice()); + } + } + log("getConnectedDevices: " + devices); + return devices; + } + } + + /** + * Set the connectionPolicy of the Broadcast Audio Scan Service profile. + * + * <p>The connection policy can be one of: + * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, + * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, + * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN} + * + * @param device paired bluetooth device + * @param connectionPolicy is the connection policy to set to for this profile + * @return true if connectionPolicy is set, false on error + */ + public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { + if (DBG) { + Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); + } + boolean setSuccessfully = + mAdapterService.getDatabase().setProfileConnectionPolicy(device, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, connectionPolicy); + if (setSuccessfully && connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { + connect(device); + } else if (setSuccessfully + && connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { + disconnect(device); + } + return setSuccessfully; + } + + /** + * Get the connection policy of the profile. + * + * <p>The connection policy can be any of: + * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, + * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, + * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN} + * + * @param device paired bluetooth device + * @return connection policy of the device + */ + public int getConnectionPolicy(BluetoothDevice device) { + return mAdapterService + .getDatabase() + .getProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + } + + /** + * Register callbacks that will be invoked during scan offloading. + * + * @param cb callbacks to be invoked + */ + public void registerCallback(IBluetoothLeBroadcastAssistantCallback cb) { + Log.i(TAG, "registerCallback"); + mCallbacks.register(cb); + return; + } + + /** + * Unregister callbacks that are invoked during scan offloading. + * + * @param cb callbacks to be unregistered + */ + public void unregisterCallback(IBluetoothLeBroadcastAssistantCallback cb) { + Log.i(TAG, "unregisterCallback"); + mCallbacks.unregister(cb); + return; + } + + /** + * Search for LE Audio Broadcast Sources on behalf of all devices connected via Broadcast Audio + * Scan Service, filtered by filters + * + * @param filters ScanFilters for finding exact Broadcast Source + */ + public void startSearchingForSources(List<ScanFilter> filters) { + log("startSearchingForSources"); + if (mBluetoothAdapter == null) { + Log.e(TAG, "startSearchingForSources: Adapter is NULL"); + return; + } + BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); + if (scanner == null) { + Log.e(TAG, "startLeScan: cannot get BluetoothLeScanner"); + return; + } + synchronized (mSearchScanCallback) { + if (mSearchScanCallback != null) { + Log.e(TAG, "LE Scan has already started"); + mCallbacks.notifySearchStartFailed(BluetoothStatusCodes.ERROR_UNKNOWN); + return; + } + mSearchScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + log("onScanResult:" + result); + if (callbackType != ScanSettings.CALLBACK_TYPE_ALL_MATCHES) { + // Should not happen + Log.e(TAG, "LE Scan has already started"); + return; + } + ScanRecord scanRecord = result.getScanRecord(); + if (scanRecord == null) { + Log.e(TAG, "Null scan record"); + return; + } + Map<ParcelUuid, byte[]> listOfUuids = scanRecord.getServiceData(); + if (listOfUuids == null) { + Log.e(TAG, "Service data is null"); + return; + } + if (!listOfUuids.containsKey( + BassConstants.BAAS_UUID)) { + return; + } + Message msg = mBassUtils.getAutoAssistScanHandler() + .obtainMessage(BassConstants.AA_SCAN_SUCCESS); + msg.obj = result; + mBassUtils.getAutoAssistScanHandler().sendMessage(msg); + } + + public void onScanFailed(int errorCode) { + Log.e(TAG, "Scan Failure:" + errorCode); + } + }; + ScanSettings settings = new ScanSettings.Builder().setCallbackType( + ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setLegacy(false) + .build(); + if (filters == null) { + filters = new ArrayList<ScanFilter>(); + } + if (!BassUtils.containUuid(filters, BassConstants.BAAS_UUID)) { + filters.add(new ScanFilter.Builder() + .setServiceUuid(BassConstants.BAAS_UUID).build()); + } + scanner.startScan(filters, settings, mSearchScanCallback); + mCallbacks.notifySearchStarted(BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST); + } + } + + /** + * Stops an ongoing search for nearby Broadcast Sources + */ + public void stopSearchingForSources() { + log("stopSearchingForSources"); + BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); + if (scanner == null) { + Log.e(TAG, "startLeScan: cannot get BluetoothLeScanner"); + return; + } + synchronized (mSearchScanCallback) { + if (mSearchScanCallback == null) { + Log.e(TAG, "Scan not started yet"); + mCallbacks.notifySearchStopFailed(BluetoothStatusCodes.ERROR_UNKNOWN); + return; + } + scanner.stopScan(mSearchScanCallback); + mSearchScanCallback = null; + mCallbacks.notifySearchStopped(BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST); + } + } + + /** + * Return true if a search has been started by this application + * @return true if a search has been started by this application + */ + public boolean isSearchInProgress() { + synchronized (mSearchScanCallback) { + return mSearchScanCallback != null; + } + } + + void selectSource(BluetoothDevice sink, ScanResult result, boolean autoTrigger) { + if (!hasRoomForBroadcastSourceAddition(sink)) { + log("selectSource: No more slot"); + return; + } + + synchronized (mStateMachines) { + BassClientStateMachine stateMachine = getOrCreateStateMachine(sink); + Message message = stateMachine.obtainMessage( + BassClientStateMachine.SELECT_BCAST_SOURCE); + message.obj = result; + message.arg1 = autoTrigger ? BassConstants.AUTO : BassConstants.USER; + stateMachine.sendMessage(message); + } + } + + /** + * Add a Broadcast Source to the Broadcast Sink + * + * @param sink Broadcast Sink to which the Broadcast Source should be added + * @param sourceMetadata Broadcast Source metadata to be added to the Broadcast Sink + * @param isGroupOp set to true If Application wants to perform this operation for all + * coordinated set members, False otherwise + */ + public void addSource(BluetoothDevice sink, BluetoothLeBroadcastMetadata sourceMetadata, + boolean isGroupOp) { + log("addSource: device: " + sink + " sourceMetadata" + sourceMetadata + + " isGroupOp" + isGroupOp); + BassClientStateMachine stateMachine = getOrCreateStateMachine(sink); + if (sourceMetadata == null || stateMachine == null) { + log("Error bad parameters: sourceMetadata = " + sourceMetadata); + mCallbacks.notifySourceAddFailed(sink, sourceMetadata, + BluetoothStatusCodes.ERROR_BAD_PARAMETERS); + return; + } + if (!hasRoomForBroadcastSourceAddition(sink)) { + mCallbacks.notifySourceAddFailed(sink, sourceMetadata, + BluetoothStatusCodes.ERROR_REMOTE_NOT_ENOUGH_RESOURCES); + return; + } + if (!isValidBroadcastSourceAddition(sink, sourceMetadata)) { + mCallbacks.notifySourceAddFailed(sink, sourceMetadata, + BluetoothStatusCodes.ERROR_LE_BROADCAST_ASSISTANT_DUPLICATE_ADDITION); + return; + } + Message message = stateMachine.obtainMessage(BassClientStateMachine.ADD_BCAST_SOURCE); + message.obj = sourceMetadata; + stateMachine.sendMessage(message); + } + + /** + * Modify the Broadcast Source information on a Broadcast Sink + * + * @param sink representing the Broadcast Sink to which the Broadcast + * Source should be updated + * @param sourceId source ID as delivered in onSourceAdded + * @param updatedMetadata updated Broadcast Source metadata to be updated on the Broadcast Sink + */ + public void modifySource(BluetoothDevice sink, int sourceId, + BluetoothLeBroadcastMetadata updatedMetadata) { + log("modifySource: device: " + sink + " sourceId " + sourceId); + BassClientStateMachine stateMachine = getOrCreateStateMachine(sink); + if (sourceId == BassConstants.INVALID_SOURCE_ID + || updatedMetadata == null + || stateMachine == null) { + log("Error bad parameters: sourceId = " + sourceId + + " updatedMetadata = " + updatedMetadata); + mCallbacks.notifySourceModifyFailed(sink, sourceId, + BluetoothStatusCodes.ERROR_BAD_PARAMETERS); + return; + } + Message message = stateMachine.obtainMessage(BassClientStateMachine.UPDATE_BCAST_SOURCE); + message.arg1 = sourceId; + message.obj = updatedMetadata; + stateMachine.sendMessage(message); + } + + /** + * Removes the Broadcast Source from a Broadcast Sink + * + * @param sink representing the Broadcast Sink from which a Broadcast + * Source should be removed + * @param sourceId source ID as delivered in onSourceAdded + */ + public void removeSource(BluetoothDevice sink, int sourceId) { + log("removeSource: device = " + sink + + "sourceId " + sourceId); + BassClientStateMachine stateMachine = getOrCreateStateMachine(sink); + if (sourceId == BassConstants.INVALID_SOURCE_ID + || stateMachine == null) { + log("Error bad parameters: sourceId = " + sourceId); + mCallbacks.notifySourceRemoveFailed(sink, sourceId, + BluetoothStatusCodes.ERROR_BAD_PARAMETERS); + return; + } + Message message = stateMachine.obtainMessage(BassClientStateMachine.REMOVE_BCAST_SOURCE); + message.arg1 = sourceId; + stateMachine.sendMessage(message); + } + + /** + * Get information about all Broadcast Sources + * + * @param sink Broadcast Sink from which to get all Broadcast Sources + * @return the list of Broadcast Receive State {@link BluetoothLeBroadcastReceiveState} + */ + public List<BluetoothLeBroadcastReceiveState> getAllSources(BluetoothDevice sink) { + log("getAllSources for " + sink); + synchronized (mStateMachines) { + BassClientStateMachine stateMachine = getOrCreateStateMachine(sink); + if (stateMachine == null) { + log("stateMachine is null"); + return Collections.emptyList(); + } + return stateMachine.getAllSources(); + } + } + + /** + * Get maximum number of sources that can be added to this Broadcast Sink + * + * @param sink Broadcast Sink device + * @return maximum number of sources that can be added to this Broadcast Sink + */ + int getMaximumSourceCapacity(BluetoothDevice sink) { + log("getMaximumSourceCapacity: device = " + sink); + BassClientStateMachine stateMachine = getOrCreateStateMachine(sink); + if (stateMachine == null) { + log("stateMachine is null"); + return 0; + } + return stateMachine.getMaximumSourceCapacity(); + } + + static void log(String msg) { + if (BassConstants.BASS_DBG) { + Log.d(TAG, msg); + } + } + + /** + * Callback handler + */ + static class Callbacks extends Handler { + private static final int MSG_SEARCH_STARTED = 1; + private static final int MSG_SEARCH_STARTED_FAILED = 2; + private static final int MSG_SEARCH_STOPPED = 3; + private static final int MSG_SEARCH_STOPPED_FAILED = 4; + private static final int MSG_SOURCE_FOUND = 5; + private static final int MSG_SOURCE_ADDED = 6; + private static final int MSG_SOURCE_ADDED_FAILED = 7; + private static final int MSG_SOURCE_MODIFIED = 8; + private static final int MSG_SOURCE_MODIFIED_FAILED = 9; + private static final int MSG_SOURCE_REMOVED = 10; + private static final int MSG_SOURCE_REMOVED_FAILED = 11; + private static final int MSG_RECEIVESTATE_CHANGED = 12; + + private final RemoteCallbackList<IBluetoothLeBroadcastAssistantCallback> + mCallbacks = new RemoteCallbackList<>(); + + Callbacks(Looper looper) { + super(looper); + } + + public void register(IBluetoothLeBroadcastAssistantCallback callback) { + mCallbacks.register(callback); + } + + public void unregister(IBluetoothLeBroadcastAssistantCallback callback) { + mCallbacks.unregister(callback); + } + + @Override + public void handleMessage(Message msg) { + final int n = mCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + final IBluetoothLeBroadcastAssistantCallback callback = + mCallbacks.getBroadcastItem(i); + try { + invokeCallback(callback, msg); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(e)); + } + } + mCallbacks.finishBroadcast(); + } + + private class ObjParams { + Object mObj1; + Object mObj2; + ObjParams(Object o1, Object o2) { + mObj1 = o1; + mObj2 = o2; + } + } + + private void invokeCallback(IBluetoothLeBroadcastAssistantCallback callback, + Message msg) throws RemoteException { + final int reason = msg.arg1; + final int sourceId = msg.arg2; + ObjParams param; + BluetoothDevice sink; + + switch (msg.what) { + case MSG_SEARCH_STARTED: + callback.onSearchStarted(reason); + break; + case MSG_SEARCH_STARTED_FAILED: + callback.onSearchStartFailed(reason); + break; + case MSG_SEARCH_STOPPED: + callback.onSearchStopped(reason); + break; + case MSG_SEARCH_STOPPED_FAILED: + callback.onSearchStopFailed(reason); + break; + case MSG_SOURCE_FOUND: + callback.onSourceFound((BluetoothLeBroadcastMetadata) msg.obj); + break; + case MSG_SOURCE_ADDED: + callback.onSourceAdded((BluetoothDevice) msg.obj, sourceId, reason); + break; + case MSG_SOURCE_ADDED_FAILED: + param = (ObjParams) msg.obj; + sink = (BluetoothDevice) param.mObj1; + BluetoothLeBroadcastMetadata metadata = + (BluetoothLeBroadcastMetadata) param.mObj2; + callback.onSourceAddFailed(sink, metadata, reason); + break; + case MSG_SOURCE_MODIFIED: + callback.onSourceModified((BluetoothDevice) msg.obj, sourceId, reason); + break; + case MSG_SOURCE_MODIFIED_FAILED: + callback.onSourceModifyFailed((BluetoothDevice) msg.obj, sourceId, reason); + break; + case MSG_SOURCE_REMOVED: + callback.onSourceRemoved((BluetoothDevice) msg.obj, sourceId, reason); + break; + case MSG_SOURCE_REMOVED_FAILED: + callback.onSourceRemoveFailed((BluetoothDevice) msg.obj, sourceId, reason); + break; + case MSG_RECEIVESTATE_CHANGED: + param = (ObjParams) msg.obj; + sink = (BluetoothDevice) param.mObj1; + BluetoothLeBroadcastReceiveState state = + (BluetoothLeBroadcastReceiveState) param.mObj2; + callback.onReceiveStateChanged(sink, sourceId, state); + break; + default: + Log.e(TAG, "Invalid msg: " + msg.what); + break; + } + } + + void notifySearchStarted(int reason) { + obtainMessage(MSG_SEARCH_STARTED, reason, 0).sendToTarget(); + } + + void notifySearchStartFailed(int reason) { + obtainMessage(MSG_SEARCH_STARTED_FAILED, reason, 0).sendToTarget(); + } + + void notifySearchStopped(int reason) { + obtainMessage(MSG_SEARCH_STOPPED, reason, 0).sendToTarget(); + } + + void notifySearchStopFailed(int reason) { + obtainMessage(MSG_SEARCH_STOPPED_FAILED, reason, 0).sendToTarget(); + } + + void notifySourceFound(BluetoothLeBroadcastMetadata source) { + obtainMessage(MSG_SOURCE_FOUND, 0, 0, source).sendToTarget(); + } + + void notifySourceAdded(BluetoothDevice sink, int sourceId, int reason) { + obtainMessage(MSG_SOURCE_ADDED, reason, sourceId, sink).sendToTarget(); + } + + void notifySourceAddFailed(BluetoothDevice sink, BluetoothLeBroadcastMetadata source, + int reason) { + ObjParams param = new ObjParams(sink, source); + obtainMessage(MSG_SOURCE_ADDED_FAILED, reason, 0, param).sendToTarget(); + } + + void notifySourceModified(BluetoothDevice sink, int sourceId, int reason) { + obtainMessage(MSG_SOURCE_MODIFIED, reason, sourceId, sink).sendToTarget(); + } + + void notifySourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) { + obtainMessage(MSG_SOURCE_MODIFIED_FAILED, reason, sourceId, sink).sendToTarget(); + } + + void notifySourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + obtainMessage(MSG_SOURCE_REMOVED, reason, sourceId, sink).sendToTarget(); + } + + void notifySourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) { + obtainMessage(MSG_SOURCE_REMOVED_FAILED, reason, sourceId, sink).sendToTarget(); + } + + void notifyReceiveStateChanged(BluetoothDevice sink, int sourceId, + BluetoothLeBroadcastReceiveState state) { + ObjParams param = new ObjParams(sink, state); + obtainMessage(MSG_RECEIVESTATE_CHANGED, 0, sourceId, param).sendToTarget(); + } + } + + /** Binder object: must be a static class or memory leak may occur */ + @VisibleForTesting + static class BluetoothLeBroadcastAssistantBinder extends IBluetoothLeBroadcastAssistant.Stub + implements IProfileServiceBinder { + private BassClientService mService; + + private BassClientService getService() { + if (!Utils.checkCallerIsSystemOrActiveUser(TAG) + || !Utils.checkServiceAvailable(mService, TAG)) { + return null; + } + return mService; + } + + BluetoothLeBroadcastAssistantBinder(BassClientService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @Override + public int getConnectionState(BluetoothDevice sink) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return BluetoothProfile.STATE_DISCONNECTED; + } + return service.getConnectionState(sink); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return BluetoothProfile.STATE_DISCONNECTED; + } + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return Collections.emptyList(); + } + } + + @Override + public List<BluetoothDevice> getConnectedDevices() { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return Collections.emptyList(); + } + } + + @Override + public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return false; + } + return service.setConnectionPolicy(device, connectionPolicy); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return false; + } + } + + @Override + public int getConnectionPolicy(BluetoothDevice device) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; + } + return service.getConnectionPolicy(device); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; + } + } + + @Override + public void registerCallback(IBluetoothLeBroadcastAssistantCallback cb) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.registerCallback(cb); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } + } + + @Override + public void unregisterCallback(IBluetoothLeBroadcastAssistantCallback cb) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.unregisterCallback(cb); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } + } + + @Override + public void startSearchingForSources(List<ScanFilter> filters) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.startSearchingForSources(filters); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } + } + + @Override + public void stopSearchingForSources() { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.stopSearchingForSources(); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } + } + + @Override + public boolean isSearchInProgress() { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return false; + } + return service.isSearchInProgress(); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return false; + } + } + + @Override + public void addSource( + BluetoothDevice sink, BluetoothLeBroadcastMetadata sourceMetadata, + boolean isGroupOp) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.addSource(sink, sourceMetadata, isGroupOp); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } + } + + @Override + public void modifySource( + BluetoothDevice sink, int sourceId, BluetoothLeBroadcastMetadata updatedMetadata) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.modifySource(sink, sourceId, updatedMetadata); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } + } + + @Override + public void removeSource(BluetoothDevice sink, int sourceId) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.removeSource(sink, sourceId); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } + } + + @Override + public List<BluetoothLeBroadcastReceiveState> getAllSources(BluetoothDevice sink) { + try { + BassClientService service = getService(); + if (sink == null) { + Log.e(TAG, "Service is null"); + return Collections.emptyList(); + } + return service.getAllSources(sink); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return Collections.emptyList(); + } + } + + @Override + public int getMaximumSourceCapacity(BluetoothDevice sink) { + try { + BassClientService service = getService(); + if (service == null) { + Log.e(TAG, "Service is null"); + return 0; + } + return service.getMaximumSourceCapacity(sink); + } catch (RuntimeException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return 0; + } + } + } +} diff --git a/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java b/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java new file mode 100755 index 0000000000..39ed58e211 --- /dev/null +++ b/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java @@ -0,0 +1,1820 @@ +/* + * Copyright 2022 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. + */ + +/** + * Bluetooth Bassclient StateMachine. There is one instance per remote device. + * - "Disconnected" and "Connected" are steady states. + * - "Connecting" and "Disconnecting" are transient states until the + * connection / disconnection is completed. + * - "ConnectedProcessing" is an intermediate state to ensure, there is only + * one Gatt transaction from the profile at any point of time + * + * + * (Disconnected) + * | ^ + * CONNECT | | DISCONNECTED + * V | + * (Connecting)<--->(Disconnecting) + * | ^ + * CONNECTED | | DISCONNECT + * V | + * (Connected) + * | ^ + * GATT_TXN | | GATT_TXN_DONE/GATT_TXN_TIMEOUT + * V | + * (ConnectedProcessing) + * NOTES: + * - If state machine is in "Connecting" state and the remote device sends + * DISCONNECT request, the state machine transitions to "Disconnecting" state. + * - Similarly, if the state machine is in "Disconnecting" state and the remote device + * sends CONNECT request, the state machine transitions to "Connecting" state. + * - Whenever there is any Gatt Write/read, State machine will moved "ConnectedProcessing" and + * all other requests (add, update, remove source) operations will be deferred in + * "ConnectedProcessing" state + * - Once the gatt transaction is done (or after a specified timeout of no response), + * State machine will move back to "Connected" and try to process the deferred requests + * as needed + * + * DISCONNECT + * (Connecting) ---------------> (Disconnecting) + * <--------------- + * CONNECT + * + */ +package com.android.bluetooth.bass_client; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothLeAudioContentMetadata; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; +import android.bluetooth.le.PeriodicAdvertisingCallback; +import android.bluetooth.le.PeriodicAdvertisingManager; +import android.bluetooth.le.PeriodicAdvertisingReport; +import android.bluetooth.le.ScanRecord; +import android.bluetooth.le.ScanResult; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelUuid; +import android.provider.DeviceConfig; +import android.util.Log; + +import com.android.bluetooth.btservice.ProfileService; +import com.android.bluetooth.btservice.ServiceFactory; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Scanner; +import java.util.stream.IntStream; + +class BassClientStateMachine extends StateMachine { + private static final String TAG = "BassClientStateMachine"; + private static final byte[] REMOTE_SCAN_STOP = {00}; + private static final byte[] REMOTE_SCAN_START = {01}; + private static final byte OPCODE_ADD_SOURCE = 0x02; + private static final byte OPCODE_UPDATE_SOURCE = 0x03; + private static final byte OPCODE_SET_BCAST_PIN = 0x04; + private static final byte OPCODE_REMOVE_SOURCE = 0x05; + private static final int ADD_SOURCE_FIXED_LENGTH = 16; + private static final int UPDATE_SOURCE_FIXED_LENGTH = 6; + + static final int CONNECT = 1; + static final int DISCONNECT = 2; + static final int CONNECTION_STATE_CHANGED = 3; + static final int GATT_TXN_PROCESSED = 4; + static final int READ_BASS_CHARACTERISTICS = 5; + static final int START_SCAN_OFFLOAD = 6; + static final int STOP_SCAN_OFFLOAD = 7; + static final int SELECT_BCAST_SOURCE = 8; + static final int ADD_BCAST_SOURCE = 9; + static final int UPDATE_BCAST_SOURCE = 10; + static final int SET_BCAST_CODE = 11; + static final int REMOVE_BCAST_SOURCE = 12; + static final int GATT_TXN_TIMEOUT = 13; + static final int PSYNC_ACTIVE_TIMEOUT = 14; + static final int CONNECT_TIMEOUT = 15; + + /*key is combination of sourceId, Address and advSid for this hashmap*/ + private final Map<Integer, BluetoothLeBroadcastReceiveState> + mBluetoothLeBroadcastReceiveStates = + new HashMap<Integer, BluetoothLeBroadcastReceiveState>(); + private final Disconnected mDisconnected = new Disconnected(); + private final Connected mConnected = new Connected(); + private final Connecting mConnecting = new Connecting(); + private final Disconnecting mDisconnecting = new Disconnecting(); + private final ConnectedProcessing mConnectedProcessing = new ConnectedProcessing(); + private final List<BluetoothGattCharacteristic> mBroadcastCharacteristics = + new ArrayList<BluetoothGattCharacteristic>(); + private final BluetoothDevice mDevice; + + private boolean mIsAllowedList = false; + private int mLastConnectionState = -1; + private boolean mMTUChangeRequested = false; + private boolean mDiscoveryInitiated = false; + private BassClientService mService; + private BluetoothGatt mBluetoothGatt = null; + + private BluetoothGattCharacteristic mBroadcastScanControlPoint; + private boolean mFirstTimeBisDiscovery = false; + private int mPASyncRetryCounter = 0; + private ScanResult mScanRes = null; + private int mNumOfBroadcastReceiverStates = 0; + private BluetoothAdapter mBluetoothAdapter = + BluetoothAdapter.getDefaultAdapter(); + private ServiceFactory mFactory = new ServiceFactory(); + private int mPendingOperation = -1; + private byte mPendingSourceId = -1; + private BluetoothLeBroadcastMetadata mPendingMetadata = null; + private BluetoothLeBroadcastReceiveState mSetBroadcastPINRcvState = null; + private boolean mSetBroadcastCodePending = false; + // Psync and PAST interfaces + private PeriodicAdvertisingManager mPeriodicAdvManager; + private boolean mAutoAssist = false; + private boolean mAutoTriggered = false; + private boolean mNoStopScanOffload = false; + private boolean mDefNoPAS = false; + private boolean mForceSB = false; + private int mBroadcastSourceIdLength = 3; + private byte mNextSourceId = 0; + + BassClientStateMachine(BluetoothDevice device, BassClientService svc, Looper looper) { + super(TAG + "(" + device.toString() + ")", looper); + mDevice = device; + mService = svc; + addState(mDisconnected); + addState(mDisconnecting); + addState(mConnected); + addState(mConnecting); + addState(mConnectedProcessing); + setInitialState(mDisconnected); + // PSYNC and PAST instances + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (mBluetoothAdapter != null) { + mPeriodicAdvManager = mBluetoothAdapter.getPeriodicAdvertisingManager(); + } + mIsAllowedList = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, + "persist.vendor.service.bt.wl", true); + mDefNoPAS = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, + "persist.vendor.service.bt.defNoPAS", false); + mForceSB = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, + "persist.vendor.service.bt.forceSB", false); + } + + static BassClientStateMachine make(BluetoothDevice device, + BassClientService svc, Looper looper) { + Log.d(TAG, "make for device " + device); + BassClientStateMachine BassclientSm = new BassClientStateMachine(device, svc, looper); + BassclientSm.start(); + return BassclientSm; + } + + public void doQuit() { + log("doQuit for device " + mDevice); + quitNow(); + } + + public void cleanup() { + log("cleanup for device " + mDevice); + clearCharsCache(); + + if (mBluetoothGatt != null) { + log("disconnect gatt"); + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + mBluetoothGatt = null; + } + mPendingOperation = -1; + mPendingSourceId = -1; + mPendingMetadata = null; + } + + BluetoothLeBroadcastReceiveState getBroadcastReceiveStateForSourceDevice( + BluetoothDevice srcDevice) { + List<BluetoothLeBroadcastReceiveState> currentSources = getAllSources(); + BluetoothLeBroadcastReceiveState state = null; + for (int i = 0; i < currentSources.size(); i++) { + BluetoothDevice device = currentSources.get(i).getSourceDevice(); + if (device != null && device.equals(srcDevice)) { + state = currentSources.get(i); + Log.e(TAG, + "getBroadcastReceiveStateForSourceDevice: returns for: " + + srcDevice + "&srcInfo" + state); + return state; + } + } + return null; + } + + BluetoothLeBroadcastReceiveState getBroadcastReceiveStateForSourceId(int sourceId) { + List<BluetoothLeBroadcastReceiveState> currentSources = getAllSources(); + for (int i = 0; i < currentSources.size(); i++) { + if (sourceId == currentSources.get(i).getSourceId()) { + return currentSources.get(i); + } + } + return null; + } + + void parseBaseData(BluetoothDevice device, int syncHandle, byte[] serviceData) { + log("parseBaseData" + Arrays.toString(serviceData)); + BaseData base = BaseData.parseBaseData(serviceData); + if (base != null) { + mService.updateBase(syncHandle, base); + base.print(); + if (mAutoTriggered) { + // successful auto periodic synchrnization with source + log("auto triggered assist"); + mAutoTriggered = false; + // perform PAST with this device + BluetoothDevice srcDevice = mService.getDeviceForSyncHandle(syncHandle); + if (srcDevice != null) { + BluetoothLeBroadcastReceiveState recvState = + getBroadcastReceiveStateForSourceDevice(srcDevice); + processPASyncState(recvState); + } else { + Log.w(TAG, "Autoassist: no matching device"); + } + } + } else { + Log.e(TAG, "Seems BASE is not in parsable format"); + if (!mAutoTriggered) { + BluetoothDevice srcDevice = mService.getDeviceForSyncHandle(syncHandle); + cancelActiveSync(srcDevice); + } else { + mAutoTriggered = false; + } + } + } + + void parseScanRecord(int syncHandle, ScanRecord record) { + log("parseScanRecord" + record); + BluetoothDevice srcDevice = mService.getDeviceForSyncHandle(syncHandle); + Map<ParcelUuid, byte[]> bmsAdvDataMap = record.getServiceData(); + if (bmsAdvDataMap != null) { + for (Map.Entry<ParcelUuid, byte[]> entry : bmsAdvDataMap.entrySet()) { + log("ParcelUUid = " + entry.getKey() + ", Value = " + entry.getValue()); + } + } + byte[] advData = record.getServiceData(BassConstants.BASIC_AUDIO_UUID); + if (advData != null) { + parseBaseData(mDevice, syncHandle, advData); + } else { + Log.e(TAG, "No service data in Scan record"); + if (!mAutoTriggered) { + cancelActiveSync(srcDevice); + } else { + mAutoTriggered = false; + } + } + } + + private boolean selectSource( + ScanResult scanRes, boolean autoTriggered) { + log("selectSource: ScanResult " + scanRes); + mAutoTriggered = autoTriggered; + mFirstTimeBisDiscovery = true; + mPASyncRetryCounter = 1; + // Cache Scan res for Retrys + mScanRes = scanRes; + /*This is an override case + if Previous sync is still active, cancel It + But don't stop the Scan offload as we still trying to assist remote*/ + mNoStopScanOffload = true; + cancelActiveSync(null); + try { + mPeriodicAdvManager.registerSync(scanRes, 0, + BassConstants.PSYNC_TIMEOUT, mPeriodicAdvCallback); + } catch (IllegalArgumentException ex) { + Log.w(TAG, "registerSync:IllegalArgumentException"); + Message message = obtainMessage(STOP_SCAN_OFFLOAD); + sendMessage(message); + return false; + } + // updating mainly for Address type and PA Interval here + // extract BroadcastId from ScanResult + ScanRecord scanRecord = scanRes.getScanRecord(); + if (scanRecord != null) { + Map<ParcelUuid, byte[]> listOfUuids = scanRecord.getServiceData(); + int broadcastId = BassConstants.INVALID_BROADCAST_ID; + if (listOfUuids != null) { + if (listOfUuids.containsKey(BassConstants.BAAS_UUID)) { + byte[] bId = listOfUuids.get(BassConstants.BAAS_UUID); + broadcastId = BassUtils.parseBroadcastId(bId); + } + } + mService.updatePeriodicAdvertisementResultMap( + scanRes.getDevice(), + scanRes.getDevice().getAddressType(), + BassConstants.INVALID_SYNC_HANDLE, + BassConstants.INVALID_ADV_SID, + scanRes.getPeriodicAdvertisingInterval(), + broadcastId); + } + return true; + } + + private void cancelActiveSync(BluetoothDevice sourceDev) { + log("cancelActiveSync"); + boolean isCancelSyncNeeded = false; + BluetoothDevice activeSyncedSrc = mService.getActiveSyncedSource(mDevice); + if (activeSyncedSrc != null) { + if (sourceDev == null) { + isCancelSyncNeeded = true; + } else if (activeSyncedSrc.equals(sourceDev)) { + isCancelSyncNeeded = true; + } + } + if (isCancelSyncNeeded) { + removeMessages(PSYNC_ACTIVE_TIMEOUT); + try { + log("calling unregisterSync"); + mPeriodicAdvManager.unregisterSync(mPeriodicAdvCallback); + } catch (IllegalArgumentException ex) { + Log.w(TAG, "unregisterSync:IllegalArgumentException"); + } + mService.clearPeriodicAdvertisementResult(activeSyncedSrc); + mService.setActiveSyncedSource(mDevice, null); + if (!mNoStopScanOffload) { + // trigger scan stop here + Message message = obtainMessage(STOP_SCAN_OFFLOAD); + sendMessage(message); + } + } + mNoStopScanOffload = false; + } + + /** Internal periodc Advertising manager callback */ + private PeriodicAdvertisingCallback mPeriodicAdvCallback = + new PeriodicAdvertisingCallback() { + @Override + public void onSyncEstablished( + int syncHandle, + BluetoothDevice device, + int advertisingSid, + int skip, + int timeout, + int status) { + log("onSyncEstablished syncHandle" + syncHandle + + "device" + device + + "advertisingSid" + advertisingSid + + "skip" + skip + "timeout" + timeout + + "status" + status); + if (status == BluetoothGatt.GATT_SUCCESS) { + // updates syncHandle, advSid + mService.updatePeriodicAdvertisementResultMap( + device, + BassConstants.INVALID_ADV_ADDRESS_TYPE, + syncHandle, + advertisingSid, + BassConstants.INVALID_ADV_INTERVAL, + BassConstants.INVALID_BROADCAST_ID); + sendMessageDelayed(PSYNC_ACTIVE_TIMEOUT, + BassConstants.PSYNC_ACTIVE_TIMEOUT_MS); + mService.setActiveSyncedSource(mDevice, device); + } else { + log("failed to sync to PA" + mPASyncRetryCounter); + mScanRes = null; + if (!mAutoTriggered) { + Message message = obtainMessage(STOP_SCAN_OFFLOAD); + sendMessage(message); + } + mAutoTriggered = false; + } + } + + @Override + public void onPeriodicAdvertisingReport(PeriodicAdvertisingReport report) { + log("onPeriodicAdvertisingReport"); + // Parse the BIS indices from report's service data + if (mFirstTimeBisDiscovery) { + parseScanRecord(report.getSyncHandle(), report.getData()); + mFirstTimeBisDiscovery = false; + } + } + + @Override + public void onSyncLost(int syncHandle) { + log("OnSyncLost" + syncHandle); + BluetoothDevice srcDevice = mService.getDeviceForSyncHandle(syncHandle); + cancelActiveSync(srcDevice); + } + }; + + private void broadcastReceiverState( + BluetoothLeBroadcastReceiveState state, int sourceId) { + log("broadcastReceiverState: " + mDevice); + mService.getCallbacks().notifyReceiveStateChanged(mDevice, sourceId, state); + } + + private static boolean isEmpty(final byte[] data) { + return IntStream.range(0, data.length).parallel().allMatch(i -> data[i] == 0); + } + + private void processPASyncState(BluetoothLeBroadcastReceiveState recvState) { + log("processPASyncState " + recvState); + int serviceData = 0; + if (recvState == null) { + Log.e(TAG, "processPASyncState: recvState is null"); + return; + } + int state = recvState.getPaSyncState(); + if (state == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCINFO_REQUEST) { + log("Initiate PAST procedure"); + PeriodicAdvertisementResult result = + mService.getPeriodicAdvertisementResult( + recvState.getSourceDevice()); + if (result != null) { + int syncHandle = result.getSyncHandle(); + log("processPASyncState: syncHandle " + result.getSyncHandle()); + if (syncHandle != BassConstants.INVALID_SYNC_HANDLE) { + serviceData = 0x000000FF & recvState.getSourceId(); + serviceData = serviceData << 8; + //advA matches EXT_ADV_ADDRESS + //also matches source address (as we would have written) + serviceData = serviceData + & (~BassConstants.ADV_ADDRESS_DONT_MATCHES_EXT_ADV_ADDRESS); + serviceData = serviceData + & (~BassConstants.ADV_ADDRESS_DONT_MATCHES_SOURCE_ADV_ADDRESS); + log("Initiate PAST for :" + mDevice + "syncHandle:" + syncHandle + + "serviceData" + serviceData); + mPeriodicAdvManager.transferSync(mDevice, serviceData, syncHandle); + } + } else { + Log.e(TAG, "There is no valid sync handle for this Source"); + if (mAutoAssist) { + //initiate Auto Assist procedure for this device + mService.getBassUtils().triggerAutoAssist(recvState); + } + } + } else if (state == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED + || state == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_NO_PAST) { + Message message = obtainMessage(STOP_SCAN_OFFLOAD); + sendMessage(message); + } + } + + private void checkAndUpdateBroadcastCode(BluetoothLeBroadcastReceiveState recvState) { + log("checkAndUpdateBroadcastCode"); + // non colocated case, Broadcast PIN should have been updated from lyaer + // If there is pending one process it Now + if (recvState.getBigEncryptionState() + == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_CODE_REQUIRED + && mSetBroadcastCodePending) { + log("Update the Broadcast now"); + Message m = obtainMessage(BassClientStateMachine.SET_BCAST_CODE); + m.obj = mSetBroadcastPINRcvState; + sendMessage(m); + mSetBroadcastCodePending = false; + mSetBroadcastPINRcvState = null; + } + } + + private BluetoothLeBroadcastReceiveState parseBroadcastReceiverState( + byte[] receiverState) { + byte sourceId = 0; + if (receiverState.length > 0) { + sourceId = receiverState[BassConstants.BCAST_RCVR_STATE_SRC_ID_IDX]; + } + log("processBroadcastReceiverState: receiverState length: " + receiverState.length); + + BluetoothLeBroadcastReceiveState recvState = null; + if (receiverState.length == 0 + || isEmpty(Arrays.copyOfRange(receiverState, 1, receiverState.length - 1))) { + if (mPendingOperation == REMOVE_BCAST_SOURCE) { + recvState = new BluetoothLeBroadcastReceiveState(mPendingSourceId, + BluetoothDevice.ADDRESS_TYPE_UNKNOWN, // sourceAddressType + null, // sourceDevice + 0, // sourceAdvertisingSid + 0, // broadcastId + BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE, // paSyncState + // bigEncryptionState + BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, + null, // badCode + 0, // numSubgroups + null, // bisSyncState + null // subgroupMetadata + ); + } else if (receiverState.length == 0) { + if (mBluetoothLeBroadcastReceiveStates != null) { + mNextSourceId = (byte) mBluetoothLeBroadcastReceiveStates.size(); + } + if (mNextSourceId >= mNumOfBroadcastReceiverStates) { + Log.e(TAG, "reached the remote supported max SourceInfos"); + return null; + } + mNextSourceId++; + recvState = new BluetoothLeBroadcastReceiveState(mNextSourceId, + BluetoothDevice.ADDRESS_TYPE_UNKNOWN, // sourceAddressType + null, // sourceDevice + 0, // sourceAdvertisingSid + 0, // broadcastId + BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE, // paSyncState + // bigEncryptionState + BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, + null, // badCode + 0, // numSubgroups + null, // bisSyncState + null // subgroupMetadata + ); + } + } else { + byte metaDataSyncState = receiverState[BassConstants.BCAST_RCVR_STATE_PA_SYNC_IDX]; + byte encryptionStatus = receiverState[BassConstants.BCAST_RCVR_STATE_ENC_STATUS_IDX]; + byte[] badBroadcastCode = null; + if (encryptionStatus + == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE) { + badBroadcastCode = new byte[BassConstants.BCAST_RCVR_STATE_BADCODE_SIZE]; + System.arraycopy( + receiverState, + BassConstants.BCAST_RCVR_STATE_BADCODE_START_IDX, + badBroadcastCode, + 0, + BassConstants.BCAST_RCVR_STATE_BADCODE_SIZE); + badBroadcastCode = reverseBytes(badBroadcastCode); + } + byte numSubGroups = receiverState[BassConstants.BCAST_RCVR_STATE_BADCODE_START_IDX + + BassConstants.BCAST_RCVR_STATE_BADCODE_SIZE]; + int offset = BassConstants.BCAST_RCVR_STATE_BADCODE_START_IDX + + BassConstants.BCAST_RCVR_STATE_BADCODE_SIZE + 1; + ArrayList<BluetoothLeAudioContentMetadata> metadataList = + new ArrayList<BluetoothLeAudioContentMetadata>(); + ArrayList<Long> audioSyncState = new ArrayList<Long>(); + for (int i = 0; i < numSubGroups; i++) { + byte[] audioSyncIndex = new byte[BassConstants.BCAST_RCVR_STATE_BIS_SYNC_SIZE]; + System.arraycopy(receiverState, offset, audioSyncIndex, 0, + BassConstants.BCAST_RCVR_STATE_BIS_SYNC_SIZE); + offset += BassConstants.BCAST_RCVR_STATE_BIS_SYNC_SIZE; + log("BIS index byte array: "); + BassUtils.printByteArray(audioSyncIndex); + ByteBuffer wrapped = ByteBuffer.wrap(reverseBytes(audioSyncIndex)); + audioSyncState.add(wrapped.getLong()); + + byte metaDataLength = receiverState[offset++]; + if (metaDataLength > 0) { + log("metadata of length: " + metaDataLength + "is available"); + byte[] metaData = new byte[metaDataLength]; + System.arraycopy(receiverState, offset, metaData, 0, metaDataLength); + offset += metaDataLength; + metaData = reverseBytes(metaData); + metadataList.add(BluetoothLeAudioContentMetadata.fromRawBytes(metaData)); + } + } + byte[] broadcastIdBytes = new byte[mBroadcastSourceIdLength]; + System.arraycopy( + receiverState, + BassConstants.BCAST_RCVR_STATE_SRC_BCAST_ID_START_IDX, + broadcastIdBytes, + 0, + mBroadcastSourceIdLength); + int broadcastId = BassUtils.parseBroadcastId(broadcastIdBytes); + BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); + byte[] sourceAddress = new byte[BassConstants.BCAST_RCVR_STATE_SRC_ADDR_SIZE]; + System.arraycopy( + receiverState, + BassConstants.BCAST_RCVR_STATE_SRC_ADDR_START_IDX, + sourceAddress, + 0, + BassConstants.BCAST_RCVR_STATE_SRC_ADDR_SIZE); + byte sourceAddressType = receiverState[BassConstants + .BCAST_RCVR_STATE_SRC_ADDR_TYPE_IDX]; + byte[] revAddress = reverseBytes(sourceAddress); + String address = String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X", + revAddress[0], revAddress[1], revAddress[2], + revAddress[3], revAddress[4], revAddress[5]); + BluetoothDevice device = btAdapter.getRemoteLeDevice( + address, sourceAddressType); + byte sourceAdvSid = receiverState[BassConstants.BCAST_RCVR_STATE_SRC_ADV_SID_IDX]; + recvState = new BluetoothLeBroadcastReceiveState( + sourceId, + (int) sourceAddressType, + device, + sourceAdvSid, + broadcastId, + (int) metaDataSyncState, + (int) encryptionStatus, + badBroadcastCode, + numSubGroups, + audioSyncState, + metadataList); + } + return recvState; + } + + private void processBroadcastReceiverState( + byte[] receiverState, BluetoothGattCharacteristic characteristic) { + log("processBroadcastReceiverState: characteristic:" + characteristic); + BluetoothLeBroadcastReceiveState recvState = parseBroadcastReceiverState( + receiverState); + if (recvState == null || recvState.getSourceId() == -1) { + log("Null recvState or processBroadcastReceiverState: invalid index: " + + recvState.getSourceId()); + return; + } + BluetoothLeBroadcastReceiveState oldRecvState = + mBluetoothLeBroadcastReceiveStates.get(characteristic.getInstanceId()); + if (oldRecvState == null) { + log("Initial Read and Populating values"); + if (mBluetoothLeBroadcastReceiveStates.size() == mNumOfBroadcastReceiverStates) { + Log.e(TAG, "reached the Max SourceInfos"); + return; + } + mBluetoothLeBroadcastReceiveStates.put(characteristic.getInstanceId(), recvState); + checkAndUpdateBroadcastCode(recvState); + processPASyncState(recvState); + } else { + log("old sourceInfo: " + oldRecvState); + log("new sourceInfo: " + recvState); + mBluetoothLeBroadcastReceiveStates.replace(characteristic.getInstanceId(), recvState); + if (oldRecvState.getSourceDevice() == null) { + log("New Source Addition"); + mService.getCallbacks().notifySourceAdded(mDevice, + recvState.getSourceId(), BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST); + if (mPendingMetadata != null) { + mService.updateSourceInternal(recvState.getSourceId(), mPendingMetadata); + } + checkAndUpdateBroadcastCode(recvState); + processPASyncState(recvState); + } else { + if (recvState.getSourceDevice() == null) { + BluetoothDevice removedDevice = oldRecvState.getSourceDevice(); + log("sourceInfo removal" + removedDevice); + cancelActiveSync(removedDevice); + mService.updateSourceInternal(oldRecvState.getSourceId(), null); + mService.getCallbacks().notifySourceRemoved(mDevice, + oldRecvState.getSourceId(), + BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST); + } else { + log("update to an existing recvState"); + mService.updateSourceInternal(recvState.getSourceId(), mPendingMetadata); + mService.getCallbacks().notifySourceModified(mDevice, + recvState.getSourceId(), BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST); + checkAndUpdateBroadcastCode(recvState); + processPASyncState(recvState); + } + } + } + broadcastReceiverState(recvState, recvState.getSourceId()); + } + + // Implements callback methods for GATT events that the app cares about. + // For example, connection change and services discovered. + private final BluetoothGattCallback mGattCallback = + new BluetoothGattCallback() { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + boolean isStateChanged = false; + log("onConnectionStateChange : Status=" + status + "newState" + newState); + if (newState == BluetoothProfile.STATE_CONNECTED + && getConnectionState() != BluetoothProfile.STATE_CONNECTED) { + isStateChanged = true; + Log.w(TAG, "Bassclient Connected from Disconnected state: " + mDevice); + if (mService.okToConnect(mDevice)) { + log("Bassclient Connected to: " + mDevice); + if (mBluetoothGatt != null) { + log("Attempting to start service discovery:" + + mBluetoothGatt.discoverServices()); + mDiscoveryInitiated = true; + } + } else if (mBluetoothGatt != null) { + // Reject the connection + Log.w(TAG, "Bassclient Connect request rejected: " + mDevice); + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + mBluetoothGatt = null; + // force move to disconnected + newState = BluetoothProfile.STATE_DISCONNECTED; + } + } else if (newState == BluetoothProfile.STATE_DISCONNECTED + && getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) { + isStateChanged = true; + log("Disconnected from Bass GATT server."); + } + if (isStateChanged) { + Message m = obtainMessage(CONNECTION_STATE_CHANGED); + m.obj = newState; + sendMessage(m); + } + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + log("onServicesDiscovered:" + status); + if (mDiscoveryInitiated) { + mDiscoveryInitiated = false; + if (status == BluetoothGatt.GATT_SUCCESS && mBluetoothGatt != null) { + mBluetoothGatt.requestMtu(BassConstants.BASS_MAX_BYTES); + mMTUChangeRequested = true; + } else { + Log.w(TAG, "onServicesDiscovered received: " + + status + "mBluetoothGatt" + mBluetoothGatt); + } + } else { + log("remote initiated callback"); + } + } + + @Override + public void onCharacteristicRead( + BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + log("onCharacteristicRead:: status: " + status + "char:" + characteristic); + if (status == BluetoothGatt.GATT_SUCCESS && characteristic.getUuid() + .equals(BassConstants.BASS_BCAST_RECEIVER_STATE)) { + log("onCharacteristicRead: BASS_BCAST_RECEIVER_STATE: status" + status); + logByteArray("Received ", characteristic.getValue(), 0, + characteristic.getValue().length); + if (characteristic.getValue() == null) { + Log.e(TAG, "Remote receiver state is NULL"); + return; + } + processBroadcastReceiverState(characteristic.getValue(), characteristic); + } + // switch to receiving notifications after initial characteristic read + BluetoothGattDescriptor desc = characteristic + .getDescriptor(BassConstants.CLIENT_CHARACTERISTIC_CONFIG); + if (mBluetoothGatt != null && desc != null) { + log("Setting the value for Desc"); + mBluetoothGatt.setCharacteristicNotification(characteristic, true); + desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + mBluetoothGatt.writeDescriptor(desc); + } else { + Log.w(TAG, "CCC for " + characteristic + "seem to be not present"); + // at least move the SM to stable state + Message m = obtainMessage(GATT_TXN_PROCESSED); + m.arg1 = status; + sendMessage(m); + } + } + + @Override + public void onDescriptorWrite( + BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + log("onDescriptorWrite"); + if (status == BluetoothGatt.GATT_SUCCESS + && descriptor.getUuid() + .equals(BassConstants.CLIENT_CHARACTERISTIC_CONFIG)) { + log("CCC write resp"); + } + + // Move the SM to connected so further reads happens + Message m = obtainMessage(GATT_TXN_PROCESSED); + m.arg1 = status; + sendMessage(m); + } + + @Override + public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + log("onMtuChanged: mtu:" + mtu); + if (mMTUChangeRequested && mBluetoothGatt != null) { + acquireAllBassChars(); + mMTUChangeRequested = false; + } else { + log("onMtuChanged is remote initiated trigger, mBluetoothGatt:" + + mBluetoothGatt); + } + } + + @Override + public void onCharacteristicChanged( + BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + log("onCharacteristicChanged :: " + characteristic.getUuid().toString()); + if (characteristic.getUuid().equals(BassConstants.BASS_BCAST_RECEIVER_STATE)) { + log("onCharacteristicChanged is rcvr State :: " + + characteristic.getUuid().toString()); + if (characteristic.getValue() == null) { + Log.e(TAG, "Remote receiver state is NULL"); + return; + } + logByteArray("onCharacteristicChanged: Received ", + characteristic.getValue(), + 0, + characteristic.getValue().length); + processBroadcastReceiverState(characteristic.getValue(), characteristic); + } + } + + @Override + public void onCharacteristicWrite( + BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + log("onCharacteristicWrite: " + characteristic.getUuid().toString() + + "status:" + status); + if (status == 0 + && characteristic.getUuid() + .equals(BassConstants.BASS_BCAST_AUDIO_SCAN_CTRL_POINT)) { + log("BASS_BCAST_AUDIO_SCAN_CTRL_POINT is written successfully"); + } + Message m = obtainMessage(GATT_TXN_PROCESSED); + m.arg1 = status; + sendMessage(m); + } + }; + + /** + * getAllSources + */ + public List<BluetoothLeBroadcastReceiveState> getAllSources() { + log("getAllSources"); + List list = new ArrayList(mBluetoothLeBroadcastReceiveStates.values()); + return list; + } + + void acquireAllBassChars() { + clearCharsCache(); + BluetoothGattService service = null; + if (mBluetoothGatt != null) { + log("getting Bass Service handle"); + service = mBluetoothGatt.getService(BassConstants.BASS_UUID); + } + if (service == null) { + log("acquireAllBassChars: BASS service not found"); + return; + } + log("found BASS_SERVICE"); + List<BluetoothGattCharacteristic> allChars = service.getCharacteristics(); + int numOfChars = allChars.size(); + mNumOfBroadcastReceiverStates = numOfChars - 1; + log("Total number of chars" + numOfChars); + for (int i = 0; i < allChars.size(); i++) { + if (allChars.get(i).getUuid().equals(BassConstants.BASS_BCAST_AUDIO_SCAN_CTRL_POINT)) { + mBroadcastScanControlPoint = allChars.get(i); + log("Index of ScanCtrlPoint:" + i); + } else { + log("Reading " + i + "th ReceiverState"); + mBroadcastCharacteristics.add(allChars.get(i)); + Message m = obtainMessage(READ_BASS_CHARACTERISTICS); + m.obj = allChars.get(i); + sendMessage(m); + } + } + } + + void clearCharsCache() { + if (mBroadcastCharacteristics != null) { + mBroadcastCharacteristics.clear(); + } + if (mBroadcastScanControlPoint != null) { + mBroadcastScanControlPoint = null; + } + mNumOfBroadcastReceiverStates = 0; + if (mBluetoothLeBroadcastReceiveStates != null) { + mBluetoothLeBroadcastReceiveStates.clear(); + } + mPendingOperation = -1; + mPendingMetadata = null; + } + + @VisibleForTesting + class Disconnected extends State { + @Override + public void enter() { + log("Enter Disconnected(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + clearCharsCache(); + mNextSourceId = 0; + removeDeferredMessages(DISCONNECT); + if (mLastConnectionState == -1) { + log("no Broadcast of initial profile state "); + } else { + broadcastConnectionState( + mDevice, mLastConnectionState, BluetoothProfile.STATE_DISCONNECTED); + } + } + + @Override + public void exit() { + log("Exit Disconnected(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED; + } + + @Override + public boolean processMessage(Message message) { + log("Disconnected process message(" + mDevice + + "): " + messageWhatToString(message.what)); + switch (message.what) { + case CONNECT: + log("Connecting to " + mDevice); + if (mBluetoothGatt != null) { + Log.d(TAG, "clear off, pending wl connection"); + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + mBluetoothGatt = null; + } + mBluetoothGatt = mDevice.connectGatt(mService, mIsAllowedList, + mGattCallback, BluetoothDevice.TRANSPORT_LE, false, + (BluetoothDevice.PHY_LE_1M_MASK + | BluetoothDevice.PHY_LE_2M_MASK + | BluetoothDevice.PHY_LE_CODED_MASK), null); + if (mBluetoothGatt == null) { + Log.e(TAG, "Disconnected: error connecting to " + mDevice); + break; + } else { + transitionTo(mConnecting); + } + break; + case DISCONNECT: + Log.w(TAG, "Disconnected: DISCONNECT ignored: " + mDevice); + break; + case CONNECTION_STATE_CHANGED: + int state = (int) message.obj; + Log.w(TAG, "connection state changed:" + state); + if (state == BluetoothProfile.STATE_CONNECTED) { + log("remote/wl connection"); + transitionTo(mConnected); + } else { + Log.w(TAG, "Disconnected: Connection failed to " + mDevice); + } + break; + case PSYNC_ACTIVE_TIMEOUT: + cancelActiveSync(null); + break; + default: + log("DISCONNECTED: not handled message:" + message.what); + return NOT_HANDLED; + } + return HANDLED; + } + } + + @VisibleForTesting + class Connecting extends State { + @Override + public void enter() { + log("Enter Connecting(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + sendMessageDelayed(CONNECT_TIMEOUT, mDevice, BassConstants.CONNECT_TIMEOUT_MS); + broadcastConnectionState( + mDevice, mLastConnectionState, BluetoothProfile.STATE_CONNECTING); + } + + @Override + public void exit() { + log("Exit Connecting(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + mLastConnectionState = BluetoothProfile.STATE_CONNECTING; + removeMessages(CONNECT_TIMEOUT); + } + + @Override + public boolean processMessage(Message message) { + log("Connecting process message(" + mDevice + "): " + + messageWhatToString(message.what)); + switch (message.what) { + case CONNECT: + log("Already Connecting to " + mDevice); + log("Ignore this connection request " + mDevice); + break; + case DISCONNECT: + Log.w(TAG, "Connecting: DISCONNECT deferred: " + mDevice); + deferMessage(message); + break; + case READ_BASS_CHARACTERISTICS: + Log.w(TAG, "defer READ_BASS_CHARACTERISTICS requested!: " + mDevice); + deferMessage(message); + break; + case CONNECTION_STATE_CHANGED: + int state = (int) message.obj; + Log.w(TAG, "Connecting: connection state changed:" + state); + if (state == BluetoothProfile.STATE_CONNECTED) { + transitionTo(mConnected); + } else { + Log.w(TAG, "Connection failed to " + mDevice); + transitionTo(mDisconnected); + } + break; + case CONNECT_TIMEOUT: + Log.w(TAG, "CONNECT_TIMEOUT"); + BluetoothDevice device = (BluetoothDevice) message.obj; + if (!mDevice.equals(device)) { + Log.e(TAG, "Unknown device timeout " + device); + break; + } + transitionTo(mDisconnected); + break; + case PSYNC_ACTIVE_TIMEOUT: + deferMessage(message); + break; + default: + log("CONNECTING: not handled message:" + message.what); + return NOT_HANDLED; + } + return HANDLED; + } + } + + private byte[] reverseBytes(byte[] a) { + for (int i = 0; i < a.length / 2; i++) { + byte tmp = a[i]; + a[i] = a[a.length - i - 1]; + a[a.length - i - 1] = tmp; + } + return a; + } + + private byte[] bluetoothAddressToBytes(String s) { + log("BluetoothAddressToBytes: input string:" + s); + String[] splits = s.split(":"); + byte[] addressBytes = new byte[6]; + for (int i = 0; i < 6; i++) { + int hexValue = Integer.parseInt(splits[i], 16); + log("hexValue:" + hexValue); + addressBytes[i] = (byte) hexValue; + } + return addressBytes; + } + + private byte[] convertMetadataToAddSourceByteArray(BluetoothLeBroadcastMetadata metaData) { + log("Get PeriodicAdvertisementResult for :" + metaData.getSourceDevice()); + BluetoothDevice broadcastSource = metaData.getSourceDevice(); + PeriodicAdvertisementResult paRes = + mService.getPeriodicAdvertisementResult(broadcastSource); + if (paRes == null) { + Log.e(TAG, "No matching psync, scan res for this addition"); + mService.getCallbacks().notifySourceAddFailed( + mDevice, metaData, BluetoothStatusCodes.ERROR_UNKNOWN); + return null; + } + // populate metadata from BASE levelOne + BaseData base = mService.getBase(paRes.getSyncHandle()); + if (base == null) { + Log.e(TAG, "No valid base data populated for this device"); + mService.getCallbacks().notifySourceAddFailed( + mDevice, metaData, BluetoothStatusCodes.ERROR_UNKNOWN); + return null; + } + int numSubGroups = base.getNumberOfSubgroupsofBIG(); + byte[] metaDataLength = new byte[numSubGroups]; + int totalMetadataLength = 0; + for (int i = 0; i < numSubGroups; i++) { + if (base.getMetadata(i) == null) { + Log.w(TAG, "no valid metadata from BASE"); + metaDataLength[i] = 0; + } else { + metaDataLength[i] = (byte) base.getMetadata(i).length; + log("metaDataLength updated:" + metaDataLength[i]); + } + totalMetadataLength = totalMetadataLength + metaDataLength[i]; + } + byte[] res = new byte[ADD_SOURCE_FIXED_LENGTH + + numSubGroups * 5 + totalMetadataLength]; + int offset = 0; + // Opcode + res[offset++] = OPCODE_ADD_SOURCE; + // Advertiser_Address_Type + if (paRes.getAddressType() != (byte) BassConstants.INVALID_ADV_ADDRESS_TYPE) { + res[offset++] = (byte) paRes.getAddressType(); + } else { + res[offset++] = (byte) BassConstants.BROADCAST_ASSIST_ADDRESS_TYPE_PUBLIC; + } + String address = broadcastSource.getAddress(); + byte[] addrByteVal = bluetoothAddressToBytes(address); + log("Address bytes: " + Arrays.toString(addrByteVal)); + byte[] revAddress = reverseBytes(addrByteVal); + log("reverse Address bytes: " + Arrays.toString(revAddress)); + // Advertiser_Address + System.arraycopy(revAddress, 0, res, offset, 6); + offset += 6; + // Advertising_SID + res[offset++] = (byte) paRes.getAdvSid(); + log("mBroadcastId: " + paRes.getBroadcastId()); + // Broadcast_ID + res[offset++] = (byte) (paRes.getBroadcastId() & 0x00000000000000FF); + res[offset++] = (byte) ((paRes.getBroadcastId() & 0x000000000000FF00) >>> 8); + res[offset++] = (byte) ((paRes.getBroadcastId() & 0x0000000000FF0000) >>> 16); + // PA_Sync + if (!mDefNoPAS) { + res[offset++] = (byte) (0x01); + } else { + log("setting PA sync to ZERO"); + res[offset++] = (byte) 0x00; + } + // PA_Interval + res[offset++] = (byte) (paRes.getAdvInterval() & 0x00000000000000FF); + res[offset++] = (byte) ((paRes.getAdvInterval() & 0x000000000000FF00) >>> 8); + // Num_Subgroups + res[offset++] = base.getNumberOfSubgroupsofBIG(); + for (int i = 0; i < base.getNumberOfSubgroupsofBIG(); i++) { + // BIS_Sync + res[offset++] = (byte) 0xFF; + res[offset++] = (byte) 0xFF; + res[offset++] = (byte) 0xFF; + res[offset++] = (byte) 0xFF; + // Metadata_Length + res[offset++] = metaDataLength[i]; + if (metaDataLength[i] != 0) { + byte[] revMetadata = reverseBytes(base.getMetadata(i)); + // Metadata + System.arraycopy(revMetadata, 0, res, offset, metaDataLength[i]); + } + offset = offset + metaDataLength[i]; + } + log("ADD_BCAST_SOURCE in Bytes"); + BassUtils.printByteArray(res); + return res; + } + + private byte[] convertBroadcastMetadataToUpdateSourceByteArray(int sourceId, + BluetoothLeBroadcastMetadata metaData) { + BluetoothLeBroadcastReceiveState existingState = + getBroadcastReceiveStateForSourceId(sourceId); + if (existingState == null) { + log("no existing SI for update source op"); + return null; + } + BluetoothDevice broadcastSource = metaData.getSourceDevice(); + PeriodicAdvertisementResult paRes = + mService.getPeriodicAdvertisementResult(broadcastSource); + if (paRes == null) { + Log.e(TAG, "No matching psync, scan res for update"); + mService.getCallbacks().notifySourceRemoveFailed( + mDevice, sourceId, BluetoothStatusCodes.ERROR_UNKNOWN); + return null; + } + // populate metadata from BASE levelOne + BaseData base = mService.getBase(paRes.getSyncHandle()); + if (base == null) { + Log.e(TAG, "No valid base data populated for this device"); + mService.getCallbacks().notifySourceRemoveFailed( + mDevice, sourceId, BluetoothStatusCodes.ERROR_UNKNOWN); + return null; + } + byte numSubGroups = base.getNumberOfSubgroupsofBIG(); + byte[] res = new byte[UPDATE_SOURCE_FIXED_LENGTH + numSubGroups * 5]; + int offset = 0; + // Opcode + res[offset++] = OPCODE_UPDATE_SOURCE; + // Source_ID + res[offset++] = (byte) sourceId; + // PA_Sync + if (existingState.getPaSyncState() + == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED) { + res[offset++] = (byte) (0x01); + } else { + res[offset++] = (byte) 0x00; + } + // PA_Interval + res[offset++] = (byte) 0xFF; + res[offset++] = (byte) 0xFF; + // Num_Subgroups + res[offset++] = numSubGroups; + for (int i = 0; i < numSubGroups; i++) { + int bisIndexValue = existingState.getBisSyncState().get(i).intValue(); + log("UPDATE_BCAST_SOURCE: bisIndexValue : " + bisIndexValue); + // BIS_Sync + res[offset++] = (byte) (bisIndexValue & 0x00000000000000FF); + res[offset++] = (byte) ((bisIndexValue & 0x000000000000FF00) >>> 8); + res[offset++] = (byte) ((bisIndexValue & 0x0000000000FF0000) >>> 16); + res[offset++] = (byte) ((bisIndexValue & 0x00000000FF000000) >>> 24); + // Metadata_Length; On Modify source, don't update any Metadata + res[offset++] = 0; + } + log("UPDATE_BCAST_SOURCE in Bytes"); + BassUtils.printByteArray(res); + return res; + } + + private byte[] convertAsciitoValues(byte[] val) { + byte[] ret = new byte[val.length]; + for (int i = 0; i < val.length; i++) { + ret[i] = (byte) (val[i] - (byte) '0'); + } + log("convertAsciitoValues: returns:" + Arrays.toString(val)); + return ret; + } + + private byte[] convertRecvStateToSetBroadcastCodeByteArray( + BluetoothLeBroadcastReceiveState recvState) { + byte[] res = new byte[BassConstants.PIN_CODE_CMD_LEN]; + // Opcode + res[0] = OPCODE_SET_BCAST_PIN; + // Source_ID + res[1] = (byte) recvState.getSourceId(); + log("convertRecvStateToSetBroadcastCodeByteArray: Source device : " + + recvState.getSourceDevice()); + BluetoothLeBroadcastMetadata metaData = mService.getSourceInternal( + recvState.getSourceId()); + if (metaData == null) { + Log.e(TAG, "Fail to find broadcast source, sourceId = " + + recvState.getSourceId()); + return null; + } + // Can Keep as ASCII as is + String reversePIN = new StringBuffer(new String(metaData.getBroadcastCode())) + .reverse().toString(); + byte[] actualPIN = reversePIN.getBytes(); + if (actualPIN == null) { + Log.e(TAG, "actual PIN is null"); + return null; + } else { + log("byte array broadcast Code:" + Arrays.toString(actualPIN)); + log("pinLength:" + actualPIN.length); + // Broadcast_Code, Fill the PIN code in the Last Position + System.arraycopy( + actualPIN, 0, res, + (BassConstants.PIN_CODE_CMD_LEN - actualPIN.length), actualPIN.length); + log("SET_BCAST_PIN in Bytes"); + BassUtils.printByteArray(res); + } + return res; + } + + private boolean isItRightTimeToUpdateBroadcastPin(byte sourceId) { + Collection<BluetoothLeBroadcastReceiveState> recvStates = + mBluetoothLeBroadcastReceiveStates.values(); + Iterator<BluetoothLeBroadcastReceiveState> iterator = recvStates.iterator(); + boolean retval = false; + if (mForceSB) { + log("force SB is set"); + return true; + } + while (iterator.hasNext()) { + BluetoothLeBroadcastReceiveState state = iterator.next(); + if (state == null) { + log("Source state is null"); + continue; + } + if (sourceId == state.getSourceId() && state.getBigEncryptionState() + == BluetoothLeBroadcastReceiveState + .BIG_ENCRYPTION_STATE_CODE_REQUIRED) { + retval = true; + break; + } + } + log("IsItRightTimeToUpdateBroadcastPIN returning:" + retval); + return retval; + } + + @VisibleForTesting + class Connected extends State { + @Override + public void enter() { + log("Enter Connected(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + removeDeferredMessages(CONNECT); + if (mLastConnectionState == BluetoothProfile.STATE_CONNECTED) { + log("CONNECTED->CONNTECTED: Ignore"); + } else { + broadcastConnectionState(mDevice, mLastConnectionState, + BluetoothProfile.STATE_CONNECTED); + } + } + + @Override + public void exit() { + log("Exit Connected(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + mLastConnectionState = BluetoothProfile.STATE_CONNECTED; + } + + @Override + public boolean processMessage(Message message) { + log("Connected process message(" + mDevice + "): " + messageWhatToString(message.what)); + BluetoothLeBroadcastMetadata metaData; + switch (message.what) { + case CONNECT: + Log.w(TAG, "Connected: CONNECT ignored: " + mDevice); + break; + case DISCONNECT: + log("Disconnecting from " + mDevice); + if (mBluetoothGatt != null) { + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + mBluetoothGatt = null; + cancelActiveSync(null); + transitionTo(mDisconnected); + } else { + log("mBluetoothGatt is null"); + } + break; + case CONNECTION_STATE_CHANGED: + int state = (int) message.obj; + Log.w(TAG, "Connected:connection state changed:" + state); + if (state == BluetoothProfile.STATE_CONNECTED) { + Log.w(TAG, "device is already connected to Bass" + mDevice); + } else { + Log.w(TAG, "unexpected disconnected from " + mDevice); + cancelActiveSync(null); + transitionTo(mDisconnected); + } + break; + case READ_BASS_CHARACTERISTICS: + BluetoothGattCharacteristic characteristic = + (BluetoothGattCharacteristic) message.obj; + if (mBluetoothGatt != null) { + mBluetoothGatt.readCharacteristic(characteristic); + transitionTo(mConnectedProcessing); + } else { + Log.e(TAG, "READ_BASS_CHARACTERISTICS is ignored, Gatt handle is null"); + } + break; + case START_SCAN_OFFLOAD: + if (mBluetoothGatt != null && mBroadcastScanControlPoint != null) { + mBroadcastScanControlPoint.setValue(REMOTE_SCAN_START); + mBluetoothGatt.writeCharacteristic(mBroadcastScanControlPoint); + mPendingOperation = message.what; + transitionTo(mConnectedProcessing); + } else { + log("no Bluetooth Gatt handle, may need to fetch write"); + } + break; + case STOP_SCAN_OFFLOAD: + if (mBluetoothGatt != null && mBroadcastScanControlPoint != null) { + mBroadcastScanControlPoint.setValue(REMOTE_SCAN_STOP); + mBluetoothGatt.writeCharacteristic(mBroadcastScanControlPoint); + mPendingOperation = message.what; + transitionTo(mConnectedProcessing); + } else { + log("no Bluetooth Gatt handle, may need to fetch write"); + } + break; + case SELECT_BCAST_SOURCE: + ScanResult scanRes = (ScanResult) message.obj; + boolean auto = ((int) message.arg1) == BassConstants.AUTO; + selectSource(scanRes, auto); + break; + case ADD_BCAST_SOURCE: + metaData = (BluetoothLeBroadcastMetadata) message.obj; + log("Adding Broadcast source" + metaData); + byte[] addSourceInfo = convertMetadataToAddSourceByteArray(metaData); + if (addSourceInfo == null) { + Log.e(TAG, "add source: source Info is NULL"); + break; + } + if (mBluetoothGatt != null && mBroadcastScanControlPoint != null) { + mBroadcastScanControlPoint.setValue(addSourceInfo); + mBluetoothGatt.writeCharacteristic(mBroadcastScanControlPoint); + mPendingOperation = message.what; + mPendingMetadata = metaData; + transitionTo(mConnectedProcessing); + sendMessageDelayed(GATT_TXN_TIMEOUT, BassConstants.GATT_TXN_TIMEOUT_MS); + } else { + Log.e(TAG, "ADD_BCAST_SOURCE: no Bluetooth Gatt handle, Fatal"); + mService.getCallbacks().notifySourceAddFailed(mDevice, + metaData, BluetoothStatusCodes.ERROR_UNKNOWN); + } + break; + case UPDATE_BCAST_SOURCE: + metaData = (BluetoothLeBroadcastMetadata) message.obj; + int sourceId = message.arg1; + log("Updating Broadcast source" + metaData); + byte[] updateSourceInfo = convertBroadcastMetadataToUpdateSourceByteArray( + sourceId, metaData); + if (updateSourceInfo == null) { + Log.e(TAG, "update source: source Info is NULL"); + break; + } + if (mBluetoothGatt != null && mBroadcastScanControlPoint != null) { + mBroadcastScanControlPoint.setValue(updateSourceInfo); + mBluetoothGatt.writeCharacteristic(mBroadcastScanControlPoint); + mPendingOperation = message.what; + mPendingSourceId = (byte) sourceId; + mPendingMetadata = metaData; + transitionTo(mConnectedProcessing); + sendMessageDelayed(GATT_TXN_TIMEOUT, BassConstants.GATT_TXN_TIMEOUT_MS); + } else { + Log.e(TAG, "UPDATE_BCAST_SOURCE: no Bluetooth Gatt handle, Fatal"); + mService.getCallbacks().notifySourceModifyFailed( + mDevice, sourceId, BluetoothStatusCodes.ERROR_UNKNOWN); + } + break; + case SET_BCAST_CODE: + BluetoothLeBroadcastReceiveState recvState = + (BluetoothLeBroadcastReceiveState) message.obj; + sourceId = message.arg2; + log("SET_BCAST_CODE metaData: " + recvState); + if (!isItRightTimeToUpdateBroadcastPin((byte) recvState.getSourceId())) { + mSetBroadcastCodePending = true; + mSetBroadcastPINRcvState = recvState; + log("Ignore SET_BCAST now, but store it for later"); + } else { + byte[] setBroadcastPINcmd = + convertRecvStateToSetBroadcastCodeByteArray(recvState); + if (setBroadcastPINcmd == null) { + Log.e(TAG, "SET_BCAST_CODE: Broadcast code is NULL"); + break; + } + if (mBluetoothGatt != null && mBroadcastScanControlPoint != null) { + mBroadcastScanControlPoint.setValue(setBroadcastPINcmd); + mBluetoothGatt.writeCharacteristic(mBroadcastScanControlPoint); + mPendingOperation = message.what; + mPendingSourceId = (byte) recvState.getSourceId(); + transitionTo(mConnectedProcessing); + sendMessageDelayed(GATT_TXN_TIMEOUT, BassConstants.GATT_TXN_TIMEOUT_MS); + } + } + break; + case REMOVE_BCAST_SOURCE: + byte sid = (byte) message.arg1; + log("Removing Broadcast source: audioSource:" + "sourceId:" + + sid); + byte[] removeSourceInfo = new byte[2]; + removeSourceInfo[0] = OPCODE_REMOVE_SOURCE; + removeSourceInfo[1] = sid; + if (mBluetoothGatt != null && mBroadcastScanControlPoint != null) { + mBroadcastScanControlPoint.setValue(removeSourceInfo); + mBluetoothGatt.writeCharacteristic(mBroadcastScanControlPoint); + mPendingOperation = message.what; + mPendingSourceId = sid; + transitionTo(mConnectedProcessing); + sendMessageDelayed(GATT_TXN_TIMEOUT, BassConstants.GATT_TXN_TIMEOUT_MS); + } else { + Log.e(TAG, "REMOVE_BCAST_SOURCE: no Bluetooth Gatt handle, Fatal"); + mService.getCallbacks().notifySourceRemoveFailed(mDevice, + sid, BluetoothStatusCodes.ERROR_UNKNOWN); + } + break; + case PSYNC_ACTIVE_TIMEOUT: + cancelActiveSync(null); + break; + default: + log("CONNECTED: not handled message:" + message.what); + return NOT_HANDLED; + } + return HANDLED; + } + } + + private boolean isSuccess(int status) { + boolean ret = false; + switch (status) { + case BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST: + case BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST: + case BluetoothStatusCodes.REASON_REMOTE_REQUEST: + case BluetoothStatusCodes.REASON_SYSTEM_POLICY: + ret = true; + break; + default: + break; + } + return ret; + } + + void sendPendingCallbacks(int pendingOp, int status) { + switch (pendingOp) { + case START_SCAN_OFFLOAD: + if (!isSuccess(status)) { + if (!mAutoTriggered) { + cancelActiveSync(null); + } else { + mAutoTriggered = false; + } + } + break; + case ADD_BCAST_SOURCE: + if (!isSuccess(status)) { + cancelActiveSync(null); + Message message = obtainMessage(STOP_SCAN_OFFLOAD); + sendMessage(message); + mService.getCallbacks().notifySourceAddFailed(mDevice, + mPendingMetadata, status); + mPendingMetadata = null; + } + break; + case UPDATE_BCAST_SOURCE: + if (!mAutoTriggered) { + if (!isSuccess(status)) { + mService.getCallbacks().notifySourceModifyFailed(mDevice, + mPendingSourceId, status); + mPendingMetadata = null; + } + } else { + mAutoTriggered = false; + } + break; + case REMOVE_BCAST_SOURCE: + if (!isSuccess(status)) { + mService.getCallbacks().notifySourceRemoveFailed(mDevice, + mPendingSourceId, status); + } + break; + case SET_BCAST_CODE: + log("sendPendingCallbacks: SET_BCAST_CODE"); + break; + default: + log("sendPendingCallbacks: unhandled case"); + break; + } + } + + @VisibleForTesting + class ConnectedProcessing extends State { + @Override + public void enter() { + log("Enter ConnectedProcessing(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + } + @Override + public void exit() { + log("Exit ConnectedProcessing(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + } + @Override + public boolean processMessage(Message message) { + log("ConnectedProcessing process message(" + mDevice + "): " + + messageWhatToString(message.what)); + switch (message.what) { + case CONNECT: + Log.w(TAG, "CONNECT request is ignored" + mDevice); + break; + case DISCONNECT: + Log.w(TAG, "DISCONNECT requested!: " + mDevice); + if (mBluetoothGatt != null) { + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + mBluetoothGatt = null; + cancelActiveSync(null); + transitionTo(mDisconnected); + } else { + log("mBluetoothGatt is null"); + } + break; + case READ_BASS_CHARACTERISTICS: + Log.w(TAG, "defer READ_BASS_CHARACTERISTICS requested!: " + mDevice); + deferMessage(message); + break; + case CONNECTION_STATE_CHANGED: + int state = (int) message.obj; + Log.w(TAG, "ConnectedProcessing: connection state changed:" + state); + if (state == BluetoothProfile.STATE_CONNECTED) { + Log.w(TAG, "should never happen from this state"); + } else { + Log.w(TAG, "Unexpected disconnection " + mDevice); + transitionTo(mDisconnected); + } + break; + case GATT_TXN_PROCESSED: + removeMessages(GATT_TXN_TIMEOUT); + int status = (int) message.arg1; + log("GATT transaction processed for" + mDevice); + if (status == BluetoothGatt.GATT_SUCCESS) { + sendPendingCallbacks( + mPendingOperation, + BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST); + } else { + sendPendingCallbacks( + mPendingOperation, + BluetoothStatusCodes.ERROR_UNKNOWN); + } + transitionTo(mConnected); + break; + case GATT_TXN_TIMEOUT: + log("GATT transaction timedout for" + mDevice); + sendPendingCallbacks( + mPendingOperation, + BluetoothStatusCodes.ERROR_UNKNOWN); + mPendingOperation = -1; + mPendingSourceId = -1; + transitionTo(mConnected); + break; + case START_SCAN_OFFLOAD: + case STOP_SCAN_OFFLOAD: + case SELECT_BCAST_SOURCE: + case ADD_BCAST_SOURCE: + case SET_BCAST_CODE: + case REMOVE_BCAST_SOURCE: + case PSYNC_ACTIVE_TIMEOUT: + log("defer the message:" + message.what + "so that it will be processed later"); + deferMessage(message); + break; + default: + log("CONNECTEDPROCESSING: not handled message:" + message.what); + return NOT_HANDLED; + } + return HANDLED; + } + } + + @VisibleForTesting + class Disconnecting extends State { + @Override + public void enter() { + log("Enter Disconnecting(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + sendMessageDelayed(CONNECT_TIMEOUT, mDevice, BassConstants.CONNECT_TIMEOUT_MS); + broadcastConnectionState( + mDevice, mLastConnectionState, BluetoothProfile.STATE_DISCONNECTING); + } + + @Override + public void exit() { + log("Exit Disconnecting(" + mDevice + "): " + + messageWhatToString(getCurrentMessage().what)); + removeMessages(CONNECT_TIMEOUT); + mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING; + } + + @Override + public boolean processMessage(Message message) { + log("Disconnecting process message(" + mDevice + "): " + + messageWhatToString(message.what)); + switch (message.what) { + case CONNECT: + log("Disconnecting to " + mDevice); + log("deferring this connection request " + mDevice); + deferMessage(message); + break; + case DISCONNECT: + Log.w(TAG, "Already disconnecting: DISCONNECT ignored: " + mDevice); + break; + case CONNECTION_STATE_CHANGED: + int state = (int) message.obj; + Log.w(TAG, "Disconnecting: connection state changed:" + state); + if (state == BluetoothProfile.STATE_CONNECTED) { + Log.e(TAG, "should never happen from this state"); + transitionTo(mConnected); + } else { + Log.w(TAG, "disconnection successful to " + mDevice); + cancelActiveSync(null); + transitionTo(mDisconnected); + } + break; + case CONNECT_TIMEOUT: + Log.w(TAG, "CONNECT_TIMEOUT"); + BluetoothDevice device = (BluetoothDevice) message.obj; + if (!mDevice.equals(device)) { + Log.e(TAG, "Unknown device timeout " + device); + break; + } + transitionTo(mDisconnected); + break; + default: + log("Disconnecting: not handled message:" + message.what); + return NOT_HANDLED; + } + return HANDLED; + } + } + + void broadcastConnectionState(BluetoothDevice device, int fromState, int toState) { + log("broadcastConnectionState " + device + ": " + fromState + "->" + toState); + if (fromState == BluetoothProfile.STATE_CONNECTED + && toState == BluetoothProfile.STATE_CONNECTED) { + log("CONNECTED->CONNTECTED: Ignore"); + return; + } + } + + int getConnectionState() { + String currentState = "Unknown"; + if (getCurrentState() != null) { + currentState = getCurrentState().getName(); + } + switch (currentState) { + case "Disconnected": + log("Disconnected"); + return BluetoothProfile.STATE_DISCONNECTED; + case "Disconnecting": + log("Disconnecting"); + return BluetoothProfile.STATE_DISCONNECTING; + case "Connecting": + log("Connecting"); + return BluetoothProfile.STATE_CONNECTING; + case "Connected": + case "ConnectedProcessing": + log("connected"); + return BluetoothProfile.STATE_CONNECTED; + default: + Log.e(TAG, "Bad currentState: " + currentState); + return BluetoothProfile.STATE_DISCONNECTED; + } + } + + int getMaximumSourceCapacity() { + return mNumOfBroadcastReceiverStates; + } + + BluetoothDevice getDevice() { + return mDevice; + } + + synchronized boolean isConnected() { + return getCurrentState() == mConnected; + } + + public static String messageWhatToString(int what) { + switch (what) { + case CONNECT: + return "CONNECT"; + case DISCONNECT: + return "DISCONNECT"; + case CONNECTION_STATE_CHANGED: + return "CONNECTION_STATE_CHANGED"; + case GATT_TXN_PROCESSED: + return "GATT_TXN_PROCESSED"; + case READ_BASS_CHARACTERISTICS: + return "READ_BASS_CHARACTERISTICS"; + case START_SCAN_OFFLOAD: + return "START_SCAN_OFFLOAD"; + case STOP_SCAN_OFFLOAD: + return "STOP_SCAN_OFFLOAD"; + case ADD_BCAST_SOURCE: + return "ADD_BCAST_SOURCE"; + case SELECT_BCAST_SOURCE: + return "SELECT_BCAST_SOURCE"; + case UPDATE_BCAST_SOURCE: + return "UPDATE_BCAST_SOURCE"; + case SET_BCAST_CODE: + return "SET_BCAST_CODE"; + case REMOVE_BCAST_SOURCE: + return "REMOVE_BCAST_SOURCE"; + case PSYNC_ACTIVE_TIMEOUT: + return "PSYNC_ACTIVE_TIMEOUT"; + case CONNECT_TIMEOUT: + return "CONNECT_TIMEOUT"; + default: + break; + } + return Integer.toString(what); + } + + private static String profileStateToString(int state) { + switch (state) { + case BluetoothProfile.STATE_DISCONNECTED: + return "DISCONNECTED"; + case BluetoothProfile.STATE_CONNECTING: + return "CONNECTING"; + case BluetoothProfile.STATE_CONNECTED: + return "CONNECTED"; + case BluetoothProfile.STATE_DISCONNECTING: + return "DISCONNECTING"; + default: + break; + } + return Integer.toString(state); + } + + /** + * Dump info + */ + public void dump(StringBuilder sb) { + ProfileService.println(sb, "mDevice: " + mDevice); + ProfileService.println(sb, " StateMachine: " + this); + // Dump the state machine logs + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + super.dump(new FileDescriptor(), printWriter, new String[] {}); + printWriter.flush(); + stringWriter.flush(); + ProfileService.println(sb, " StateMachineLog:"); + Scanner scanner = new Scanner(stringWriter.toString()); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + ProfileService.println(sb, " " + line); + } + scanner.close(); + } + + @Override + protected void log(String msg) { + if (BassConstants.BASS_DBG) { + super.log(msg); + } + } + + private static void logByteArray(String prefix, byte[] value, int offset, int count) { + StringBuilder builder = new StringBuilder(prefix); + for (int i = offset; i < count; i++) { + builder.append(String.format("0x%02X", value[i])); + if (i != value.length - 1) { + builder.append(", "); + } + } + Log.d(TAG, builder.toString()); + } +} diff --git a/android/app/src/com/android/bluetooth/bass_client/BassConstants.java b/android/app/src/com/android/bluetooth/bass_client/BassConstants.java new file mode 100755 index 0000000000..dcd6b5b5a4 --- /dev/null +++ b/android/app/src/com/android/bluetooth/bass_client/BassConstants.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022 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. + */ + +package com.android.bluetooth.bass_client; + +import android.os.ParcelUuid; + +import java.util.UUID; + +/** + * Broadcast Audio Scan Service constants class + */ +public class BassConstants { + public static final boolean BASS_DBG = true; + public static final ParcelUuid BAAS_UUID = + ParcelUuid.fromString("00001852-0000-1000-8000-00805F9B34FB"); + public static final UUID BASS_UUID = + UUID.fromString("0000184F-0000-1000-8000-00805F9B34FB"); + public static final UUID BASS_BCAST_AUDIO_SCAN_CTRL_POINT = + UUID.fromString("00002BC7-0000-1000-8000-00805F9B34FB"); + public static final UUID BASS_BCAST_RECEIVER_STATE = + UUID.fromString("00002BC8-0000-1000-8000-00805F9B34FB"); + public static final UUID CLIENT_CHARACTERISTIC_CONFIG = + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + public static final ParcelUuid BASIC_AUDIO_UUID = + ParcelUuid.fromString("00001851-0000-1000-8000-00805F9B34FB"); + public static final int AA_START_SCAN = 1; + public static final int AA_SCAN_SUCCESS = 2; + public static final int AA_SCAN_FAILURE = 3; + public static final int AA_SCAN_TIMEOUT = 4; + // timeout for internal scan + public static final int AA_SCAN_TIMEOUT_MS = 1000; + public static final int INVALID_SYNC_HANDLE = -1; + public static final int INVALID_ADV_SID = -1; + public static final int INVALID_ADV_ADDRESS_TYPE = -1; + public static final int INVALID_ADV_INTERVAL = -1; + public static final int INVALID_BROADCAST_ID = -1; + public static final int BROADCAST_ASSIST_ADDRESS_TYPE_PUBLIC = 0; + public static final int INVALID_SOURCE_ID = -1; + public static final int ADV_ADDRESS_DONT_MATCHES_EXT_ADV_ADDRESS = 0x00000001; + public static final int ADV_ADDRESS_DONT_MATCHES_SOURCE_ADV_ADDRESS = 0x00000002; + // types of command for select and add Broadcast source operations + public static final int AUTO = 1; + public static final int USER = 2; + public static final int BASS_MAX_BYTES = 100; + // broadcast receiver state indices + public static final int BCAST_RCVR_STATE_SRC_ID_IDX = 0; + public static final int BCAST_RCVR_STATE_SRC_ADDR_TYPE_IDX = 1; + public static final int BCAST_RCVR_STATE_SRC_ADDR_START_IDX = 2; + public static final int BCAST_RCVR_STATE_SRC_BCAST_ID_START_IDX = 9; + public static final int BCAST_RCVR_STATE_SRC_ADDR_SIZE = 6; + public static final int BCAST_RCVR_STATE_SRC_ADV_SID_IDX = 8; + public static final int BCAST_RCVR_STATE_PA_SYNC_IDX = 12; + public static final int BCAST_RCVR_STATE_ENC_STATUS_IDX = 13; + public static final int BCAST_RCVR_STATE_BADCODE_START_IDX = 14; + public static final int BCAST_RCVR_STATE_BADCODE_SIZE = 16; + public static final int BCAST_RCVR_STATE_BIS_SYNC_SIZE = 4; + // 30 secs time out for all gatt writes + public static final int GATT_TXN_TIMEOUT_MS = 30000; + // 3 min time out for keeping PSYNC active + public static final int PSYNC_ACTIVE_TIMEOUT_MS = 3 * 60000; + // 2 secs time out achieving psync + public static final int PSYNC_TIMEOUT = 200; + public static final int PIN_CODE_CMD_LEN = 18; + public static final int CONNECT_TIMEOUT_MS = 30000; +} diff --git a/android/app/src/com/android/bluetooth/bass_client/BassUtils.java b/android/app/src/com/android/bluetooth/bass_client/BassUtils.java new file mode 100755 index 0000000000..42f88cad0c --- /dev/null +++ b/android/app/src/com/android/bluetooth/bass_client/BassUtils.java @@ -0,0 +1,144 @@ +/* + * Copyright 2022 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. + */ + +package com.android.bluetooth.bass_client; + +import android.annotation.NonNull; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.IBluetoothLeBroadcastAssistantCallback; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.os.Handler; +import android.os.Message; +import android.os.ParcelUuid; +import android.util.Log; + +import com.android.bluetooth.btservice.ServiceFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Bass Utility functions + */ +class BassUtils { + private static final String TAG = "BassUtils"; + + // Using ArrayList as KEY to hashmap. May be not risk + // in this case as It is used to track the callback to cancel Scanning later + private final Map<ArrayList<IBluetoothLeBroadcastAssistantCallback>, ScanCallback> + mLeAudioSourceScanCallbacks = + new HashMap<ArrayList<IBluetoothLeBroadcastAssistantCallback>, ScanCallback>(); + private final Map<BluetoothDevice, ScanCallback> mBassAutoAssist = + new HashMap<BluetoothDevice, ScanCallback>(); + + /*LE Scan related members*/ + private boolean mBroadcastersAround = false; + private BluetoothAdapter mBluetoothAdapter = null; + private BluetoothLeScanner mLeScanner = null; + private BassClientService mService = null; + private ServiceFactory mFactory = new ServiceFactory(); + + BassUtils(BassClientService service) { + mService = service; + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); + } + + void cleanUp() { + if (mLeAudioSourceScanCallbacks != null) { + mLeAudioSourceScanCallbacks.clear(); + } + if (mBassAutoAssist != null) { + mBassAutoAssist.clear(); + } + } + + private final Handler mAutoAssistScanHandler = + new Handler() { + public void handleMessage(Message msg) { + super.handleMessage(msg); + switch (msg.what) { + case BassConstants.AA_START_SCAN: + Message m = obtainMessage(BassConstants.AA_SCAN_TIMEOUT); + sendMessageDelayed(m, BassConstants.AA_SCAN_TIMEOUT_MS); + mService.startSearchingForSources(null); + break; + case BassConstants.AA_SCAN_SUCCESS: + // Able to find to desired desired Source Device + ScanResult scanRes = (ScanResult) msg.obj; + BluetoothDevice dev = scanRes.getDevice(); + mService.stopSearchingForSources(); + mService.selectSource(dev, scanRes, true); + break; + case BassConstants.AA_SCAN_FAILURE: + // Not able to find the given source + break; + case BassConstants.AA_SCAN_TIMEOUT: + mService.stopSearchingForSources(); + break; + } + } + }; + + @NonNull Handler getAutoAssistScanHandler() { + return mAutoAssistScanHandler; + } + + void triggerAutoAssist(BluetoothLeBroadcastReceiveState recvState) { + Message msg = mAutoAssistScanHandler.obtainMessage(BassConstants.AA_START_SCAN); + msg.obj = recvState.getSourceDevice(); + mAutoAssistScanHandler.sendMessage(msg); + } + + static boolean containUuid(List<ScanFilter> filters, ParcelUuid uuid) { + for (ScanFilter filter: filters) { + if (filter.getServiceUuid().equals(uuid)) { + return true; + } + } + return false; + } + + static int parseBroadcastId(byte[] broadcastIdBytes) { + int broadcastId; + broadcastId = (0x00FF0000 & (broadcastIdBytes[2] << 16)); + broadcastId |= (0x0000FF00 & (broadcastIdBytes[1] << 8)); + broadcastId |= (0x000000FF & broadcastIdBytes[0]); + return broadcastId; + } + + static void log(String msg) { + if (BassConstants.BASS_DBG) { + Log.d(TAG, msg); + } + } + + static void printByteArray(byte[] array) { + log("Entire byte Array as string: " + Arrays.toString(array)); + log("printitng byte by bte"); + for (int i = 0; i < array.length; i++) { + log("array[" + i + "] :" + Byte.toUnsignedInt(array[i])); + } + } +} diff --git a/android/app/src/com/android/bluetooth/bass_client/PeriodicAdvertisementResult.java b/android/app/src/com/android/bluetooth/bass_client/PeriodicAdvertisementResult.java new file mode 100755 index 0000000000..9d1aab0e5a --- /dev/null +++ b/android/app/src/com/android/bluetooth/bass_client/PeriodicAdvertisementResult.java @@ -0,0 +1,138 @@ +/* + * Copyright 2022 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. + */ + +package com.android.bluetooth.bass_client; + +import android.bluetooth.BluetoothDevice; +import android.util.Log; + +/** + * Periodic Advertisement Result + */ +public class PeriodicAdvertisementResult { + private static final String TAG = PeriodicAdvertisementResult.class.getSimpleName(); + + private BluetoothDevice mDevice; + private int mAddressType; + private int mAdvSid; + private int mSyncHandle; + private int mPAInterval; + private int mBroadcastId; + + PeriodicAdvertisementResult(BluetoothDevice device, + int addressType, + int syncHandle, + int advSid, + int paInterval, + int broadcastId) { + mDevice = device; + mAddressType = addressType; + mAdvSid = advSid; + mSyncHandle = syncHandle; + mPAInterval = paInterval; + mBroadcastId = broadcastId; + } + + /** + * Update Sync handle + */ + public void updateSyncHandle(int syncHandle) { + mSyncHandle = syncHandle; + } + + /** + * Get Sync handle + */ + public int getSyncHandle() { + return mSyncHandle; + } + + /** + * Update Adv ID + */ + public void updateAdvSid(int advSid) { + mAdvSid = advSid; + } + + /** + * Get Adv ID + */ + public int getAdvSid() { + return mAdvSid; + } + + /** + * Update address type + */ + public void updateAddressType(int addressType) { + mAddressType = addressType; + } + + /** + * Get address type + */ + public int getAddressType() { + return mAddressType; + } + + /** + * Update Adv interval + */ + public void updateAdvInterval(int advInterval) { + mPAInterval = advInterval; + } + + /** + * Get Adv interval + */ + public int getAdvInterval() { + return mPAInterval; + } + + /** + * Update broadcast ID + */ + public void updateBroadcastId(int broadcastId) { + mBroadcastId = broadcastId; + } + + /** + * Get broadcast ID + */ + public int getBroadcastId() { + return mBroadcastId; + } + + /** + * print + */ + public void print() { + log("-- PeriodicAdvertisementResult --"); + log("mDevice:" + mDevice); + log("mAddressType:" + mAddressType); + log("mAdvSid:" + mAdvSid); + log("mSyncHandle:" + mSyncHandle); + log("mPAInterval:" + mPAInterval); + log("mBroadcastId:" + mBroadcastId); + log("-- END: PeriodicAdvertisementResult --"); + } + + static void log(String msg) { + if (BassConstants.BASS_DBG) { + Log.d(TAG, msg); + } + } +} diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java index 1758940211..05bf73dc14 100644 --- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java +++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java @@ -105,6 +105,7 @@ import com.android.bluetooth.R; import com.android.bluetooth.Utils; import com.android.bluetooth.a2dp.A2dpService; import com.android.bluetooth.a2dpsink.A2dpSinkService; +import com.android.bluetooth.bass_client.BassClientService; import com.android.bluetooth.btservice.RemoteDevices.DeviceProperties; import com.android.bluetooth.btservice.activityattribution.ActivityAttributionService; import com.android.bluetooth.btservice.bluetoothkeystore.BluetoothKeystoreService; @@ -326,6 +327,7 @@ public class AdapterService extends Service { private VolumeControlService mVolumeControlService; private CsipSetCoordinatorService mCsipSetCoordinatorService; private LeAudioService mLeAudioService; + private BassClientService mBassClientService; private volatile boolean mTestModeEnabled = false; @@ -1093,6 +1095,9 @@ public class AdapterService extends Service { if (profile == BluetoothProfile.HAP_CLIENT) { return Utils.arrayContains(remoteDeviceUuids, BluetoothUuid.HAS); } + if (profile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { + return Utils.arrayContains(remoteDeviceUuids, BluetoothUuid.BASS); + } Log.e(TAG, "isSupported: Unexpected profile passed in to function: " + profile); return false; @@ -1160,6 +1165,10 @@ public class AdapterService extends Service { > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { return true; } + if (mBassClientService != null && mBassClientService.getConnectionPolicy(device) + > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { + return true; + } return false; } @@ -1265,6 +1274,13 @@ public class AdapterService extends Service { Log.i(TAG, "connectEnabledProfiles: Connecting LeAudio profile (BAP)"); mLeAudioService.connect(device); } + if (mBassClientService != null && isSupported(localDeviceUuids, remoteDeviceUuids, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, device) + && mBassClientService.getConnectionPolicy(device) + > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { + Log.i(TAG, "connectEnabledProfiles: Connecting LE Broadcast Assistant Profile"); + mBassClientService.connect(device); + } return BluetoothStatusCodes.SUCCESS; } @@ -1305,6 +1321,7 @@ public class AdapterService extends Service { mVolumeControlService = VolumeControlService.getVolumeControlService(); mCsipSetCoordinatorService = CsipSetCoordinatorService.getCsipSetCoordinatorService(); mLeAudioService = LeAudioService.getLeAudioService(); + mBassClientService = BassClientService.getBassClientService(); } @BluetoothAdapter.RfcommListenerResult @@ -4242,6 +4259,13 @@ public class AdapterService extends Service { BluetoothProfile.CONNECTION_POLICY_ALLOWED); numProfilesConnected++; } + if (mBassClientService != null && isSupported(localDeviceUuids, remoteDeviceUuids, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, device)) { + Log.i(TAG, "connectAllEnabledProfiles: Connecting LE Broadcast Assistant Profile"); + mBassClientService.setConnectionPolicy(device, + BluetoothProfile.CONNECTION_POLICY_ALLOWED); + numProfilesConnected++; + } Log.i(TAG, "connectAllEnabledProfiles: Number of Profiles Connected: " + numProfilesConnected); @@ -4349,6 +4373,12 @@ public class AdapterService extends Service { Log.i(TAG, "disconnectAllEnabledProfiles: Disconnecting LeAudio profile (BAP)"); mLeAudioService.disconnect(device); } + if (mBassClientService != null && mBassClientService.getConnectionState(device) + == BluetoothProfile.STATE_CONNECTED) { + Log.i(TAG, "disconnectAllEnabledProfiles: Disconnecting " + + "LE Broadcast Assistant Profile"); + mBassClientService.disconnect(device); + } return BluetoothStatusCodes.SUCCESS; } diff --git a/android/app/src/com/android/bluetooth/btservice/Config.java b/android/app/src/com/android/bluetooth/btservice/Config.java index 1d2938cd89..829696b5ea 100644 --- a/android/app/src/com/android/bluetooth/btservice/Config.java +++ b/android/app/src/com/android/bluetooth/btservice/Config.java @@ -33,6 +33,7 @@ import com.android.bluetooth.a2dp.A2dpService; import com.android.bluetooth.a2dpsink.A2dpSinkService; import com.android.bluetooth.avrcp.AvrcpTargetService; import com.android.bluetooth.avrcpcontroller.AvrcpControllerService; +import com.android.bluetooth.bass_client.BassClientService; import com.android.bluetooth.csip.CsipSetCoordinatorService; import com.android.bluetooth.gatt.GattService; import com.android.bluetooth.hap.HapClientService; @@ -136,6 +137,9 @@ public class Config { (1 << BluetoothProfile.CSIP_SET_COORDINATOR)), new ProfileConfig(HapClientService.class, R.bool.profile_supported_hap_client, (1 << BluetoothProfile.HAP_CLIENT)), + new ProfileConfig(BassClientService.class, + R.bool.profile_supported_bass_client, + (1 << BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT)), }; private static Class[] sSupportedProfiles = new Class[0]; diff --git a/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java b/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java index f1cef7af66..451bd4c887 100644 --- a/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java +++ b/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java @@ -41,6 +41,7 @@ import android.util.Log; import com.android.bluetooth.R; import com.android.bluetooth.Utils; import com.android.bluetooth.a2dp.A2dpService; +import com.android.bluetooth.bass_client.BassClientService; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.bluetooth.csip.CsipSetCoordinatorService; import com.android.bluetooth.hap.HapClientService; @@ -298,6 +299,7 @@ class PhonePolicy { VolumeControlService volumeControlService = mFactory.getVolumeControlService(); HapClientService hapClientService = mFactory.getHapClientService(); + BassClientService bcService = mFactory.getBassClientService(); // Set profile priorities only for the profiles discovered on the remote device. // This avoids needless auto-connect attempts to profiles non-existent on the remote device @@ -374,6 +376,15 @@ class PhonePolicy { mAdapterService.getDatabase().setProfileConnectionPolicy(device, BluetoothProfile.HAP_CLIENT, BluetoothProfile.CONNECTION_POLICY_ALLOWED); } + + if ((bcService != null) && Utils.arrayContains(uuids, + BluetoothUuid.BASS) && (bcService.getConnectionPolicy(device) + == BluetoothProfile.CONNECTION_POLICY_UNKNOWN)) { + debugLog("setting broadcast assistant profile priority for device " + device); + mAdapterService.getDatabase().setProfileConnectionPolicy(device, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, + BluetoothProfile.CONNECTION_POLICY_ALLOWED); + } } @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) diff --git a/android/app/src/com/android/bluetooth/btservice/ServiceFactory.java b/android/app/src/com/android/bluetooth/btservice/ServiceFactory.java index f9256ad2a1..98d37e96a3 100644 --- a/android/app/src/com/android/bluetooth/btservice/ServiceFactory.java +++ b/android/app/src/com/android/bluetooth/btservice/ServiceFactory.java @@ -18,6 +18,7 @@ package com.android.bluetooth.btservice; import com.android.bluetooth.a2dp.A2dpService; import com.android.bluetooth.avrcp.AvrcpTargetService; +import com.android.bluetooth.bass_client.BassClientService; import com.android.bluetooth.csip.CsipSetCoordinatorService; import com.android.bluetooth.hap.HapClientService; import com.android.bluetooth.hearingaid.HearingAidService; @@ -78,4 +79,8 @@ public class ServiceFactory { public HapClientService getHapClientService() { return HapClientService.getHapClientService(); } + + public BassClientService getBassClientService() { + return BassClientService.getBassClientService(); + } } diff --git a/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java b/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java index 73ca0cbe16..83e6f10db6 100644 --- a/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java +++ b/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java @@ -295,6 +295,7 @@ public class DatabaseManager { * {@link BluetoothProfile#MAP_CLIENT}, {@link BluetoothProfile#SAP}, * {@link BluetoothProfile#HEARING_AID}, {@link BluetoothProfile#LE_AUDIO}, * {@link BluetoothProfile#VOLUME_CONTROL}, {@link BluetoothProfile#CSIP_SET_COORDINATOR}, + * {@link BluetoothProfile#LE_AUDIO_BROADCAST_ASSISTANT}, * @param newConnectionPolicy the connectionPolicy to set; one of * {@link BluetoothProfile.CONNECTION_POLICY_UNKNOWN}, * {@link BluetoothProfile.CONNECTION_POLICY_FORBIDDEN}, @@ -352,6 +353,7 @@ public class DatabaseManager { * {@link BluetoothProfile#MAP_CLIENT}, {@link BluetoothProfile#SAP}, * {@link BluetoothProfile#HEARING_AID}, {@link BluetoothProfile#LE_AUDIO}, * {@link BluetoothProfile#VOLUME_CONTROL}, {@link BluetoothProfile#CSIP_SET_COORDINATOR}, + * {@link BluetoothProfile#LE_AUDIO_BROADCAST_ASSISTANT}, * @return the profile connection policy of the device; one of * {@link BluetoothProfile.CONNECTION_POLICY_UNKNOWN}, * {@link BluetoothProfile.CONNECTION_POLICY_FORBIDDEN}, diff --git a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java index 0de7e6b2c9..6d917393a3 100644 --- a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java +++ b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java @@ -125,6 +125,9 @@ class Metadata { case BluetoothProfile.LE_CALL_CONTROL: profileConnectionPolicies.le_call_control_connection_policy = connectionPolicy; break; + case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT: + profileConnectionPolicies.bass_client_connection_policy = connectionPolicy; + break; default: throw new IllegalArgumentException("invalid profile " + profile); } @@ -166,6 +169,8 @@ class Metadata { return profileConnectionPolicies.csip_set_coordinator_connection_policy; case BluetoothProfile.LE_CALL_CONTROL: return profileConnectionPolicies.le_call_control_connection_policy; + case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT: + return profileConnectionPolicies.bass_client_connection_policy; } return BluetoothProfile.CONNECTION_POLICY_UNKNOWN; } diff --git a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java index 2b1ca5e6de..c008ef95da 100644 --- a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java +++ b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java @@ -33,7 +33,7 @@ import java.util.List; /** * MetadataDatabase is a Room database stores Bluetooth persistence data */ -@Database(entities = {Metadata.class}, version = 110) +@Database(entities = {Metadata.class}, version = 111) public abstract class MetadataDatabase extends RoomDatabase { /** * The metadata database file name @@ -63,6 +63,7 @@ public abstract class MetadataDatabase extends RoomDatabase { .addMigrations(MIGRATION_107_108) .addMigrations(MIGRATION_108_109) .addMigrations(MIGRATION_109_110) + .addMigrations(MIGRATION_110_111) .allowMainThreadQueries() .build(); } @@ -426,4 +427,23 @@ public abstract class MetadataDatabase extends RoomDatabase { } } }; + + @VisibleForTesting + static final Migration MIGRATION_110_111 = new Migration(110, 111) { + @Override + public void migrate(SupportSQLiteDatabase database) { + try { + database.execSQL( + "ALTER TABLE metadata ADD COLUMN `bass_client_connection_policy` " + + "INTEGER DEFAULT 100"); + } catch (SQLException ex) { + // Check if user has new schema, but is just missing the version update + Cursor cursor = database.query("SELECT * FROM metadata"); + if (cursor == null + || cursor.getColumnIndex("bass_client_connection_policy") == -1) { + throw ex; + } + } + } + }; } diff --git a/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java b/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java index 18f9db0f6b..4f81f9eeb5 100644 --- a/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java +++ b/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java @@ -40,6 +40,7 @@ class ProfilePrioritiesEntity { public int volume_control_connection_policy; public int csip_set_coordinator_connection_policy; public int le_call_control_connection_policy; + public int bass_client_connection_policy; ProfilePrioritiesEntity() { a2dp_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN; @@ -59,6 +60,7 @@ class ProfilePrioritiesEntity { volume_control_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN; csip_set_coordinator_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN; le_call_control_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN; + bass_client_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN; } public String toString() { @@ -79,7 +81,8 @@ class ProfilePrioritiesEntity { .append("|HEARING_AID=").append(hearing_aid_connection_policy) .append("|LE_AUDIO=").append(le_audio_connection_policy) .append("|VOLUME_CONTROL=").append(volume_control_connection_policy) - .append("|LE_CALL_CONTROL=").append(le_call_control_connection_policy); + .append("|LE_CALL_CONTROL=").append(le_call_control_connection_policy) + .append("|LE_BROADCAST_ASSISTANT=").append(bass_client_connection_policy); return builder.toString(); } diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/110.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/110.json index 7e934727a4..c7efe3e21d 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/110.json +++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/110.json @@ -307,4 +307,4 @@ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c7e5587836ae523b01483700aa686a1f')" ] } -}
\ No newline at end of file +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/111.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/111.json new file mode 100644 index 0000000000..2bf5607def --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/111.json @@ -0,0 +1,316 @@ +{ + "formatVersion": 1, + "database": { + "version": 111, + "identityHash": "5f8763839846642b1ae59a0f3524b0b6", + "entities": [ + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `hap_client_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `bass_client_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, PRIMARY KEY(`address`))", + "fields": [ + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "migrated", + "columnName": "migrated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "a2dpSupportsOptionalCodecs", + "columnName": "a2dpSupportsOptionalCodecs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "a2dpOptionalCodecsEnabled", + "columnName": "a2dpOptionalCodecsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "last_active_time", + "columnName": "last_active_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_active_a2dp_device", + "columnName": "is_active_a2dp_device", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileConnectionPolicies.a2dp_connection_policy", + "columnName": "a2dp_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy", + "columnName": "a2dp_sink_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.hfp_connection_policy", + "columnName": "hfp_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy", + "columnName": "hfp_client_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.hid_host_connection_policy", + "columnName": "hid_host_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.pan_connection_policy", + "columnName": "pan_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.pbap_connection_policy", + "columnName": "pbap_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy", + "columnName": "pbap_client_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.map_connection_policy", + "columnName": "map_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.sap_connection_policy", + "columnName": "sap_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy", + "columnName": "hearing_aid_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.hap_client_connection_policy", + "columnName": "hap_client_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.map_client_connection_policy", + "columnName": "map_client_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.le_audio_connection_policy", + "columnName": "le_audio_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.volume_control_connection_policy", + "columnName": "volume_control_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy", + "columnName": "csip_set_coordinator_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy", + "columnName": "le_call_control_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileConnectionPolicies.bass_client_connection_policy", + "columnName": "bass_client_connection_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "publicMetadata.manufacturer_name", + "columnName": "manufacturer_name", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.model_name", + "columnName": "model_name", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.software_version", + "columnName": "software_version", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.hardware_version", + "columnName": "hardware_version", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.companion_app", + "columnName": "companion_app", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.main_icon", + "columnName": "main_icon", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.is_untethered_headset", + "columnName": "is_untethered_headset", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_left_icon", + "columnName": "untethered_left_icon", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_right_icon", + "columnName": "untethered_right_icon", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_case_icon", + "columnName": "untethered_case_icon", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_left_battery", + "columnName": "untethered_left_battery", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_right_battery", + "columnName": "untethered_right_battery", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_case_battery", + "columnName": "untethered_case_battery", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_left_charging", + "columnName": "untethered_left_charging", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_right_charging", + "columnName": "untethered_right_charging", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_case_charging", + "columnName": "untethered_case_charging", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.enhanced_settings_ui_uri", + "columnName": "enhanced_settings_ui_uri", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.device_type", + "columnName": "device_type", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.main_battery", + "columnName": "main_battery", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.main_charging", + "columnName": "main_charging", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.main_low_battery_threshold", + "columnName": "main_low_battery_threshold", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_left_low_battery_threshold", + "columnName": "untethered_left_low_battery_threshold", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_right_low_battery_threshold", + "columnName": "untethered_right_low_battery_threshold", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "publicMetadata.untethered_case_low_battery_threshold", + "columnName": "untethered_case_low_battery_threshold", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "address" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5f8763839846642b1ae59a0f3524b0b6')" + ] + } +}
\ No newline at end of file diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt index 14df144b72..28d8f6c4a9 100644 --- a/framework/api/system-current.txt +++ b/framework/api/system-current.txt @@ -440,7 +440,7 @@ package android.bluetooth { method public void onPlaybackStopped(int, int); } - public final class BluetoothLeBroadcastAssistant implements android.bluetooth.BluetoothProfile { + public final class BluetoothLeBroadcastAssistant implements java.lang.AutoCloseable android.bluetooth.BluetoothProfile { method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void addSource(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.bluetooth.BluetoothLeBroadcastMetadata, boolean); method @NonNull @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public java.util.List<android.bluetooth.BluetoothLeBroadcastReceiveState> getAllSources(@NonNull android.bluetooth.BluetoothDevice); method @NonNull @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public java.util.List<android.bluetooth.BluetoothDevice> getConnectedDevices(); diff --git a/framework/java/android/bluetooth/BluetoothDevice.java b/framework/java/android/bluetooth/BluetoothDevice.java index 01ceaa7f7d..8c591e897d 100644 --- a/framework/java/android/bluetooth/BluetoothDevice.java +++ b/framework/java/android/bluetooth/BluetoothDevice.java @@ -1385,6 +1385,17 @@ public final class BluetoothDevice implements Parcelable, Attributable { } /** + * Returns the address type of this BluetoothDevice. + * + * @return Bluetooth address type + * @hide + */ + public int getAddressType() { + if (DBG) Log.d(TAG, "mAddressType: " + mAddressType); + return mAddressType; + } + + /** * Returns the anonymized hardware address of this BluetoothDevice. The first three octets * will be suppressed for anonymization. * <p> For example, "XX:XX:XX:AA:BB:CC". diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcastAssistant.java b/framework/java/android/bluetooth/BluetoothLeBroadcastAssistant.java index 870ab0fb30..3eef0a9a54 100644..100755 --- a/framework/java/android/bluetooth/BluetoothLeBroadcastAssistant.java +++ b/framework/java/android/bluetooth/BluetoothLeBroadcastAssistant.java @@ -19,6 +19,7 @@ package android.bluetooth; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SystemApi; @@ -27,13 +28,19 @@ import android.bluetooth.annotations.RequiresBluetoothLocationPermission; import android.bluetooth.annotations.RequiresBluetoothScanPermission; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanSettings; +import android.content.AttributionSource; import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.CloseGuard; import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Collections; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; /** @@ -62,9 +69,10 @@ import java.util.concurrent.Executor; * @hide */ @SystemApi -public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { +public final class BluetoothLeBroadcastAssistant implements BluetoothProfile, AutoCloseable { private static final String TAG = "BluetoothLeBroadcastAssistant"; private static final boolean DBG = true; + private final Map<Callback, Executor> mCallbackMap = new HashMap<>(); /** * This class provides a set of callbacks that are invoked when scanning for Broadcast Sources @@ -292,6 +300,21 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { public static final String ACTION_CONNECTION_STATE_CHANGED = "android.bluetooth.action.CONNECTION_STATE_CHANGED"; + private CloseGuard mCloseGuard; + private Context mContext; + private BluetoothAdapter mBluetoothAdapter; + private final AttributionSource mAttributionSource; + private BluetoothLeBroadcastAssistantCallback mCallback; + + private final BluetoothProfileConnector<IBluetoothLeBroadcastAssistant> mProfileConnector = + new BluetoothProfileConnector(this, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, + TAG, IBluetoothLeBroadcastAssistant.class.getName()) { + @Override + public IBluetoothLeBroadcastAssistant getServiceInterface(IBinder service) { + return IBluetoothLeBroadcastAssistant.Stub.asInterface(service); + } + }; + /** * Create a new instance of an LE Audio Broadcast Assistant. * @@ -299,8 +322,32 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { */ /*package*/ BluetoothLeBroadcastAssistant( @NonNull Context context, @NonNull ServiceListener listener) { + mContext = context; + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mAttributionSource = mBluetoothAdapter.getAttributionSource(); + mProfileConnector.connect(context, listener); + mCloseGuard = new CloseGuard(); + mCloseGuard.open("close"); + } + + /** @hide */ + protected void finalize() { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } + + /** + * @hide + */ + public void close() { + mProfileConnector.disconnect(); } + private IBluetoothLeBroadcastAssistant getService() { + return mProfileConnector.getService(); + } /** * {@inheritDoc} @@ -314,7 +361,20 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { }) @Override public @BluetoothProfile.BtProfileState int getConnectionState(@NonNull BluetoothDevice sink) { - return BluetoothProfile.STATE_DISCONNECTED; + log("getConnectionState(" + sink + ")"); + final IBluetoothLeBroadcastAssistant service = getService(); + final int defaultValue = BluetoothProfile.STATE_DISCONNECTED; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled() && isValidDevice(sink)) { + try { + return service.getConnectionState(sink); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; } /** @@ -330,7 +390,20 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { @Override public @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates( @NonNull int[] states) { - return Collections.emptyList(); + log("getDevicesMatchingConnectionStates()"); + final IBluetoothLeBroadcastAssistant service = getService(); + final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled()) { + try { + return service.getDevicesMatchingConnectionStates(states); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; } /** @@ -345,7 +418,20 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { }) @Override public @NonNull List<BluetoothDevice> getConnectedDevices() { - return Collections.emptyList(); + log("getConnectedDevices()"); + final IBluetoothLeBroadcastAssistant service = getService(); + final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled()) { + try { + return service.getConnectedDevices(); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; } /** @@ -368,7 +454,22 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { }) public boolean setConnectionPolicy(@NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) { - return false; + log("setConnectionPolicy()"); + final IBluetoothLeBroadcastAssistant service = getService(); + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled() && isValidDevice(device) + && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN + || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) { + try { + return service.setConnectionPolicy(device, connectionPolicy); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; } /** @@ -389,7 +490,20 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) { - return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; + log("getConnectionPolicy()"); + final IBluetoothLeBroadcastAssistant service = getService(); + final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled() && isValidDevice(device)) { + try { + return service.getConnectionPolicy(device); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; } /** @@ -420,7 +534,16 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { throw new IllegalArgumentException("callback cannot be null"); } log("registerCallback"); - throw new UnsupportedOperationException("Not Implemented"); + final IBluetoothLeBroadcastAssistant service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled()) { + if (mCallback == null) { + mCallback = new BluetoothLeBroadcastAssistantCallback(service); + } + mCallback.register(executor, callback); + } } /** @@ -446,7 +569,15 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { throw new IllegalArgumentException("callback cannot be null"); } log("unregisterCallback"); - throw new UnsupportedOperationException("Not Implemented"); + final IBluetoothLeBroadcastAssistant service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled()) { + if (mCallback != null) { + mCallback.unregister(callback); + } + } } /** @@ -492,7 +623,17 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { if (filters == null) { throw new IllegalArgumentException("filters can be empty, but not null"); } - throw new UnsupportedOperationException("Not Implemented"); + final IBluetoothLeBroadcastAssistant service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled()) { + try { + service.startSearchingForSources(filters); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } } /** @@ -513,7 +654,17 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { }) public void stopSearchingForSources() { log("stopSearchingForSources:"); - throw new UnsupportedOperationException("Not Implemented"); + final IBluetoothLeBroadcastAssistant service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled()) { + try { + service.stopSearchingForSources(); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } } /** @@ -529,7 +680,20 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public boolean isSearchInProgress() { - return false; + log("stopSearchingForSources:"); + final IBluetoothLeBroadcastAssistant service = getService(); + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled()) { + try { + return service.isSearchInProgress(); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; } /** @@ -602,7 +766,17 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { public void addSource(@NonNull BluetoothDevice sink, @NonNull BluetoothLeBroadcastMetadata sourceMetadata, boolean isGroupOp) { log("addBroadcastSource: " + sourceMetadata + " on " + sink); - throw new UnsupportedOperationException("Not Implemented"); + final IBluetoothLeBroadcastAssistant service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled() && isValidDevice(sink)) { + try { + service.addSource(sink, sourceMetadata, isGroupOp); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } } /** @@ -657,7 +831,17 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { public void modifySource(@NonNull BluetoothDevice sink, int sourceId, @NonNull BluetoothLeBroadcastMetadata updatedMetadata) { log("updateBroadcastSource: " + updatedMetadata + " on " + sink); - throw new UnsupportedOperationException("Not Implemented"); + final IBluetoothLeBroadcastAssistant service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled() && isValidDevice(sink)) { + try { + service.modifySource(sink, sourceId, updatedMetadata); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } } /** @@ -692,7 +876,17 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { }) public void removeSource(@NonNull BluetoothDevice sink, int sourceId) { log("removeBroadcastSource: " + sourceId + " from " + sink); - return; + final IBluetoothLeBroadcastAssistant service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled() && isValidDevice(sink)) { + try { + service.removeSource(sink, sourceId); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } } @@ -713,7 +907,21 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { }) public @NonNull List<BluetoothLeBroadcastReceiveState> getAllSources( @NonNull BluetoothDevice sink) { - return Collections.emptyList(); + log("getAllSources()"); + final IBluetoothLeBroadcastAssistant service = getService(); + final List<BluetoothLeBroadcastReceiveState> defaultValue = + new ArrayList<BluetoothLeBroadcastReceiveState>(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled()) { + try { + return service.getAllSources(sink); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; } /** @@ -726,7 +934,19 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { */ @SystemApi public int getMaximumSourceCapacity(@NonNull BluetoothDevice sink) { - return 0; + final IBluetoothLeBroadcastAssistant service = getService(); + final int defaultValue = 0; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (mBluetoothAdapter.isEnabled() && isValidDevice(sink)) { + try { + return service.getMaximumSourceCapacity(sink); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; } private static void log(@NonNull String msg) { @@ -734,4 +954,9 @@ public final class BluetoothLeBroadcastAssistant implements BluetoothProfile { Log.d(TAG, msg); } } + + private static boolean isValidDevice(@Nullable BluetoothDevice device) { + return device != null && BluetoothAdapter + .checkBluetoothAddress(device.getAddress()); + } } diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcastAssistantCallback.java b/framework/java/android/bluetooth/BluetoothLeBroadcastAssistantCallback.java new file mode 100755 index 0000000000..96e04a9424 --- /dev/null +++ b/framework/java/android/bluetooth/BluetoothLeBroadcastAssistantCallback.java @@ -0,0 +1,271 @@ +/* + * Copyright 2022 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. + */ + +package android.bluetooth; + +import android.annotation.NonNull; +import android.os.Binder; +import android.os.RemoteException; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * @hide + */ +public class BluetoothLeBroadcastAssistantCallback + extends IBluetoothLeBroadcastAssistantCallback.Stub { + private static final String TAG = BluetoothLeBroadcastAssistantCallback.class.getSimpleName(); + private boolean mIsRegistered = false; + private final Map<BluetoothLeBroadcastAssistant.Callback, + Executor> mCallbackMap = new HashMap<>(); + IBluetoothLeBroadcastAssistant mAdapter; + + public BluetoothLeBroadcastAssistantCallback(IBluetoothLeBroadcastAssistant adapter) { + mAdapter = adapter; + } + + /** + * @hide + * @param executor an {@link Executor} to execute given callback + * @param callback user implementation of the {@link BluetoothLeBroadcastAssistant#Callback} + */ + public void register(@NonNull Executor executor, + @NonNull BluetoothLeBroadcastAssistant.Callback callback) { + synchronized (this) { + if (mCallbackMap.containsKey(callback)) { + return; + } + mCallbackMap.put(callback, executor); + + if (!mIsRegistered) { + try { + mAdapter.registerCallback(this); + mIsRegistered = true; + } catch (RemoteException e) { + Log.w(TAG, "Failed to register broaddcast assistant callback"); + Log.e(TAG, Log.getStackTraceString(new Throwable())); + } + } + } + } + + /** + * @hide + * @param callback user implementation of the {@link BluetoothLeBroadcastAssistant#Callback} + */ + public void unregister(@NonNull BluetoothLeBroadcastAssistant.Callback callback) { + synchronized (this) { + if (!mCallbackMap.containsKey(callback)) { + return; + } + mCallbackMap.remove(callback); + if (mCallbackMap.isEmpty() && mIsRegistered) { + try { + mAdapter.unregisterCallback(this); + mIsRegistered = false; + } catch (RemoteException e) { + Log.w(TAG, "Failed to unregister broaddcast assistant with service"); + Log.e(TAG, Log.getStackTraceString(new Throwable())); + } + } + } + } + + @Override + public void onSearchStarted(int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSearchStarted(reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSearchStartFailed(int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSearchStartFailed(reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSearchStopped(int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSearchStopped(reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSearchStopFailed(int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSearchStopFailed(reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSourceFound(BluetoothLeBroadcastMetadata source) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSourceFound(source)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSourceAdded(sink, sourceId, reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSourceAddFailed(BluetoothDevice sink, BluetoothLeBroadcastMetadata source, + int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSourceAddFailed(sink, source, reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSourceModified(BluetoothDevice sink, int sourceId, int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSourceModified(sink, sourceId, reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSourceModifyFailed(sink, sourceId, reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSourceRemoved(sink, sourceId, reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onSourceRemoveFailed(sink, sourceId, reason)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + + @Override + public void onReceiveStateChanged(BluetoothDevice sink, int sourceId, + BluetoothLeBroadcastReceiveState state) { + synchronized (this) { + for (BluetoothLeBroadcastAssistant.Callback cb : mCallbackMap.keySet()) { + Executor executor = mCallbackMap.get(cb); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> cb.onReceiveStateChanged(sink, sourceId, state)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } +} diff --git a/system/binder/Android.bp b/system/binder/Android.bp index 2756662f9c..805f68c8cd 100644 --- a/system/binder/Android.bp +++ b/system/binder/Android.bp @@ -57,5 +57,7 @@ filegroup { "android/bluetooth/le/IPeriodicAdvertisingCallback.aidl", "android/bluetooth/le/IScannerCallback.aidl", "android/bluetooth/IBluetoothConnectionCallback.aidl", + "android/bluetooth/IBluetoothLeBroadcastAssistantCallback.aidl", + "android/bluetooth/IBluetoothLeBroadcastAssistant.aidl", ], } diff --git a/system/binder/android/bluetooth/BluetoothLeBroadcastMetadata.aidl b/system/binder/android/bluetooth/BluetoothLeBroadcastMetadata.aidl new file mode 100644 index 0000000000..eb00836ec0 --- /dev/null +++ b/system/binder/android/bluetooth/BluetoothLeBroadcastMetadata.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2022 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. + */ + +package android.bluetooth; + +parcelable BluetoothLeBroadcastMetadata; diff --git a/system/binder/android/bluetooth/BluetoothLeBroadcastReceiveState.aidl b/system/binder/android/bluetooth/BluetoothLeBroadcastReceiveState.aidl new file mode 100644 index 0000000000..e3f13b0fbc --- /dev/null +++ b/system/binder/android/bluetooth/BluetoothLeBroadcastReceiveState.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2022 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. + */ + +package android.bluetooth; + +parcelable BluetoothLeBroadcastReceiveState; diff --git a/system/binder/android/bluetooth/IBluetoothLeBroadcastAssistant.aidl b/system/binder/android/bluetooth/IBluetoothLeBroadcastAssistant.aidl new file mode 100755 index 0000000000..789868544a --- /dev/null +++ b/system/binder/android/bluetooth/IBluetoothLeBroadcastAssistant.aidl @@ -0,0 +1,61 @@ +/* + * Copyright 2022 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. + */ + +package android.bluetooth; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.IBluetoothLeBroadcastAssistantCallback; +import android.bluetooth.le.ScanFilter; + +/** + * APIs for Bluetooth LE Audio Broadcast Assistant service + * + * @hide + */ +interface IBluetoothLeBroadcastAssistant { + // Public API + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + int getConnectionState(in BluetoothDevice sink); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + List<BluetoothDevice> getDevicesMatchingConnectionStates(in int[] states); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + List<BluetoothDevice> getConnectedDevices(); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + boolean setConnectionPolicy(in BluetoothDevice device, int connectionPolicy); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + int getConnectionPolicy(in BluetoothDevice device); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void registerCallback(in IBluetoothLeBroadcastAssistantCallback cb); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void unregisterCallback(in IBluetoothLeBroadcastAssistantCallback cb); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void startSearchingForSources(in List<ScanFilter> filters); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void stopSearchingForSources(); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + boolean isSearchInProgress(); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void addSource(in BluetoothDevice sink, in BluetoothLeBroadcastMetadata sourceMetadata, in boolean isGroupOp); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void modifySource(in BluetoothDevice sink, in int sourceId, in BluetoothLeBroadcastMetadata updatedMetadata); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void removeSource(in BluetoothDevice sink, in int sourceId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + List<BluetoothLeBroadcastReceiveState> getAllSources(in BluetoothDevice sink); + int getMaximumSourceCapacity(in BluetoothDevice sink); +} diff --git a/system/binder/android/bluetooth/IBluetoothLeBroadcastAssistantCallback.aidl b/system/binder/android/bluetooth/IBluetoothLeBroadcastAssistantCallback.aidl new file mode 100755 index 0000000000..5dbd98f234 --- /dev/null +++ b/system/binder/android/bluetooth/IBluetoothLeBroadcastAssistantCallback.aidl @@ -0,0 +1,43 @@ +/* + * Copyright 2022 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. + */ + +package android.bluetooth; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; + +/** +* Callback definitions for interacting with LE broadcast assistant service +* +* @hide +*/ +interface IBluetoothLeBroadcastAssistantCallback { + void onSearchStarted(in int reason); + void onSearchStartFailed(in int reason); + void onSearchStopped(in int reason); + void onSearchStopFailed(in int reason); + void onSourceFound(in BluetoothLeBroadcastMetadata source); + void onSourceAdded(in BluetoothDevice sink, in int sourceId, in int reason); + void onSourceAddFailed(in BluetoothDevice sink, in BluetoothLeBroadcastMetadata source, + in int reason); + void onSourceModified(in BluetoothDevice sink, in int sourceId, in int reason); + void onSourceModifyFailed(in BluetoothDevice sink, in int sourceId, in int reason); + void onSourceRemoved(in BluetoothDevice sink, in int sourceId, in int reason); + void onSourceRemoveFailed(in BluetoothDevice sink, in int sourceId, in int reason); + void onReceiveStateChanged(in BluetoothDevice sink, in int sourceId, + in BluetoothLeBroadcastReceiveState state); +} diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc index 28abdabf5e..2b836bc31b 100644 --- a/system/btif/src/btif_dm.cc +++ b/system/btif/src/btif_dm.cc @@ -96,6 +96,7 @@ const Uuid UUID_CSIS = Uuid::FromString("1846"); const Uuid UUID_LE_AUDIO = Uuid::FromString("184E"); const Uuid UUID_LE_MIDI = Uuid::FromString("03B80E5A-EDE8-4B33-A751-6CE34EC4C700"); const Uuid UUID_HAS = Uuid::FromString("1854"); +const Uuid UUID_BASS = Uuid::FromString("184F"); const bool enable_address_consolidate = true; // TODO remove #define COD_MASK 0x07FF @@ -1335,7 +1336,7 @@ static void btif_dm_search_devices_evt(tBTA_DM_SEARCH_EVT event, static bool btif_is_interesting_le_service(bluetooth::Uuid uuid) { return (uuid.As16Bit() == UUID_SERVCLASS_LE_HID || uuid == UUID_HEARING_AID || uuid == UUID_VC || uuid == UUID_CSIS || uuid == UUID_LE_AUDIO || - uuid == UUID_LE_MIDI || uuid == UUID_HAS); + uuid == UUID_LE_MIDI || uuid == UUID_HAS || uuid == UUID_BASS); } /******************************************************************************* diff --git a/system/gd/hci/controller.h b/system/gd/hci/controller.h index 573c695ad6..b1d1f3cc00 100644 --- a/system/gd/hci/controller.h +++ b/system/gd/hci/controller.h @@ -182,7 +182,7 @@ class Controller : public Module { static const ModuleFactory Factory; static constexpr uint64_t kDefaultEventMask = 0x3dbfffffffffffff; - static constexpr uint64_t kDefaultLeEventMask = 0x000000004d021e7f; + static constexpr uint64_t kDefaultLeEventMask = 0x000000004d02fe7f; protected: void ListDependencies(ModuleList* list) const override; |