summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJack He <siyuanh@google.com>2022-03-03 06:50:22 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-03-03 06:50:22 +0000
commit2b130e18935864a10ea554c96c217a07c09d295a (patch)
treebf0f0c8fd92d0a2d6ece056ea120e944ece9a25a
parent84e3f271b6248b9a2b646464d776640d92ee4b24 (diff)
parent43c9825758098f8a8effbabaf358ee36a15a5aab (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
-rw-r--r--android/app/AndroidManifest.xml9
-rw-r--r--android/app/res/values/config.xml1
-rwxr-xr-xandroid/app/src/com/android/bluetooth/bass_client/BaseData.java520
-rwxr-xr-xandroid/app/src/com/android/bluetooth/bass_client/BassClientService.java1261
-rwxr-xr-xandroid/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java1820
-rwxr-xr-xandroid/app/src/com/android/bluetooth/bass_client/BassConstants.java79
-rwxr-xr-xandroid/app/src/com/android/bluetooth/bass_client/BassUtils.java144
-rwxr-xr-xandroid/app/src/com/android/bluetooth/bass_client/PeriodicAdvertisementResult.java138
-rw-r--r--android/app/src/com/android/bluetooth/btservice/AdapterService.java30
-rw-r--r--android/app/src/com/android/bluetooth/btservice/Config.java4
-rw-r--r--android/app/src/com/android/bluetooth/btservice/PhonePolicy.java11
-rw-r--r--android/app/src/com/android/bluetooth/btservice/ServiceFactory.java5
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java2
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/Metadata.java5
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java22
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java5
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/110.json2
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/111.json316
-rw-r--r--framework/api/system-current.txt2
-rw-r--r--framework/java/android/bluetooth/BluetoothDevice.java11
-rwxr-xr-x[-rw-r--r--]framework/java/android/bluetooth/BluetoothLeBroadcastAssistant.java259
-rwxr-xr-xframework/java/android/bluetooth/BluetoothLeBroadcastAssistantCallback.java271
-rw-r--r--system/binder/Android.bp2
-rw-r--r--system/binder/android/bluetooth/BluetoothLeBroadcastMetadata.aidl19
-rw-r--r--system/binder/android/bluetooth/BluetoothLeBroadcastReceiveState.aidl19
-rwxr-xr-xsystem/binder/android/bluetooth/IBluetoothLeBroadcastAssistant.aidl61
-rwxr-xr-xsystem/binder/android/bluetooth/IBluetoothLeBroadcastAssistantCallback.aidl43
-rw-r--r--system/btif/src/btif_dm.cc3
-rw-r--r--system/gd/hci/controller.h2
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;