Merge "Factor out duplicated warning options"
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapMethodProxy.java b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
similarity index 83%
rename from android/app/src/com/android/bluetooth/pbap/BluetoothPbapMethodProxy.java
rename to android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
index 8846e60..5710451 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapMethodProxy.java
+++ b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.bluetooth.pbap;
+package com.android.bluetooth;
import android.content.ContentResolver;
import android.content.Context;
@@ -22,7 +22,6 @@
import android.net.Uri;
import android.util.Log;
-import com.android.bluetooth.Utils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.obex.HeaderSet;
@@ -31,22 +30,22 @@
/**
* Proxy class for method calls to help with unit testing
*/
-public class BluetoothPbapMethodProxy {
- private static final String TAG = BluetoothPbapMethodProxy.class.getSimpleName();
- private static BluetoothPbapMethodProxy sInstance;
+public class BluetoothMethodProxy {
+ private static final String TAG = BluetoothMethodProxy.class.getSimpleName();
+ private static BluetoothMethodProxy sInstance;
private static final Object INSTANCE_LOCK = new Object();
- private BluetoothPbapMethodProxy() {}
+ private BluetoothMethodProxy() {}
/**
* Get the singleton instance of proxy
*
* @return the singleton instance, guaranteed not null
*/
- public static BluetoothPbapMethodProxy getInstance() {
+ public static BluetoothMethodProxy getInstance() {
synchronized (INSTANCE_LOCK) {
if (sInstance == null) {
- sInstance = new BluetoothPbapMethodProxy();
+ sInstance = new BluetoothMethodProxy();
}
}
return sInstance;
@@ -58,7 +57,7 @@
* @param proxy a test instance of the BluetoothPbapMethodCallProxy
*/
@VisibleForTesting
- public static void setInstanceForTesting(BluetoothPbapMethodProxy proxy) {
+ public static void setInstanceForTesting(BluetoothMethodProxy proxy) {
Utils.enforceInstrumentationTestMode();
synchronized (INSTANCE_LOCK) {
Log.d(TAG, "setInstanceForTesting(), set to " + proxy);
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index c3f04a1..fda23c9 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -5247,6 +5247,7 @@
}
// Boolean flags
+ private static final String SDP_SERIALIZATION_FLAG = "INIT_sdp_serialization";
private static final String GD_CORE_FLAG = "INIT_gd_core";
private static final String GD_ADVERTISING_FLAG = "INIT_gd_advertising";
private static final String GD_SCANNING_FLAG = "INIT_gd_scanning";
@@ -5285,6 +5286,10 @@
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_CORE_FLAG, false)) {
initFlags.add(String.format("%s=%s", GD_CORE_FLAG, "true"));
}
+ if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH,
+ SDP_SERIALIZATION_FLAG, true)) {
+ initFlags.add(String.format("%s=%s", SDP_SERIALIZATION_FLAG, "true"));
+ }
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_ADVERTISING_FLAG, false)) {
initFlags.add(String.format("%s=%s", GD_ADVERTISING_FLAG, "true"));
}
diff --git a/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
index f85dbbc..172383e 100644
--- a/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
+++ b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
@@ -598,6 +598,24 @@
}
/**
+ * Get group ID for a given device and UUID
+ * @param device potential group member
+ * @param uuid profile context UUID
+ * @return group ID
+ */
+ public Integer getGroupId(BluetoothDevice device, ParcelUuid uuid) {
+ Map<Integer, Integer> device_groups =
+ mDeviceGroupIdRankMap.getOrDefault(device, new HashMap<>());
+ return mGroupIdToUuidMap.entrySet()
+ .stream()
+ .filter(e -> (device_groups.containsKey(e.getKey())
+ && e.getValue().equals(uuid)))
+ .map(Map.Entry::getKey)
+ .findFirst()
+ .orElse(IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID);
+ }
+
+ /**
* Get device's groups/
* @param device group member device
* @return map of group id and related uuids.
diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
index c6d3b0f..729aaf1 100644
--- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
+++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
@@ -93,6 +93,11 @@
private static LeAudioService sLeAudioService;
/**
+ * Indicates group audio support for none direction
+ */
+ private static final int AUDIO_DIRECTION_NONE = 0x00;
+
+ /**
* Indicates group audio support for input direction
*/
private static final int AUDIO_DIRECTION_INPUT_BIT = 0x01;
@@ -102,11 +107,6 @@
*/
private static final int AUDIO_DIRECTION_OUTPUT_BIT = 0x02;
- /*
- * Indicates no active contexts
- */
- private static final int ACTIVE_CONTEXTS_NONE = 0;
-
private AdapterService mAdapterService;
private DatabaseManager mDatabaseManager;
private HandlerThread mStateMachinesThread;
@@ -137,14 +137,14 @@
LeAudioGroupDescriptor() {
mIsConnected = false;
mIsActive = false;
- mActiveContexts = ACTIVE_CONTEXTS_NONE;
+ mDirection = AUDIO_DIRECTION_NONE;
mCodecStatus = null;
mLostLeadDeviceWhileStreaming = null;
}
public Boolean mIsConnected;
public Boolean mIsActive;
- public Integer mActiveContexts;
+ public Integer mDirection;
public BluetoothLeAudioCodecStatus mCodecStatus;
/* This can be non empty only for the streaming time */
BluetoothDevice mLostLeadDeviceWhileStreaming;
@@ -161,21 +161,6 @@
private final Map<BluetoothDevice, Integer> mDeviceGroupIdMap = new ConcurrentHashMap<>();
private final Map<BluetoothDevice, Integer> mDeviceAudioLocationMap = new ConcurrentHashMap<>();
- private final int mContextSupportingInputAudio = BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL
- | BluetoothLeAudio.CONTEXT_TYPE_VOICE_ASSISTANTS;
-
- private final int mContextSupportingOutputAudio = BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL
- | BluetoothLeAudio.CONTEXT_TYPE_MEDIA
- | BluetoothLeAudio.CONTEXT_TYPE_GAME
- | BluetoothLeAudio.CONTEXT_TYPE_INSTRUCTIONAL
- | BluetoothLeAudio.CONTEXT_TYPE_VOICE_ASSISTANTS
- | BluetoothLeAudio.CONTEXT_TYPE_LIVE
- | BluetoothLeAudio.CONTEXT_TYPE_SOUND_EFFECTS
- | BluetoothLeAudio.CONTEXT_TYPE_NOTIFICATIONS
- | BluetoothLeAudio.CONTEXT_TYPE_RINGTONE
- | BluetoothLeAudio.CONTEXT_TYPE_ALERTS
- | BluetoothLeAudio.CONTEXT_TYPE_EMERGENCY_ALARM;
-
private BroadcastReceiver mBondStateChangedReceiver;
private BroadcastReceiver mConnectionStateChangedReceiver;
private BroadcastReceiver mMuteStateChangedReceiver;
@@ -320,8 +305,8 @@
Integer group_id = entry.getKey();
if (descriptor.mIsActive) {
descriptor.mIsActive = false;
- updateActiveDevices(group_id, descriptor.mActiveContexts,
- ACTIVE_CONTEXTS_NONE, descriptor.mIsActive);
+ updateActiveDevices(group_id, descriptor.mDirection, AUDIO_DIRECTION_NONE,
+ descriptor.mIsActive);
break;
}
}
@@ -624,32 +609,6 @@
return result;
}
- /**
- * Get supported group audio direction from available context.
- *
- * @param activeContexts bitset of active context to be matched with possible audio direction
- * support.
- * @return matched possible audio direction support masked bitset
- * {@link #AUDIO_DIRECTION_INPUT_BIT} if input audio is supported
- * {@link #AUDIO_DIRECTION_OUTPUT_BIT} if output audio is supported
- */
- private Integer getAudioDirectionsFromActiveContextsMap(Integer activeContexts) {
- Integer supportedAudioDirections = 0;
-
- if (((activeContexts & mContextSupportingInputAudio) != 0)
- || (Utils.isPtsTestMode()
- && (activeContexts
- & (BluetoothLeAudio.CONTEXT_TYPE_RINGTONE
- | BluetoothLeAudio.CONTEXT_TYPE_MEDIA)) != 0)) {
- supportedAudioDirections |= AUDIO_DIRECTION_INPUT_BIT;
- }
- if ((activeContexts & mContextSupportingOutputAudio) != 0) {
- supportedAudioDirections |= AUDIO_DIRECTION_OUTPUT_BIT;
- }
-
- return supportedAudioDirections;
- }
-
private Integer getActiveGroupId() {
synchronized (mGroupLock) {
for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) {
@@ -801,12 +760,7 @@
}
private boolean updateActiveInDevice(BluetoothDevice device, Integer groupId,
- Integer oldActiveContexts, Integer newActiveContexts) {
- Integer oldSupportedAudioDirections =
- getAudioDirectionsFromActiveContextsMap(oldActiveContexts);
- Integer newSupportedAudioDirections =
- getAudioDirectionsFromActiveContextsMap(newActiveContexts);
-
+ Integer oldSupportedAudioDirections, Integer newSupportedAudioDirections) {
boolean oldSupportedByDeviceInput = (oldSupportedAudioDirections
& AUDIO_DIRECTION_INPUT_BIT) != 0;
boolean newSupportedByDeviceInput = (newSupportedAudioDirections
@@ -866,12 +820,7 @@
}
private boolean updateActiveOutDevice(BluetoothDevice device, Integer groupId,
- Integer oldActiveContexts, Integer newActiveContexts) {
- Integer oldSupportedAudioDirections =
- getAudioDirectionsFromActiveContextsMap(oldActiveContexts);
- Integer newSupportedAudioDirections =
- getAudioDirectionsFromActiveContextsMap(newActiveContexts);
-
+ Integer oldSupportedAudioDirections, Integer newSupportedAudioDirections) {
boolean oldSupportedByDeviceOutput = (oldSupportedAudioDirections
& AUDIO_DIRECTION_OUTPUT_BIT) != 0;
boolean newSupportedByDeviceOutput = (newSupportedAudioDirections
@@ -941,13 +890,13 @@
/**
* Report the active devices change to the active device manager and the media framework.
* @param groupId id of group which devices should be updated
- * @param newActiveContexts new active contexts for group of devices
- * @param oldActiveContexts old active contexts for group of devices
+ * @param newSupportedAudioDirections new supported audio directions for group of devices
+ * @param oldSupportedAudioDirections old supported audio directions for group of devices
* @param isActive if there is new active group
* @return true if group is active after change false otherwise.
*/
- private boolean updateActiveDevices(Integer groupId, Integer oldActiveContexts,
- Integer newActiveContexts, boolean isActive) {
+ private boolean updateActiveDevices(Integer groupId, Integer oldSupportedAudioDirections,
+ Integer newSupportedAudioDirections, boolean isActive) {
BluetoothDevice device = null;
if (isActive) {
@@ -955,9 +904,11 @@
}
boolean outReplaced =
- updateActiveOutDevice(device, groupId, oldActiveContexts, newActiveContexts);
+ updateActiveOutDevice(device, groupId, oldSupportedAudioDirections,
+ newSupportedAudioDirections);
boolean inReplaced =
- updateActiveInDevice(device, groupId, oldActiveContexts, newActiveContexts);
+ updateActiveInDevice(device, groupId, oldSupportedAudioDirections,
+ newSupportedAudioDirections);
if (outReplaced || inReplaced) {
Intent intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
@@ -1149,8 +1100,8 @@
return;
}
- descriptor.mIsActive = updateActiveDevices(groupId,
- ACTIVE_CONTEXTS_NONE, descriptor.mActiveContexts, true);
+ descriptor.mIsActive = updateActiveDevices(groupId, AUDIO_DIRECTION_NONE,
+ descriptor.mDirection, true);
if (descriptor.mIsActive) {
notifyGroupStatusChanged(groupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE);
@@ -1167,8 +1118,8 @@
}
descriptor.mIsActive = false;
- updateActiveDevices(groupId, descriptor.mActiveContexts,
- ACTIVE_CONTEXTS_NONE, descriptor.mIsActive);
+ updateActiveDevices(groupId, descriptor.mDirection, AUDIO_DIRECTION_NONE,
+ descriptor.mIsActive);
/* Clear lost devices */
if (DBG) Log.d(TAG, "Clear for group: " + groupId);
clearLostDevicesWhileStreaming(descriptor);
@@ -1342,13 +1293,13 @@
if (descriptor != null) {
if (descriptor.mIsActive) {
descriptor.mIsActive =
- updateActiveDevices(groupId, descriptor.mActiveContexts,
- available_contexts, descriptor.mIsActive);
+ updateActiveDevices(groupId, descriptor.mDirection, direction,
+ descriptor.mIsActive);
if (!descriptor.mIsActive) {
notifyGroupStatusChanged(groupId, BluetoothLeAudio.GROUP_STATUS_INACTIVE);
}
}
- descriptor.mActiveContexts = available_contexts;
+ descriptor.mDirection = direction;
} else {
Log.e(TAG, "no descriptors for group: " + groupId);
}
@@ -1669,8 +1620,8 @@
descriptor.mIsActive = false;
/* Update audio framework */
updateActiveDevices(myGroupId,
- descriptor.mActiveContexts,
- descriptor.mActiveContexts,
+ descriptor.mDirection,
+ descriptor.mDirection,
descriptor.mIsActive);
return;
}
@@ -1678,8 +1629,8 @@
if (descriptor.mIsActive) {
updateActiveDevices(myGroupId,
- descriptor.mActiveContexts,
- descriptor.mActiveContexts,
+ descriptor.mDirection,
+ descriptor.mDirection,
descriptor.mIsActive);
}
}
@@ -1933,6 +1884,13 @@
}
private void notifyGroupNodeAdded(BluetoothDevice device, int groupId) {
+ if (mVolumeControlService == null) {
+ mVolumeControlService = mServiceFactory.getVolumeControlService();
+ }
+ if (mVolumeControlService != null) {
+ mVolumeControlService.handleGroupNodeAdded(groupId, device);
+ }
+
if (mLeAudioCallbacks != null) {
int n = mLeAudioCallbacks.beginBroadcast();
for (int i = 0; i < n; i++) {
@@ -2796,7 +2754,7 @@
ProfileService.println(sb, " Group: " + groupId);
ProfileService.println(sb, " isActive: " + descriptor.mIsActive);
ProfileService.println(sb, " isConnected: " + descriptor.mIsConnected);
- ProfileService.println(sb, " mActiveContexts: " + descriptor.mActiveContexts);
+ ProfileService.println(sb, " mDirection: " + descriptor.mDirection);
ProfileService.println(sb, " group lead: " + getConnectedGroupLeadDevice(groupId));
ProfileService.println(sb, " first device: " + getFirstDeviceFromGroup(groupId));
ProfileService.println(sb, " lost lead device: "
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
index 3f37aac..123f145 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
+++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
@@ -24,6 +24,7 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.bluetooth.BluetoothMethodProxy;
import com.android.bluetooth.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.vcard.VCardBuilder;
@@ -107,7 +108,7 @@
return false;
}
- mCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(
+ mCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
mContext.getContentResolver(), contentUri, projection, selection, selectionArgs,
sortOrder);
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
index 1353152..e64772c 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
+++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
@@ -43,6 +43,7 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.bluetooth.BluetoothMethodProxy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.obex.ApplicationParameter;
import com.android.obex.HeaderSet;
@@ -242,7 +243,7 @@
private PbapStateMachine mStateMachine;
- private BluetoothPbapMethodProxy mPbapMethodProxy;
+ private BluetoothMethodProxy mPbapMethodProxy;
private enum ContactsType {
TYPE_PHONEBOOK , TYPE_SIM ;
@@ -272,7 +273,7 @@
mVcardManager = new BluetoothPbapVcardManager(mContext);
mVcardSimManager = new BluetoothPbapSimVcardManager(mContext);
mStateMachine = stateMachine;
- mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance();
+ mPbapMethodProxy = BluetoothMethodProxy.getInstance();
}
@Override
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java
index 5019169..22eac64 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java
+++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java
@@ -15,41 +15,35 @@
*/
package com.android.bluetooth.pbap;
-import com.android.bluetooth.R;
-
import android.content.ContentResolver;
+import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.vcard.VCardBuilder;
-import com.android.vcard.VCardConfig;
-import com.android.vcard.VCardConstants;
-import com.android.vcard.VCardUtils;
-
-import android.content.ContentValues;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
-import android.text.TextUtils;
-import android.util.Log;
-import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.bluetooth.BluetoothMethodProxy;
+import com.android.bluetooth.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.obex.Operation;
+import com.android.obex.ResponseCodes;
+import com.android.obex.ServerOperation;
+import com.android.vcard.VCardBuilder;
+import com.android.vcard.VCardConfig;
+import com.android.vcard.VCardUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
import java.util.Collections;
import java.util.Comparator;
-
-import com.android.obex.Operation;
-import com.android.obex.ResponseCodes;
-import com.android.obex.ServerOperation;
+import java.util.List;
/**
* VCard composer especially for Call Log used in Bluetooth.
@@ -119,7 +113,7 @@
}
//checkpoint Figure out if we can apply selection, projection and sort order.
- mCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mContentResolver,
+ mCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mContentResolver,
contentUri, SIM_PROJECTION, null, null, sortOrder);
if (mCursor == null) {
@@ -273,7 +267,7 @@
int size = 0;
Cursor contactCursor = null;
try {
- contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(
+ contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
mContentResolver, SIM_URI, SIM_PROJECTION, null,null, null);
if (contactCursor != null) {
size = contactCursor.getCount();
@@ -293,7 +287,7 @@
ArrayList<String> allnames = new ArrayList<String>();
Cursor contactCursor = null;
try {
- contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(
+ contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
mContentResolver, SIM_URI, SIM_PROJECTION, null,null,null);
if (contactCursor != null) {
for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
@@ -334,7 +328,7 @@
Cursor contactCursor = null;
try {
- contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(
+ contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
mContentResolver, SIM_URI, SIM_PROJECTION, null, null, null);
if (contactCursor != null) {
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
index 14e2647..658b82a 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
+++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
@@ -51,6 +51,7 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.bluetooth.BluetoothMethodProxy;
import com.android.bluetooth.R;
import com.android.bluetooth.util.DevicePolicyUtils;
import com.android.internal.annotations.VisibleForTesting;
@@ -182,7 +183,7 @@
selectionClause = Phone.STARRED + " = 1";
}
try {
- contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
myUri, new String[]{Phone.CONTACT_ID}, selectionClause,
null, Phone.CONTACT_ID);
if (contactCursor == null) {
@@ -209,7 +210,7 @@
int size = 0;
Cursor callCursor = null;
try {
- callCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ callCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
myUri, null, selection, null, CallLog.Calls.DEFAULT_SORT_ORDER);
if (callCursor != null) {
size = callCursor.getCount();
@@ -243,7 +244,7 @@
Cursor callCursor = null;
ArrayList<String> list = new ArrayList<String>();
try {
- callCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ callCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
myUri, projection, selection, null, CALLLOG_SORT_ORDER);
if (callCursor != null) {
for (callCursor.moveToFirst(); !callCursor.isAfterLast(); callCursor.moveToNext()) {
@@ -295,7 +296,7 @@
if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
orderBy = Phone.DISPLAY_NAME;
}
- contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy);
if (contactCursor != null) {
appendDistinctNameIdList(nameList, mContext.getString(android.R.string.unknownName),
@@ -354,7 +355,7 @@
final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
Cursor contactCursor = null;
try {
- contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
myUri, PHONES_CONTACTS_PROJECTION, null, null,
Phone.CONTACT_ID);
@@ -443,7 +444,7 @@
}
try {
- contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
uri, projection, null, null, Phone.CONTACT_ID);
if (contactCursor != null) {
@@ -478,7 +479,7 @@
long primaryVcMsb = 0;
ArrayList<String> list = new ArrayList<String>();
try {
- callCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ callCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
myUri, null, selection, null, null);
while (callCursor != null && callCursor.moveToNext()) {
count = count + 1;
@@ -522,7 +523,7 @@
long endPointId = 0;
try {
// Need test to see if order by _ID is ok here, or by date?
- callsCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ callsCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
myUri, CALLLOG_PROJECTION, typeSelection, null,
CALLLOG_SORT_ORDER);
if (callsCursor != null) {
@@ -596,7 +597,7 @@
}
try {
- contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
myUri, PHONES_CONTACTS_PROJECTION, selectionClause,
null, Phone.CONTACT_ID);
if (contactCursor != null) {
@@ -640,7 +641,7 @@
if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
orderBy = Phone.DISPLAY_NAME;
}
- contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+ contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy);
} catch (CursorWindowAllocationException e) {
Log.e(TAG, "CursorWindowAllocationException while composing phonebook one vcard");
diff --git a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
index e5cd625..d1e5fd0 100644
--- a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
+++ b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
@@ -26,6 +26,7 @@
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.BluetoothVolumeControl;
+import android.bluetooth.IBluetoothCsipSetCoordinator;
import android.bluetooth.IBluetoothLeAudio;
import android.bluetooth.IBluetoothVolumeControl;
import android.bluetooth.IBluetoothVolumeControlCallback;
@@ -47,6 +48,7 @@
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.ServiceFactory;
import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.le_audio.LeAudioService;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.SynchronousResultReceiver;
@@ -193,7 +195,8 @@
private BroadcastReceiver mBondStateChangedReceiver;
private BroadcastReceiver mConnectionStateChangedReceiver;
- private final ServiceFactory mFactory = new ServiceFactory();
+ @VisibleForTesting
+ ServiceFactory mFactory = new ServiceFactory();
public static boolean isEnabled() {
return BluetoothProperties.isProfileVcpControllerEnabled().orElse(false);
@@ -632,6 +635,29 @@
mVolumeControlNativeInterface.unmuteGroup(groupId);
}
+ /**
+ * {@hide}
+ */
+ public void handleGroupNodeAdded(int groupId, BluetoothDevice device) {
+ // Ignore disconnected device, its volume will be set once it connects
+ synchronized (mStateMachines) {
+ VolumeControlStateMachine sm = mStateMachines.get(device);
+ if (sm == null) {
+ return;
+ }
+ if (sm.getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
+ return;
+ }
+ }
+
+ // If group volume has already changed, the new group member should set it
+ Integer groupVolume = mGroupVolumeCache.getOrDefault(groupId,
+ IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME);
+ if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) {
+ mVolumeControlNativeInterface.setVolume(device, groupVolume);
+ }
+ }
+
void handleVolumeControlChanged(BluetoothDevice device, int groupId,
int volume, boolean mute, boolean isAutonomous) {
@@ -974,6 +1000,17 @@
}
removeStateMachine(device);
}
+ } else if (toState == BluetoothProfile.STATE_CONNECTED) {
+ // Restore the group volume if it was changed while the device was not yet connected.
+ CsipSetCoordinatorService csipClient = mFactory.getCsipSetCoordinatorService();
+ Integer groupId = csipClient.getGroupId(device, BluetoothUuid.CAP);
+ if (groupId != IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID) {
+ Integer groupVolume = mGroupVolumeCache.getOrDefault(groupId,
+ IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME);
+ if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) {
+ mVolumeControlNativeInterface.setVolume(device, groupVolume);
+ }
+ }
}
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoListingElementTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoListingElementTest.java
new file mode 100644
index 0000000..84b0d7f
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoListingElementTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.SignedLongLong;
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+import com.android.internal.util.FastXmlSerializer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class BluetoothMapConvoListingElementTest {
+ private static final long TEST_ID = 1111;
+ private static final String TEST_NAME = "test_name";
+ private static final long TEST_LAST_ACTIVITY = 0;
+ private static final boolean TEST_READ = true;
+ private static final boolean TEST_REPORT_READ = true;
+ private static final long TEST_VERSION_COUNTER = 0;
+ private static final int TEST_CURSOR_INDEX = 1;
+ private static final TYPE TEST_TYPE = TYPE.EMAIL;
+ private static final String TEST_SUMMARY = "test_summary";
+ private static final String TEST_SMS_MMS_CONTACTS = "test_sms_mms_contacts";
+
+ private final BluetoothMapConvoContactElement TEST_CONTACT_ELEMENT_ONE =
+ new BluetoothMapConvoContactElement("test_uci_one", "test_name_one",
+ "test_display_name_one", "test_presence_status_one", 2, TEST_LAST_ACTIVITY, 2,
+ 1, "1111");
+
+ private final BluetoothMapConvoContactElement TEST_CONTACT_ELEMENT_TWO =
+ new BluetoothMapConvoContactElement("test_uci_two", "test_name_two",
+ "test_display_name_two", "test_presence_status_two", 1, TEST_LAST_ACTIVITY, 1,
+ 2, "1112");
+
+ private final List<BluetoothMapConvoContactElement> TEST_CONTACTS = new ArrayList<>(
+ Arrays.asList(TEST_CONTACT_ELEMENT_ONE, TEST_CONTACT_ELEMENT_TWO));
+
+ private final SignedLongLong signedLongLong = new SignedLongLong(TEST_ID, 0);
+
+ private BluetoothMapConvoListingElement mListingElement;
+
+ @Before
+ public void setUp() throws Exception {
+ mListingElement = new BluetoothMapConvoListingElement();
+
+ mListingElement.setCursorIndex(TEST_CURSOR_INDEX);
+ mListingElement.setVersionCounter(TEST_VERSION_COUNTER);
+ mListingElement.setName(TEST_NAME);
+ mListingElement.setType(TEST_TYPE);
+ mListingElement.setContacts(TEST_CONTACTS);
+ mListingElement.setLastActivity(TEST_LAST_ACTIVITY);
+ mListingElement.setRead(TEST_READ, TEST_REPORT_READ);
+ mListingElement.setConvoId(0, TEST_ID);
+ mListingElement.setSummary(TEST_SUMMARY);
+ mListingElement.setSmsMmsContacts(TEST_SMS_MMS_CONTACTS);
+ }
+
+ @Test
+ public void getters() throws Exception {
+ assertThat(mListingElement.getCursorIndex()).isEqualTo(TEST_CURSOR_INDEX);
+ assertThat(mListingElement.getVersionCounter()).isEqualTo(TEST_VERSION_COUNTER);
+ assertThat(mListingElement.getName()).isEqualTo(TEST_NAME);
+ assertThat(mListingElement.getType()).isEqualTo(TEST_TYPE);
+ assertThat(mListingElement.getContacts()).isEqualTo(TEST_CONTACTS);
+ assertThat(mListingElement.getLastActivity()).isEqualTo(TEST_LAST_ACTIVITY);
+ assertThat(mListingElement.getRead()).isEqualTo("READ");
+ assertThat(mListingElement.getReadBool()).isEqualTo(TEST_READ);
+ assertThat(mListingElement.getConvoId()).isEqualTo(signedLongLong.toHexString());
+ assertThat(mListingElement.getCpConvoId()).isEqualTo(
+ signedLongLong.getLeastSignificantBits());
+ assertThat(mListingElement.getFullSummary()).isEqualTo(TEST_SUMMARY);
+ assertThat(mListingElement.getSmsMmsContacts()).isEqualTo(TEST_SMS_MMS_CONTACTS);
+ }
+
+ @Test
+ public void incrementVersionCounter() {
+ mListingElement.incrementVersionCounter();
+ assertThat(mListingElement.getVersionCounter()).isEqualTo(TEST_VERSION_COUNTER + 1);
+ }
+
+ @Test
+ public void removeContactWithObject() {
+ mListingElement.removeContact(TEST_CONTACT_ELEMENT_TWO);
+ assertThat(mListingElement.getContacts().size()).isEqualTo(1);
+ }
+
+ @Test
+ public void removeContactWithIndex() {
+ mListingElement.removeContact(1);
+ assertThat(mListingElement.getContacts().size()).isEqualTo(1);
+ }
+
+ @Test
+ public void encodeToXml_thenDecodeToInstance_returnsCorrectly() throws Exception {
+ final XmlSerializer serializer = new FastXmlSerializer();
+ final StringWriter writer = new StringWriter();
+
+ serializer.setOutput(writer);
+ serializer.startDocument("UTF-8", true);
+ mListingElement.encode(serializer);
+ serializer.endDocument();
+
+ final XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance();
+ parserFactory.setNamespaceAware(true);
+ final XmlPullParser parser;
+ parser = parserFactory.newPullParser();
+
+ parser.setInput(new StringReader(writer.toString()));
+ parser.next();
+
+ BluetoothMapConvoListingElement listingElementFromXml =
+ BluetoothMapConvoListingElement.createFromXml(parser);
+
+ assertThat(listingElementFromXml.getVersionCounter()).isEqualTo(0);
+ assertThat(listingElementFromXml.getName()).isEqualTo(TEST_NAME);
+ assertThat(listingElementFromXml.getContacts()).isEqualTo(TEST_CONTACTS);
+ assertThat(listingElementFromXml.getLastActivity()).isEqualTo(TEST_LAST_ACTIVITY);
+ assertThat(listingElementFromXml.getRead()).isEqualTo("UNREAD");
+ assertThat(listingElementFromXml.getConvoId()).isEqualTo(signedLongLong.toHexString());
+ assertThat(listingElementFromXml.getFullSummary().trim()).isEqualTo(TEST_SUMMARY);
+ }
+
+ @Test
+ public void equalsWithSameValues_returnsTrue() {
+ BluetoothMapConvoListingElement listingElement = new BluetoothMapConvoListingElement();
+ listingElement.setName(TEST_NAME);
+ listingElement.setContacts(TEST_CONTACTS);
+ listingElement.setLastActivity(TEST_LAST_ACTIVITY);
+ listingElement.setRead(TEST_READ, TEST_REPORT_READ);
+
+ BluetoothMapConvoListingElement listingElementEqual = new BluetoothMapConvoListingElement();
+ listingElementEqual.setName(TEST_NAME);
+ listingElementEqual.setContacts(TEST_CONTACTS);
+ listingElementEqual.setLastActivity(TEST_LAST_ACTIVITY);
+ listingElementEqual.setRead(TEST_READ, TEST_REPORT_READ);
+
+ assertThat(listingElement).isEqualTo(listingElementEqual);
+ }
+
+ @Test
+ public void equalsWithDifferentRead_returnsFalse() {
+ BluetoothMapConvoListingElement
+ listingElement = new BluetoothMapConvoListingElement();
+
+ BluetoothMapConvoListingElement listingElementWithDifferentRead =
+ new BluetoothMapConvoListingElement();
+ listingElementWithDifferentRead.setRead(TEST_READ, TEST_REPORT_READ);
+
+ assertThat(listingElement).isNotEqualTo(listingElementWithDifferentRead);
+ }
+
+ @Test
+ public void compareToWithSameValues_returnsZero() {
+ BluetoothMapConvoListingElement
+ listingElement = new BluetoothMapConvoListingElement();
+ listingElement.setLastActivity(TEST_LAST_ACTIVITY);
+
+ BluetoothMapConvoListingElement listingElementSameLastActivity =
+ new BluetoothMapConvoListingElement();
+ listingElementSameLastActivity.setLastActivity(TEST_LAST_ACTIVITY);
+
+ assertThat(listingElement.compareTo(listingElementSameLastActivity)).isEqualTo(0);
+ }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java
index d8e2b62..33089f4 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java
@@ -38,6 +38,8 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.bluetooth.BluetoothMethodProxy;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -61,7 +63,7 @@
private BluetoothPbapCallLogComposer mComposer;
@Spy
- BluetoothPbapMethodProxy mPbapCallProxy = BluetoothPbapMethodProxy.getInstance();
+ BluetoothMethodProxy mPbapCallProxy = BluetoothMethodProxy.getInstance();
@Mock
Cursor mMockCursor;
@@ -69,7 +71,7 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- BluetoothPbapMethodProxy.setInstanceForTesting(mPbapCallProxy);
+ BluetoothMethodProxy.setInstanceForTesting(mPbapCallProxy);
doReturn(mMockCursor).when(mPbapCallProxy)
.contentResolverQuery(any(), any(), any(), any(), any(), any());
@@ -82,7 +84,7 @@
@After
public void tearDown() throws Exception {
- BluetoothPbapMethodProxy.setInstanceForTesting(null);
+ BluetoothMethodProxy.setInstanceForTesting(null);
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java
index b888930..1ad0918 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java
@@ -27,15 +27,14 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.content.Context;
import android.os.Handler;
import android.os.UserManager;
-import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.bluetooth.BluetoothMethodProxy;
import com.android.obex.HeaderSet;
import com.android.obex.Operation;
import com.android.obex.ResponseCodes;
@@ -63,7 +62,7 @@
@Mock PbapStateMachine mMockStateMachine;
@Spy
- BluetoothPbapMethodProxy mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance();
+ BluetoothMethodProxy mPbapMethodProxy = BluetoothMethodProxy.getInstance();
BluetoothPbapObexServer mServer;
@@ -85,14 +84,14 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- BluetoothPbapMethodProxy.setInstanceForTesting(mPbapMethodProxy);
+ BluetoothMethodProxy.setInstanceForTesting(mPbapMethodProxy);
mServer = new BluetoothPbapObexServer(
mMockHandler, InstrumentationRegistry.getTargetContext(), mMockStateMachine);
}
@After
public void tearDown() throws Exception {
- BluetoothPbapMethodProxy.setInstanceForTesting(null);
+ BluetoothMethodProxy.setInstanceForTesting(null);
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java
index 5492e1c..0230275 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java
@@ -35,6 +35,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.bluetooth.BluetoothMethodProxy;
import com.android.obex.Operation;
import com.android.obex.ResponseCodes;
@@ -60,7 +61,7 @@
private static final String TAG = BluetoothPbapSimVcardManagerTest.class.getSimpleName();
@Spy
- BluetoothPbapMethodProxy mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance();
+ BluetoothMethodProxy mPbapMethodProxy = BluetoothMethodProxy.getInstance();
Context mContext;
BluetoothPbapSimVcardManager mManager;
@@ -70,14 +71,14 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- BluetoothPbapMethodProxy.setInstanceForTesting(mPbapMethodProxy);
+ BluetoothMethodProxy.setInstanceForTesting(mPbapMethodProxy);
mContext = InstrumentationRegistry.getTargetContext();
mManager = new BluetoothPbapSimVcardManager(mContext);
}
@After
public void tearDown() {
- BluetoothPbapMethodProxy.setInstanceForTesting(null);
+ BluetoothMethodProxy.setInstanceForTesting(null);
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java
index b6cceb5..fa8bb5a 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java
@@ -27,7 +27,6 @@
import android.content.Context;
import android.database.Cursor;
-import android.net.Uri;
import android.provider.CallLog;
import android.provider.ContactsContract;
@@ -35,6 +34,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.bluetooth.BluetoothMethodProxy;
import com.android.bluetooth.R;
import org.junit.After;
@@ -56,7 +56,7 @@
private static final String TAG = BluetoothPbapVcardManagerTest.class.getSimpleName();
@Spy
- BluetoothPbapMethodProxy mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance();
+ BluetoothMethodProxy mPbapMethodProxy = BluetoothMethodProxy.getInstance();
Context mContext;
BluetoothPbapVcardManager mManager;
@@ -64,14 +64,14 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- BluetoothPbapMethodProxy.setInstanceForTesting(mPbapMethodProxy);
+ BluetoothMethodProxy.setInstanceForTesting(mPbapMethodProxy);
mContext = InstrumentationRegistry.getTargetContext();
mManager = new BluetoothPbapVcardManager(mContext);
}
@After
public void tearDown() {
- BluetoothPbapMethodProxy.setInstanceForTesting(null);
+ BluetoothMethodProxy.setInstanceForTesting(null);
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
index 7ca7623..bdd8169 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
@@ -41,7 +41,9 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.ServiceFactory;
import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.x.com.android.modules.utils.SynchronousResultReceiver;
@@ -73,6 +75,7 @@
private VolumeControlService mService;
private VolumeControlService.BluetoothVolumeControlBinder mServiceBinder;
private BluetoothDevice mDevice;
+ private BluetoothDevice mDeviceTwo;
private HashMap<BluetoothDevice, LinkedBlockingQueue<Intent>> mDeviceQueueMap;
private static final int TIMEOUT_MS = 1000;
private static final int BT_LE_AUDIO_MAX_VOL = 255;
@@ -87,6 +90,8 @@
@Mock private DatabaseManager mDatabaseManager;
@Mock private VolumeControlNativeInterface mNativeInterface;
@Mock private AudioManager mAudioManager;
+ @Mock private ServiceFactory mServiceFactory;
+ @Mock private CsipSetCoordinatorService mCsipService;
@Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
@@ -119,9 +124,12 @@
startService();
mService.mVolumeControlNativeInterface = mNativeInterface;
mService.mAudioManager = mAudioManager;
+ mService.mFactory = mServiceFactory;
mServiceBinder = (VolumeControlService.BluetoothVolumeControlBinder) mService.initBinder();
mServiceBinder.mIsTesting = true;
+ doReturn(mCsipService).when(mServiceFactory).getCsipSetCoordinatorService();
+
// Override the timeout value to speed up the test
VolumeControlStateMachine.sConnectTimeoutMs = TIMEOUT_MS; // 1s
@@ -134,8 +142,10 @@
// Get a device for testing
mDevice = TestUtils.getTestDevice(mAdapter, 0);
+ mDeviceTwo = TestUtils.getTestDevice(mAdapter, 1);
mDeviceQueueMap = new HashMap<>();
mDeviceQueueMap.put(mDevice, new LinkedBlockingQueue<>());
+ mDeviceQueueMap.put(mDeviceTwo, new LinkedBlockingQueue<>());
doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService)
.getBondState(any(BluetoothDevice.class));
doReturn(new ParcelUuid[]{BluetoothUuid.VOLUME_CONTROL}).when(mAdapterService)
@@ -631,6 +641,92 @@
Assert.assertEquals(volume, mService.getGroupVolume(groupId));
}
+ /**
+ * Test setting volume for a group member who connects after the volume level
+ * for a group was already changed and cached.
+ */
+ @Test
+ public void testLateConnectingDevice() throws Exception {
+ int groupId = 1;
+ int groupVolume = 56;
+
+ // Both devices are in the same group
+ when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId);
+ when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId);
+
+ // Update the device policy so okToConnect() returns true
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(any(BluetoothDevice.class),
+ eq(BluetoothProfile.VOLUME_CONTROL)))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class));
+
+ generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.STATE_DISCONNECTED);
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
+ mService.getConnectionState(mDevice));
+ Assert.assertTrue(mService.getDevices().contains(mDevice));
+
+ mService.setGroupVolume(groupId, groupVolume);
+ verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(groupVolume));
+ verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume));
+
+ // Verify that second device gets the proper group volume level when connected
+ generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.STATE_DISCONNECTED);
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
+ mService.getConnectionState(mDeviceTwo));
+ Assert.assertTrue(mService.getDevices().contains(mDeviceTwo));
+ verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume));
+ }
+
+ /**
+ * Test setting volume for a new group member who is discovered after the volume level
+ * for a group was already changed and cached.
+ */
+ @Test
+ public void testLateDiscoveredGroupMember() throws Exception {
+ int groupId = 1;
+ int groupVolume = 56;
+
+ // For now only one device is in the group
+ when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId);
+ when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(-1);
+
+ // Update the device policy so okToConnect() returns true
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(any(BluetoothDevice.class),
+ eq(BluetoothProfile.VOLUME_CONTROL)))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class));
+
+ generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.STATE_DISCONNECTED);
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
+ mService.getConnectionState(mDevice));
+ Assert.assertTrue(mService.getDevices().contains(mDevice));
+
+ // Set the group volume
+ mService.setGroupVolume(groupId, groupVolume);
+
+ // Verify that second device will not get the group volume level if it is not a group member
+ generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.STATE_DISCONNECTED);
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
+ mService.getConnectionState(mDeviceTwo));
+ Assert.assertTrue(mService.getDevices().contains(mDeviceTwo));
+ verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume));
+
+ // But gets the volume when it becomes the group member
+ when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId);
+ mService.handleGroupNodeAdded(groupId, mDeviceTwo);
+ verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume));
+ }
+
@Test
public void testServiceBinderGetDevicesMatchingConnectionStates() throws Exception {
final SynchronousResultReceiver<List<BluetoothDevice>> recv =
diff --git a/android/pandora/.gitignore b/android/pandora/.gitignore
new file mode 100644
index 0000000..cdb0870
--- /dev/null
+++ b/android/pandora/.gitignore
@@ -0,0 +1,3 @@
+trace*
+log*
+out*
diff --git a/android/pandora/gen_cov.py b/android/pandora/gen_cov.py
new file mode 100755
index 0000000..e37c704
--- /dev/null
+++ b/android/pandora/gen_cov.py
@@ -0,0 +1,320 @@
+#!/usr/bin/env python
+
+import argparse
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+import xml.etree.ElementTree as ET
+
+
+def run_pts_bot():
+ run_pts_bot_cmd = [
+ # atest command with verbose mode.
+ 'atest',
+ '-d',
+ '-v',
+ 'pts-bot',
+ # Coverage tool chains and specify that coverage should be flush to the
+ # disk between each tests.
+ '--',
+ '--coverage',
+ '--coverage-toolchain JACOCO',
+ '--coverage-toolchain CLANG',
+ '--coverage-flush',
+ ]
+ subprocess.run(run_pts_bot_cmd).returncode
+
+
+def run_unit_tests():
+
+ # Output logs directory
+ logs_out = Path('logs_bt_tests')
+ logs_out.mkdir(exist_ok=True)
+
+ mts_tests = []
+ android_build_top = os.getenv('ANDROID_BUILD_TOP')
+ mts_xml = ET.parse(
+ f'{android_build_top}/test/mts/tools/mts-tradefed/res/config/mts-bluetooth-tests-list.xml'
+ )
+
+ for child in mts_xml.getroot():
+ value = child.attrib['value']
+ if 'enable:true' in value:
+ test = value.replace(':enable:true', '')
+ mts_tests.append(test)
+
+ for test in mts_tests:
+ print(f'Test started: {test}')
+
+ # Env variables necessary for native unit tests.
+ env = os.environ.copy()
+ env['CLANG_COVERAGE_CONTINUOUS_MODE'] = 'true'
+ env['CLANG_COVERAGE'] = 'true'
+ env['NATIVE_COVERAGE_PATHS'] = 'packages/modules/Bluetooth'
+ run_test_cmd = [
+ # atest command with verbose mode.
+ 'atest',
+ '-d',
+ '-v',
+ test,
+ # Coverage tool chains and specify that coverage should be flush to the
+ # disk between each tests.
+ '--',
+ '--coverage',
+ '--coverage-toolchain JACOCO',
+ '--coverage-toolchain CLANG',
+ '--coverage-flush',
+ # Allows tests to use hidden APIs.
+ '--test-arg '
+ 'com.android.compatibility.testtype.LibcoreTest:hidden-api-checks:false',
+ '--test-arg '
+ 'com.android.tradefed.testtype.AndroidJUnitTest:hidden-api-checks:false',
+ '--test-arg '
+ 'com.android.tradefed.testtype.InstrumentationTest:hidden-api-checks:false',
+ '--skip-system-status-check '
+ 'com.android.tradefed.suite.checker.ShellStatusChecker',
+ ]
+ with open(f'{logs_out}/{test}.txt', 'w') as f:
+ returncode = subprocess.run(
+ run_test_cmd, env=env, stdout=f, stderr=subprocess.STDOUT).returncode
+ print(
+ f'Test ended [{"Success" if returncode == 0 else "Failed"}]: {test}')
+
+
+def generate_java_coverage(bt_apex_name, trace_path, coverage_out):
+
+ out = os.getenv('OUT')
+ android_host_out = os.getenv('ANDROID_HOST_OUT')
+
+ java_coverage_out = Path(f'{coverage_out}/java')
+ temp_path = Path(f'{coverage_out}/temp')
+ if temp_path.exists():
+ shutil.rmtree(temp_path, ignore_errors=True)
+ temp_path.mkdir()
+
+ framework_jar_path = Path(
+ f'{out}/obj/PACKAGING/jacoco_intermediates/JAVA_LIBRARIES/framework-bluetooth.{bt_apex_name}_intermediates'
+ )
+ service_jar_path = Path(
+ f'{out}/obj/PACKAGING/jacoco_intermediates/JAVA_LIBRARIES/service-bluetooth.{bt_apex_name}_intermediates'
+ )
+ app_jar_path = Path(
+ f'{out}/obj/PACKAGING/jacoco_intermediates/ETC/Bluetooth{"Google" if "com.google" in bt_apex_name else ""}.{bt_apex_name}_intermediates'
+ )
+
+ # From google3/configs/wireless/android/testing/atp/prod/mainline-engprod/templates/modules/bluetooth.gcl.
+ framework_exclude_classes = [
+ '**/com/android/bluetooth/x/**/*.class',
+ '**/*Test$*.class',
+ '**/android/bluetooth/I*$Default.class',
+ '**/android/bluetooth/**/I*$Default.class',
+ '**/android/bluetooth/I*$Stub.class',
+ '**/android/bluetooth/**/I*$Stub.class',
+ '**/android/bluetooth/I*$Stub$Proxy.class',
+ '**/android/bluetooth/**/I*$Stub$Proxy.class',
+ '**/com/android/internal/util/**/*.class',
+ '**/android/net/**/*.class',
+ ]
+ service_exclude_classes = [
+ '**/com/android/bluetooth/x/**/*.class',
+ '**/androidx/**/*.class',
+ '**/android/net/**/*.class',
+ '**/android/support/**/*.class',
+ '**/kotlin/**/*.class',
+ '**/*Test$*.class',
+ '**/com/android/internal/annotations/**/*.class',
+ '**/android/annotation/**/*.class',
+ '**/android/net/**/*.class',
+ ]
+ app_exclude_classes = [
+ '**/*Test$*.class',
+ '**/com/android/bluetooth/x/**/*.class',
+ '**/com/android/internal/annotations/**/*.class',
+ '**/com/android/internal/util/**/*.class',
+ '**/android/annotation/**/*.class',
+ '**/android/net/**/*.class',
+ '**/android/support/v4/**/*.class',
+ '**/androidx/**/*.class',
+ '**/kotlin/**/*.class',
+ '**/com/google/**/*.class',
+ '**/javax/**/*.class',
+ '**/android/hardware/**/*.class', # Added
+ '**/android/hidl/**/*.class', # Added
+ '**/com/android/bluetooth/**/BluetoothMetrics*.class', # Added
+ ]
+
+ # Merged ec files.
+ merged_ec_path = Path(f'{temp_path}/merged.ec')
+ subprocess.run((
+ f'java -jar {android_host_out}/framework/jacoco-cli.jar merge {trace_path.absolute()}/*.ec '
+ f'--destfile {merged_ec_path.absolute()}'),
+ shell=True)
+
+ # Copy and extract jar files.
+ framework_temp_path = Path(f'{temp_path}/{framework_jar_path.name}')
+ service_temp_path = Path(f'{temp_path}/{service_jar_path.name}')
+ app_temp_path = Path(f'{temp_path}/{app_jar_path.name}')
+
+ shutil.copytree(framework_jar_path, framework_temp_path)
+ shutil.copytree(service_jar_path, service_temp_path)
+ shutil.copytree(app_jar_path, app_temp_path)
+
+ current_dir_path = Path.cwd()
+ for p in [framework_temp_path, service_temp_path, app_temp_path]:
+ os.chdir(p.absolute())
+ os.system('jar xf jacoco-report-classes.jar')
+ os.chdir(current_dir_path)
+
+ os.remove(f'{framework_temp_path}/jacoco-report-classes.jar')
+ os.remove(f'{service_temp_path}/jacoco-report-classes.jar')
+ os.remove(f'{app_temp_path}/jacoco-report-classes.jar')
+
+ # Generate coverage report.
+ exclude_classes = []
+ for glob in framework_exclude_classes:
+ exclude_classes.extend(list(framework_temp_path.glob(glob)))
+ for glob in service_exclude_classes:
+ exclude_classes.extend(list(service_temp_path.glob(glob)))
+ for glob in app_exclude_classes:
+ exclude_classes.extend(list(app_temp_path.glob(glob)))
+
+ for c in exclude_classes:
+ if c.exists():
+ os.remove(c.absolute())
+
+ gen_java_cov_report_cmd = [
+ f'java',
+ f'-jar',
+ f'{android_host_out}/framework/jacoco-cli.jar',
+ f'report',
+ f'{merged_ec_path.absolute()}',
+ f'--classfiles',
+ f'{temp_path.absolute()}',
+ f'--html',
+ f'{java_coverage_out.absolute()}',
+ f'--name',
+ f'{java_coverage_out.absolute()}.html',
+ ]
+ subprocess.run(gen_java_cov_report_cmd)
+
+ # Cleanup.
+ shutil.rmtree(temp_path, ignore_errors=True)
+
+
+def generate_native_coverage(bt_apex_name, trace_path, coverage_out):
+
+ out = os.getenv('OUT')
+ android_build_top = os.getenv('ANDROID_BUILD_TOP')
+
+ native_coverage_out = Path(f'{coverage_out}/native')
+ temp_path = Path(f'{coverage_out}/temp')
+ if temp_path.exists():
+ shutil.rmtree(temp_path, ignore_errors=True)
+ temp_path.mkdir()
+
+ # From google3/configs/wireless/android/testing/atp/prod/mainline-engprod/templates/modules/bluetooth.gcl.
+ exclude_files = {
+ 'system/.*_aidl.*',
+ 'system/.*_test.*',
+ 'system/.*_mock.*',
+ 'system/.*_unittest.*',
+ 'system/binder/',
+ 'system/blueberry/',
+ 'system/build/',
+ 'system/conf/',
+ 'system/doc/',
+ 'system/test/',
+ 'system/gd/l2cap/',
+ 'system/gd/security/',
+ 'system/gd/neighbor/',
+ # 'android/', # Should not be excluded
+ }
+
+ # Merge profdata files.
+ profdata_path = Path(f'{temp_path}/coverage.profdata')
+ subprocess.run(
+ f'llvm-profdata merge --sparse -o {profdata_path.absolute()} {trace_path.absolute()}/*.profraw',
+ shell=True)
+
+ gen_native_cov_report_cmd = [
+ f'llvm-cov',
+ f'show',
+ f'-format=html',
+ f'-output-dir={native_coverage_out.absolute()}',
+ f'-instr-profile={profdata_path.absolute()}',
+ f'{out}/symbols/apex/{bt_apex_name}/lib64/libbluetooth_jni.so',
+ f'-path-equivalence=/proc/self/cwd,{android_build_top}',
+ f'/proc/self/cwd/packages/modules/Bluetooth',
+ ]
+ for f in exclude_files:
+ gen_native_cov_report_cmd.append(f'-ignore-filename-regex={f}')
+ subprocess.run(gen_native_cov_report_cmd, cwd=android_build_top)
+
+ # Cleanup.
+ shutil.rmtree(temp_path, ignore_errors=True)
+
+
+if __name__ == '__main__':
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--apex-name',
+ default='com.android.btservices',
+ help='bluetooth apex name. Default: com.android.btservices')
+ parser.add_argument(
+ '--java', action='store_true', help='generate Java coverage')
+ parser.add_argument(
+ '--native', action='store_true', help='generate native coverage')
+ parser.add_argument(
+ '--out',
+ type=str,
+ default='out_coverage',
+ help='out directory for coverage reports. Default: ./out_coverage')
+ parser.add_argument(
+ '--trace',
+ type=str,
+ default='trace',
+ help='trace directory with .ec and .profraw files. Default: ./trace')
+ parser.add_argument(
+ '--full-report',
+ action='store_true',
+ help='run all tests and compute coverage report')
+ args = parser.parse_args()
+
+ coverage_out = Path(args.out)
+ shutil.rmtree(coverage_out, ignore_errors=True)
+ coverage_out.mkdir()
+
+ if not args.full_report:
+ trace_path = Path(args.trace)
+ if (not trace_path.exists() or not trace_path.is_dir()):
+ sys.exit('Trace directory does not exist')
+
+ if (args.java):
+ generate_java_coverage(args.bt_apex_name, trace_path, coverage_out)
+ if (args.native):
+ generate_native_coverage(args.bt_apex_name, trace_path, coverage_out)
+
+ else:
+ # Compute Pandora coverage.
+ run_pts_bot()
+ coverage_out_pandora = Path(f'{coverage_out}/pandora')
+ coverage_out_pandora.mkdir()
+ trace_pandora = Path('trace_pandora')
+ subprocess.run(['adb', 'pull', '/data/misc/trace', trace_pandora])
+ generate_java_coverage(args.bt_apex_name, trace_pandora,
+ coverage_out_pandora)
+ generate_native_coverage(args.bt_apex_name, trace_pandora,
+ coverage_out_pandora)
+
+ # Compute all coverage.
+ run_unit_tests()
+ coverage_out_mainline = Path(f'{coverage_out}/mainline')
+ coverage_out_pandora.mkdir()
+ trace_all = Path('trace_all')
+ subprocess.run(['adb', 'pull', '/data/misc/trace', trace_all])
+ generate_java_coverage(args.bt_apex_name, trace_all, coverage_out_mainline)
+ generate_native_coverage(args.bt_apex_name, trace_all,
+ coverage_out_mainline)
diff --git a/android/pandora/mmi2grpc/mmi2grpc/l2cap.py b/android/pandora/mmi2grpc/mmi2grpc/l2cap.py
index 2baefcd..760758d 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/l2cap.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/l2cap.py
@@ -1,13 +1,15 @@
import time
+import sys
from mmi2grpc._helpers import assert_description
from mmi2grpc._helpers import match_description
from mmi2grpc._proxy import ProfileProxy
+
from pandora_experimental.host_grpc import Host
-from pandora_experimental.host_pb2 import Connection
+from pandora_experimental.host_pb2 import Connection, ConnectabilityMode, AddressType
from pandora_experimental.l2cap_grpc import L2CAP
+
from typing import Optional
-import sys
class L2CAPProxy(ProfileProxy):
@@ -88,7 +90,10 @@
"""
Place the IUT into LE connectable mode.
"""
- self.host.SetLEConnectable()
+ self.host.StartAdvertising(
+ connectability_mode=ConnectabilityMode.CONECTABILITY_CONNECTABLE,
+ own_address_type=AddressType.PUBLIC,
+ )
# not strictly necessary, but can save time on waiting connection
tests_to_open_bluetooth_server_socket = [
"L2CAP/LE/CFC/BV-03-C",
diff --git a/android/pandora/mmi2grpc/mmi2grpc/sm.py b/android/pandora/mmi2grpc/mmi2grpc/sm.py
index 19928d9..7991770 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/sm.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/sm.py
@@ -23,6 +23,7 @@
from pandora_experimental.security_grpc import Security
from pandora_experimental.host_grpc import Host
+from pandora_experimental.host_pb2 import ConnectabilityMode, AddressType
def debug(*args, **kwargs):
@@ -89,7 +90,10 @@
"""
Action: Place the IUT in connectable mode
"""
- self.host.SetLEConnectable()
+ self.host.StartAdvertising(
+ connectability_mode=ConnectabilityMode.CONECTABILITY_CONNECTABLE,
+ own_address_type=AddressType.PUBLIC,
+ )
return "OK"
@assert_description
@@ -140,12 +144,31 @@
return "OK"
+ @assert_description
+ def MMI_IUT_ACCEPT_CONNECTION_BR_EDR(self, **kwargs):
+ """
+ Please prepare IUT into a connectable mode in BR/EDR.
+
+ Description:
+ Verify that the Implementation Under Test (IUT) can accept a connect
+ request from PTS.
+ """
+
+ return "OK"
+
+ @assert_description
+ def _mmi_2001(self, **kwargs):
+ """
+ Please verify the passKey is correct: 000000
+ """
+ return "OK"
+
def _handle_pairing_requests(self):
def task():
pairing_events = self.security.OnPairing()
for event in pairing_events:
- if event.just_works:
+ if event.just_works or event.numeric_comparison:
pairing_events.send(event=event, confirm=True)
if event.passkey_entry_request:
try:
diff --git a/android/pandora/server/configs/PtsBotTest.xml b/android/pandora/server/configs/PtsBotTest.xml
index e5850b7..42f3ec9 100644
--- a/android/pandora/server/configs/PtsBotTest.xml
+++ b/android/pandora/server/configs/PtsBotTest.xml
@@ -62,6 +62,7 @@
<option name="profile" value="SM/PER/KDU" />
<option name="profile" value="SM/PER/SCJW" />
<option name="profile" value="SM/PER/SCPK" />
+ <option name="profile" value="SM/PER/SCCT" />
<option name="profile" value="HOGP/RH" />
</test>
</configuration>
diff --git a/android/pandora/server/configs/pts_bot_tests_config.json b/android/pandora/server/configs/pts_bot_tests_config.json
index 759895e..2af2074 100644
--- a/android/pandora/server/configs/pts_bot_tests_config.json
+++ b/android/pandora/server/configs/pts_bot_tests_config.json
@@ -413,7 +413,11 @@
"SM/CEN/SCPK/BV-04-C",
"SM/PER/SCJW/BV-02-C",
"SM/PER/SCPK/BI-04-C",
- "SM/PER/SCPK/BV-03-C"
+ "SM/PER/SCPK/BV-03-C",
+ "SM/PER/SCCT/BV-04-C",
+ "SM/PER/SCCT/BV-06-C",
+ "SM/PER/SCCT/BV-08-C",
+ "SM/PER/SCCT/BV-10-C"
],
"ics": {
"TSPC_4.0HCI_1a_2": true,
diff --git a/android/pandora/server/proto/pandora_experimental/host.proto b/android/pandora/server/proto/pandora_experimental/host.proto
index 3a2fa57..d0cad8d 100644
--- a/android/pandora/server/proto/pandora_experimental/host.proto
+++ b/android/pandora/server/proto/pandora_experimental/host.proto
@@ -41,12 +41,23 @@
rpc GetLEConnection(GetLEConnectionRequest) returns (GetLEConnectionResponse);
// Disconnect ongoing LE connection.
rpc DisconnectLE(DisconnectLERequest) returns (google.protobuf.Empty);
- // Start LE advertisement
- rpc SetLEConnectable(google.protobuf.Empty) returns (google.protobuf.Empty);
+ // Create and enable an advertising set using legacy or extended advertising,
+ // except periodic advertising.
+ rpc StartAdvertising(StartAdvertisingRequest) returns (StartAdvertisingResponse);
+ // Create and enable a periodic advertising set.
+ rpc StartPeriodicAdvertising(StartPeriodicAdvertisingRequest) returns (StartPeriodicAdvertisingResponse);
+ // Remove an advertising set.
+ rpc StopAdvertising(StopAdvertisingRequest) returns (StopAdvertisingResponse);
// Run BR/EDR inquiry and returns each device found
rpc RunInquiry(RunInquiryRequest) returns (stream RunInquiryResponse);
// Run LE discovery (scanning) and return each device found
rpc RunDiscovery(RunDiscoveryRequest) returns (stream RunDiscoveryResponse);
+ // Set BREDR connectability mode
+ rpc SetConnectabilityMode(SetConnectabilityModeRequest) returns (SetConnectabilityModeResponse);
+ // Set BREDR discoverable mode
+ rpc SetDiscoverabilityMode(SetDiscoverabilityModeRequest) returns (SetDiscoverabilityModeResponse);
+ // Get device name from connection
+ rpc GetDeviceName(GetDeviceNameRequest) returns (GetDeviceNameResponse);
}
// Response of the `ReadLocalAddress` method.
@@ -58,15 +69,33 @@
// A Token representing an ACL connection.
// It's acquired via a Connect on the Host service.
message Connection {
- // Opaque value filled by the gRPC server, must not
- // be modified nor crafted.
+ // Opaque value filled by the gRPC server, must not be modified nor crafted
+ // Android specific: it's secretly an encoded InternelConnectionRef created using newConnection
bytes cookie = 1;
}
+// Internal representation of a Connection - not exposed to clients, included here
+// just for code-generation convenience
+message InternalConnectionRef {
+ bytes address = 1;
+ Transport transport = 2;
+}
+
+// WARNING: Leaving this enum empty will default to BREDR, so make sure that this is a
+// valid default whenever used, and that we always populate this value.
+enum Transport {
+ TRANSPORT_BREDR = 0;
+ TRANSPORT_LE = 1;
+}
+
// Request of the `Connect` method.
message ConnectRequest {
// Peer Bluetooth Device Address as array of 6 bytes.
bytes address = 1;
+ // Whether we want to initiate pairing as part of the connection
+ bool skip_pairing = 2;
+ // Whether confirmation prompts should be auto-accepted or handled manually
+ bool manually_confirm = 3;
}
// Response of the `Connect` method.
@@ -146,6 +175,74 @@
Connection connection = 1;
}
+message AdvertisingHandle {
+ bytes cookie = 1;
+}
+
+enum AddressType {
+ PUBLIC = 0x00;
+ RANDOM = 0x01;
+}
+
+// Advertising Data including one or multiple AD types.
+// Since the Flags AD type is mandatory, it must be automatically set by the
+// IUT.
+// include_<AD type> fields are used for AD type which are generally not
+// exposed and that must be set by the IUT when specified.
+// See Core Supplement, Part A, Data Types for details
+message AdvertisingData {
+ repeated string service_uuids = 1;
+ bool include_local_name = 2;
+ bytes manufacturer_specific_data = 3;
+ bool include_tx_power_level = 4;
+ bool include_peripheral_connection_interval_range = 5;
+ repeated string service_solicitation = 6;
+ map<string, bytes> service_data = 7;
+ // Must be on 16 bits.
+ uint32 appearance = 8;
+ repeated bytes public_target_addresses = 9;
+ repeated bytes random_target_addresses = 10;
+ bool include_advertising_interval = 11;
+ bool include_le_address = 12;
+ bool include_le_role = 13;
+ string uri = 14;
+}
+
+message StartAdvertisingRequest {
+ bool legacy = 1;
+ DiscoverabilityMode discovery_mode = 2;
+ ConnectabilityMode connectability_mode = 3;
+ AddressType own_address_type = 4;
+ // If none, undirected.
+ bytes peer_address = 5;
+ AdvertisingData advertising_data = 6;
+ // If none, not scannable.
+ AdvertisingData scan_response_data = 7;
+}
+
+message StartAdvertisingResponse {
+ AdvertisingHandle handle = 1;
+}
+
+message StartPeriodicAdvertisingRequest {
+ AddressType own_address_type = 1;
+ // If none, undirected.
+ bytes peer_address = 2;
+ uint32 interval_min = 3;
+ uint32 interval_max = 4;
+ AdvertisingData advertising_data = 5;
+}
+
+message StartPeriodicAdvertisingResponse {
+ AdvertisingHandle handle = 1;
+}
+
+message StopAdvertisingRequest {
+ AdvertisingHandle handle = 1;
+}
+
+message StopAdvertisingResponse {}
+
message RunInquiryRequest {
}
@@ -165,3 +262,38 @@
string name = 1;
bytes address = 2;
}
+
+// 5.3 Vol 3C 4.1 Discoverability Modes
+enum DiscoverabilityMode {
+ DISCOVERABILITY_UNSPECIFIED = 0;
+ DISCOVERABILITY_NONE = 1;
+ DISCOVERABILITY_LIMITED = 2;
+ DISCOVERABILITY_GENERAL = 3;
+}
+
+// 5.3 Vol 3C 4.2 Connectability Modes
+enum ConnectabilityMode {
+ CONNECTABILITY_UNSPECIFIED = 0;
+ CONNECTABILITY_NOT_CONNECTABLE = 1;
+ CONECTABILITY_CONNECTABLE = 2;
+}
+
+message SetConnectabilityModeRequest {
+ ConnectabilityMode connectability = 1;
+}
+
+message SetConnectabilityModeResponse {}
+
+message SetDiscoverabilityModeRequest {
+ DiscoverabilityMode discoverability = 1;
+}
+
+message SetDiscoverabilityModeResponse {}
+
+message GetDeviceNameRequest {
+ Connection connection = 1;
+}
+
+message GetDeviceNameResponse {
+ string name = 1;
+}
diff --git a/android/pandora/server/src/com/android/pandora/Gatt.kt b/android/pandora/server/src/com/android/pandora/Gatt.kt
index 6db84b8..122c634 100644
--- a/android/pandora/server/src/com/android/pandora/Gatt.kt
+++ b/android/pandora/server/src/com/android/pandora/Gatt.kt
@@ -74,7 +74,7 @@
grpcUnary<ExchangeMTUResponse>(mScope, responseObserver) {
val mtu = request.mtu
Log.i(TAG, "exchangeMTU MTU=$mtu")
- if (!GattInstance.get(request.connection.cookie).mGatt.requestMtu(mtu)) {
+ if (!GattInstance.get(request.connection.address).mGatt.requestMtu(mtu)) {
Log.e(TAG, "Error on requesting MTU $mtu")
throw Status.UNKNOWN.asException()
}
@@ -88,7 +88,7 @@
) {
grpcUnary<WriteResponse>(mScope, responseObserver) {
Log.i(TAG, "writeAttFromHandle handle=${request.handle}")
- val gattInstance = GattInstance.get(request.connection.cookie)
+ val gattInstance = GattInstance.get(request.connection.address)
var characteristic: BluetoothGattCharacteristic? =
getCharacteristicWithHandle(request.handle, gattInstance)
if (characteristic == null) {
@@ -113,7 +113,7 @@
responseObserver: StreamObserver<DiscoverServicesResponse>
) {
grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) {
- val gattInstance = GattInstance.get(request.connection.cookie)
+ val gattInstance = GattInstance.get(request.connection.address)
Log.i(TAG, "discoverServiceByUuid uuid=${request.uuid}")
// In some cases, GATT starts a discovery immediately after being connected, so
// we need to wait until the service discovery is finished to be able to discover again.
@@ -133,7 +133,7 @@
) {
grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) {
Log.i(TAG, "discoverServices")
- val gattInstance = GattInstance.get(request.connection.cookie)
+ val gattInstance = GattInstance.get(request.connection.address)
check(gattInstance.mGatt.discoverServices())
gattInstance.waitForDiscoveryEnd()
DiscoverServicesResponse.newBuilder()
@@ -168,7 +168,7 @@
) {
grpcUnary<ClearCacheResponse>(mScope, responseObserver) {
Log.i(TAG, "clearCache")
- val gattInstance = GattInstance.get(request.connection.cookie)
+ val gattInstance = GattInstance.get(request.connection.address)
check(gattInstance.mGatt.refresh())
ClearCacheResponse.newBuilder().build()
}
@@ -180,7 +180,7 @@
) {
grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) {
Log.i(TAG, "readCharacteristicFromHandle handle=${request.handle}")
- val gattInstance = GattInstance.get(request.connection.cookie)
+ val gattInstance = GattInstance.get(request.connection.address)
val characteristic: BluetoothGattCharacteristic? =
getCharacteristicWithHandle(request.handle, gattInstance)
checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." }
@@ -198,7 +198,7 @@
) {
grpcUnary<ReadCharacteristicsFromUuidResponse>(mScope, responseObserver) {
Log.i(TAG, "readCharacteristicsFromUuid uuid=${request.uuid}")
- val gattInstance = GattInstance.get(request.connection.cookie)
+ val gattInstance = GattInstance.get(request.connection.address)
tryDiscoverServices(gattInstance)
val readValues =
gattInstance.readCharacteristicUuidBlocking(
@@ -215,7 +215,7 @@
) {
grpcUnary<ReadCharacteristicDescriptorResponse>(mScope, responseObserver) {
Log.i(TAG, "readCharacteristicDescriptorFromHandle handle=${request.handle}")
- val gattInstance = GattInstance.get(request.connection.cookie)
+ val gattInstance = GattInstance.get(request.connection.address)
val descriptor: BluetoothGattDescriptor? =
getDescriptorWithHandle(request.handle, gattInstance)
checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." }
diff --git a/android/pandora/server/src/com/android/pandora/Host.kt b/android/pandora/server/src/com/android/pandora/Host.kt
index 95f6708..ad64c12 100644
--- a/android/pandora/server/src/com/android/pandora/Host.kt
+++ b/android/pandora/server/src/com/android/pandora/Host.kt
@@ -17,29 +17,34 @@
package com.android.pandora
import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothAssignedNumbers
import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothDevice.ADDRESS_TYPE_PUBLIC
+import android.bluetooth.BluetoothDevice.ADDRESS_TYPE_RANDOM
import android.bluetooth.BluetoothDevice.BOND_BONDED
+import android.bluetooth.BluetoothDevice.TRANSPORT_AUTO
+import android.bluetooth.BluetoothDevice.TRANSPORT_BREDR
import android.bluetooth.BluetoothDevice.TRANSPORT_LE
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
-import android.bluetooth.le.AdvertisingSetParameters
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.MacAddress
+import android.os.ParcelUuid
import android.util.Log
import com.google.protobuf.ByteString
import com.google.protobuf.Empty
import io.grpc.Status
import io.grpc.stub.StreamObserver
-import kotlin.Result.Companion.failure
-import kotlin.Result.Companion.success
-import kotlin.coroutines.suspendCoroutine
+import java.io.IOException
+import java.time.Duration
+import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
@@ -57,19 +62,25 @@
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
import pandora.HostGrpc.HostImplBase
import pandora.HostProto.*
@kotlinx.coroutines.ExperimentalCoroutinesApi
class Host(private val context: Context, private val server: Server) : HostImplBase() {
private val TAG = "PandoraHost"
- private val ADVERTISEMENT_DURATION_MILLIS: Int = 10000
+
private val scope: CoroutineScope
private val flow: Flow<Intent>
private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
private val bluetoothAdapter = bluetoothManager.adapter
+ private var connectability = ConnectabilityMode.CONNECTABILITY_UNSPECIFIED
+ private var discoverability = DiscoverabilityMode.DISCOVERABILITY_UNSPECIFIED
+
+ private val advertisers = mutableMapOf<UUID, AdvertiseCallback>()
+
init {
scope = CoroutineScope(Dispatchers.Default)
@@ -79,6 +90,8 @@
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
+ intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
+ intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
intentFilter.addAction(BluetoothDevice.ACTION_FOUND)
// Creates a shared flow of intents that can be used in all methods in the coroutine scope.
@@ -219,81 +232,59 @@
acceptPairingAndAwaitBonded(bluetoothDevice)
WaitConnectionResponse.newBuilder()
- .setConnection(
- Connection.newBuilder()
- .setCookie(ByteString.copyFromUtf8(bluetoothDevice.address))
- .build()
- )
+ .setConnection(newConnection(bluetoothDevice, Transport.TRANSPORT_BREDR))
.build()
}
}
- /**
- * Set the device in advertisement mode for #ADVERTISEMENT_DURATION_MILLIS milliseconds.
- * @param request Request sent by the client.
- * @param responseObserver Response to build and set back to the client.
- */
- override fun setLEConnectable(
- request: Empty,
- responseObserver: StreamObserver<Empty>,
- ) {
- // Creates a gRPC coroutine in a given coroutine scope which executes a given suspended function
- // returning a gRPC response and sends it on a given gRPC stream observer.
- grpcUnary<Empty>(scope, responseObserver) {
- Log.i(TAG, "setLEConnectable")
- val advertiser = bluetoothAdapter.getBluetoothLeAdvertiser()
- val advertiseSettings =
- AdvertiseSettings.Builder()
- .setConnectable(true)
- .setOwnAddressType(AdvertisingSetParameters.ADDRESS_TYPE_PUBLIC)
- .setTimeout(ADVERTISEMENT_DURATION_MILLIS)
- .build()
- val advertiseData = AdvertiseData.Builder().build()
- suspendCoroutine<Boolean> { continuation ->
- val advertiseCallback =
- object : AdvertiseCallback() {
- override fun onStartFailure(errorCode: Int) {
- Log.i(TAG, "Advertising failed: $errorCode")
- continuation.resumeWith(failure(Exception("Advertising failed: $errorCode")))
- }
-
- override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
- Log.i(TAG, "Advertising success")
- continuation.resumeWith(success(true))
- }
- }
- advertiser.startAdvertising(advertiseSettings, advertiseData, advertiseCallback)
- }
-
- // Response sent to client
- Empty.getDefaultInstance()
- }
- }
-
override fun connect(request: ConnectRequest, responseObserver: StreamObserver<ConnectResponse>) {
grpcUnary(scope, responseObserver) {
val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
Log.i(TAG, "connect: address=$bluetoothDevice")
+ bluetoothAdapter.cancelDiscovery()
+
if (!bluetoothDevice.isConnected()) {
- if (bluetoothDevice.bondState == BOND_BONDED) {
- // already bonded, just reconnect
- bluetoothDevice.connect()
- waitConnectionIntent(bluetoothDevice)
+ if (request.skipPairing) {
+ // do an SDP request to trigger a temporary BREDR connection
+ try {
+ withTimeout(1500) { bluetoothDevice.createRfcommSocket(3).connect() }
+ } catch (e: IOException) {
+ // ignore
+ }
} else {
- // need to bond
- bluetoothDevice.createBond()
- acceptPairingAndAwaitBonded(bluetoothDevice)
+ if (bluetoothDevice.bondState == BOND_BONDED) {
+ // already bonded, just reconnect
+ bluetoothDevice.connect()
+ waitConnectionIntent(bluetoothDevice)
+ } else {
+ // need to bond
+ bluetoothDevice.createBond()
+ if (!request.manuallyConfirm) {
+ acceptPairingAndAwaitBonded(bluetoothDevice)
+ }
+ }
}
}
ConnectResponse.newBuilder()
- .setConnection(
- Connection.newBuilder()
- .setCookie(ByteString.copyFromUtf8(bluetoothDevice.address))
- .build()
- )
+ .setConnection(newConnection(bluetoothDevice, Transport.TRANSPORT_BREDR))
+ .build()
+ }
+ }
+
+ override fun getConnection(
+ request: GetConnectionRequest,
+ responseObserver: StreamObserver<GetConnectionResponse>
+ ) {
+ grpcUnary(scope, responseObserver) {
+ val device = bluetoothAdapter.getRemoteDevice(request.address.toByteArray())
+ check(
+ device.isConnected() && device.type != BluetoothDevice.DEVICE_TYPE_LE
+ ) // either classic or dual
+ GetConnectionResponse.newBuilder()
+ .setConnection(newConnection(device, Transport.TRANSPORT_BREDR))
.build()
}
}
@@ -319,6 +310,7 @@
bluetoothDevice.disconnect()
connectionStateChangedFlow.filter { it == BluetoothAdapter.STATE_DISCONNECTED }.first()
+
DisconnectResponse.getDefaultInstance()
}
}
@@ -329,13 +321,17 @@
) {
grpcUnary<ConnectLEResponse>(scope, responseObserver) {
val address = request.address.decodeAsMacAddressToString()
- Log.i(TAG, "connectLE: $address")
- val device = scanLeDevice(address)
- GattInstance(device!!, TRANSPORT_LE, context).waitForState(BluetoothProfile.STATE_CONNECTED)
+ Log.i(TAG, "connect LE: $address")
+ val device = scanLeDevice(address)!!
+ GattInstance(device, TRANSPORT_LE, context)
+
+ flow
+ .filter { it.action == BluetoothDevice.ACTION_ACL_CONNECTED }
+ .filter { it.getBluetoothDeviceExtra() == device }
+ .first()
+
ConnectLEResponse.newBuilder()
- .setConnection(
- Connection.newBuilder().setCookie(ByteString.copyFromUtf8(device.address)).build()
- )
+ .setConnection(newConnection(device, Transport.TRANSPORT_LE))
.build()
}
}
@@ -347,13 +343,10 @@
grpcUnary<GetLEConnectionResponse>(scope, responseObserver) {
val address = request.address.decodeAsMacAddressToString()
Log.i(TAG, "getLEConnection: $address")
- val device =
- bluetoothAdapter.getRemoteLeDevice(address, BluetoothDevice.ADDRESS_TYPE_PUBLIC)
+ val device = bluetoothAdapter.getRemoteLeDevice(address, BluetoothDevice.ADDRESS_TYPE_PUBLIC)
if (device.isConnected) {
GetLEConnectionResponse.newBuilder()
- .setConnection(
- Connection.newBuilder().setCookie(ByteString.copyFromUtf8(device.address)).build()
- )
+ .setConnection(newConnection(device, Transport.TRANSPORT_LE))
.build()
} else {
Log.e(TAG, "Device: $device is not connected")
@@ -364,7 +357,7 @@
override fun disconnectLE(request: DisconnectLERequest, responseObserver: StreamObserver<Empty>) {
grpcUnary<Empty>(scope, responseObserver) {
- val address = request.connection.cookie.toByteArray().decodeToString()
+ val address = request.connection.address
Log.i(TAG, "disconnectLE: $address")
val gattInstance = GattInstance.get(address)
@@ -409,6 +402,72 @@
return bluetoothDevice
}
+ override fun startAdvertising(
+ request: StartAdvertisingRequest,
+ responseObserver: StreamObserver<StartAdvertisingResponse>
+ ) {
+ Log.d(TAG, "startAdvertising")
+ grpcUnary(scope, responseObserver) {
+ val handle = UUID.randomUUID()
+
+ callbackFlow {
+ val callback =
+ object : AdvertiseCallback() {
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
+ sendBlocking(
+ StartAdvertisingResponse.newBuilder()
+ .setHandle(
+ AdvertisingHandle.newBuilder()
+ .setCookie(ByteString.copyFromUtf8(handle.toString()))
+ )
+ .build()
+ )
+ }
+ override fun onStartFailure(errorCode: Int) {
+ error("failed to start advertising")
+ }
+ }
+
+ advertisers[handle] = callback
+
+ val advertisingDataBuilder = AdvertiseData.Builder()
+
+ for (service_uuid in request.advertisingData.serviceUuidsList) {
+ advertisingDataBuilder.addServiceUuid(ParcelUuid.fromString(service_uuid))
+ }
+
+ advertisingDataBuilder
+ .setIncludeDeviceName(request.advertisingData.includeLocalName)
+ .setIncludeTxPowerLevel(request.advertisingData.includeTxPowerLevel)
+ .addManufacturerData(
+ BluetoothAssignedNumbers.GOOGLE,
+ request.advertisingData.manufacturerSpecificData.toByteArray()
+ )
+
+ bluetoothAdapter.bluetoothLeAdvertiser.startAdvertising(
+ AdvertiseSettings.Builder()
+ .setConnectable(
+ request.connectabilityMode == ConnectabilityMode.CONECTABILITY_CONNECTABLE
+ )
+ .setOwnAddressType(
+ when (request.ownAddressType!!) {
+ AddressType.PUBLIC -> ADDRESS_TYPE_PUBLIC
+ AddressType.RANDOM -> ADDRESS_TYPE_RANDOM
+ AddressType.UNRECOGNIZED ->
+ error("unrecognized address type ${request.ownAddressType}")
+ }
+ )
+ .build(),
+ advertisingDataBuilder.build(),
+ callback,
+ )
+
+ awaitClose { /* no-op */}
+ }
+ .first()
+ }
+ }
+
override fun runInquiry(
request: RunInquiryRequest,
responseObserver: StreamObserver<RunInquiryResponse>
@@ -432,15 +491,83 @@
.addDevice(
Device.newBuilder()
.setName(device.name)
- .setAddress(
- ByteString.copyFrom(MacAddress.fromString(device.address).toByteArray())
- )
+ .setAddress(device.toByteString())
)
.build()
}
}
}
+ override fun setConnectabilityMode(
+ request: SetConnectabilityModeRequest,
+ responseObserver: StreamObserver<SetConnectabilityModeResponse>
+ ) {
+ grpcUnary(scope, responseObserver) {
+ Log.d(TAG, "setConnectabilityMode")
+ connectability = request.connectability!!
+
+ val scanMode =
+ when (connectability) {
+ ConnectabilityMode.CONNECTABILITY_UNSPECIFIED,
+ ConnectabilityMode.UNRECOGNIZED -> null
+ ConnectabilityMode.CONNECTABILITY_NOT_CONNECTABLE -> {
+ BluetoothAdapter.SCAN_MODE_NONE
+ }
+ ConnectabilityMode.CONECTABILITY_CONNECTABLE -> {
+ if (
+ discoverability == DiscoverabilityMode.DISCOVERABILITY_LIMITED ||
+ discoverability == DiscoverabilityMode.DISCOVERABILITY_GENERAL
+ ) {
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE
+ } else {
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE
+ }
+ }
+ }
+
+ if (scanMode != null) {
+ bluetoothAdapter.setScanMode(scanMode)
+ }
+ SetConnectabilityModeResponse.getDefaultInstance()
+ }
+ }
+
+ override fun setDiscoverabilityMode(
+ request: SetDiscoverabilityModeRequest,
+ responseObserver: StreamObserver<SetDiscoverabilityModeResponse>
+ ) {
+ Log.d(TAG, "setDiscoverabilityMode")
+ grpcUnary(scope, responseObserver) {
+ discoverability = request.discoverability!!
+
+ val scanMode =
+ when (discoverability) {
+ DiscoverabilityMode.DISCOVERABILITY_UNSPECIFIED,
+ DiscoverabilityMode.UNRECOGNIZED -> null
+ DiscoverabilityMode.DISCOVERABILITY_NONE ->
+ if (connectability == ConnectabilityMode.CONECTABILITY_CONNECTABLE) {
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE
+ } else {
+ BluetoothAdapter.SCAN_MODE_NONE
+ }
+ DiscoverabilityMode.DISCOVERABILITY_LIMITED,
+ DiscoverabilityMode.DISCOVERABILITY_GENERAL ->
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE
+ }
+
+ if (scanMode != null) {
+ bluetoothAdapter.setScanMode(scanMode)
+ }
+
+ if (request.discoverability == DiscoverabilityMode.DISCOVERABILITY_LIMITED) {
+ bluetoothAdapter.setDiscoverableTimeout(
+ Duration.ofSeconds(120)
+ ) // limited discoverability needs a timeout, 120s is Android default
+ }
+ SetDiscoverabilityModeResponse.getDefaultInstance()
+ }
+ }
+
override fun runDiscovery(
request: RunDiscoveryRequest,
responseObserver: StreamObserver<RunDiscoveryResponse>
@@ -477,4 +604,14 @@
}
}
}
+
+ override fun getDeviceName(
+ request: GetDeviceNameRequest,
+ responseObserver: StreamObserver<GetDeviceNameResponse>
+ ) {
+ grpcUnary(scope, responseObserver) {
+ val device = request.connection.toBluetoothDevice(bluetoothAdapter)
+ GetDeviceNameResponse.newBuilder().setName(device.name).build()
+ }
+ }
}
diff --git a/android/pandora/server/src/com/android/pandora/Security.kt b/android/pandora/server/src/com/android/pandora/Security.kt
index e7ded29..01247f0 100644
--- a/android/pandora/server/src/com/android/pandora/Security.kt
+++ b/android/pandora/server/src/com/android/pandora/Security.kt
@@ -18,9 +18,15 @@
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothDevice.ACTION_PAIRING_REQUEST
+import android.bluetooth.BluetoothDevice.BOND_BONDED
import android.bluetooth.BluetoothDevice.DEVICE_TYPE_CLASSIC
+import android.bluetooth.BluetoothDevice.DEVICE_TYPE_DUAL
import android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE
import android.bluetooth.BluetoothDevice.EXTRA_PAIRING_VARIANT
+import android.bluetooth.BluetoothDevice.TRANSPORT_AUTO
+import android.bluetooth.BluetoothDevice.TRANSPORT_BREDR
+import android.bluetooth.BluetoothDevice.TRANSPORT_LE
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
@@ -31,6 +37,7 @@
import io.grpc.stub.StreamObserver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@@ -57,6 +64,7 @@
init {
val intentFilter = IntentFilter()
intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
+ intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
flow = intentFlow(context, intentFilter).shareIn(globalScope, SharingStarted.Eagerly)
}
@@ -68,8 +76,17 @@
override fun pair(request: PairRequest, responseObserver: StreamObserver<Empty>) {
grpcUnary(globalScope, responseObserver) {
val bluetoothDevice = request.connection.toBluetoothDevice(bluetoothAdapter)
- Log.i(TAG, "pair: ${bluetoothDevice.address}")
- bluetoothDevice.createBond()
+ Log.i(
+ TAG,
+ "pair: ${bluetoothDevice.address} (current bond state: ${bluetoothDevice.bondState})"
+ )
+ bluetoothDevice.createBond(
+ when (request.connection.transport!!) {
+ Transport.TRANSPORT_LE -> TRANSPORT_LE
+ Transport.TRANSPORT_BREDR -> TRANSPORT_BREDR
+ Transport.UNRECOGNIZED -> TRANSPORT_AUTO
+ }
+ )
Empty.getDefaultInstance()
}
}
@@ -78,26 +95,28 @@
request: DeletePairingRequest,
responseObserver: StreamObserver<DeletePairingResponse>
) {
- grpcUnary<DeletePairingResponse>(globalScope, responseObserver) {
+ grpcUnary(globalScope, responseObserver) {
val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
Log.i(TAG, "DeletePairing: device=$bluetoothDevice")
+ val unbonded =
+ globalScope.async {
+ flow
+ .filter { it.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED }
+ .filter { it.getBluetoothDeviceExtra() == bluetoothDevice }
+ .filter {
+ it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) ==
+ BluetoothDevice.BOND_NONE
+ }
+ .first()
+ }
+
if (bluetoothDevice.removeBond()) {
Log.i(TAG, "DeletePairing: device=$bluetoothDevice - wait BOND_NONE intent")
- flow
- .filter { it.getAction() == BluetoothDevice.ACTION_BOND_STATE_CHANGED }
- .filter { it.getBluetoothDeviceExtra() == bluetoothDevice }
- .filter {
- it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) ==
- BluetoothDevice.BOND_NONE
- }
- .filter {
- it.getIntExtra(BluetoothDevice.EXTRA_REASON, BluetoothAdapter.ERROR) ==
- BluetoothDevice.BOND_SUCCESS
- }
- .first()
+ unbonded.await()
} else {
Log.i(TAG, "DeletePairing: device=$bluetoothDevice - Already unpaired")
+ unbonded.cancel()
}
DeletePairingResponse.getDefaultInstance()
}
@@ -125,63 +144,62 @@
}
.launchIn(this)
- flow.map { intent ->
- val device = intent.getBluetoothDeviceExtra()
- val variant = intent.getIntExtra(EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR)
- Log.i(
- TAG,
- "OnPairing: Handling PairingEvent ${variant} for device ${device.address}"
- )
- val eventBuilder =
- PairingEvent.newBuilder().setAddress(ByteString.copyFrom(device.toByteArray()))
- when (variant) {
- // SSP / LE Just Works
- BluetoothDevice.PAIRING_VARIANT_CONSENT ->
- eventBuilder.justWorks = Empty.getDefaultInstance()
+ flow
+ .filter { intent -> intent.action == ACTION_PAIRING_REQUEST }
+ .map { intent ->
+ val device = intent.getBluetoothDeviceExtra()
+ val variant = intent.getIntExtra(EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR)
+ Log.i(TAG, "OnPairing: Handling PairingEvent ${variant} for device ${device.address}")
+ val eventBuilder =
+ PairingEvent.newBuilder().setAddress(device.toByteString())
+ when (variant) {
+ // SSP / LE Just Works
+ BluetoothDevice.PAIRING_VARIANT_CONSENT ->
+ eventBuilder.justWorks = Empty.getDefaultInstance()
- // SSP / LE Numeric Comparison
- BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION ->
- eventBuilder.numericComparison =
- intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
- BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY -> {
- val passkey =
- intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
- eventBuilder.passkeyEntryNotification = passkey
- }
-
- // Out-Of-Band not currently supported
- BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT ->
- error("Received OOB pairing confirmation (UNSUPPORTED)")
-
- // Legacy PIN entry, or LE legacy passkey entry, depending on transport
- BluetoothDevice.PAIRING_VARIANT_PIN ->
- when (device.type) {
- DEVICE_TYPE_CLASSIC -> eventBuilder.pinCodeRequest = Empty.getDefaultInstance()
- DEVICE_TYPE_LE ->
- eventBuilder.passkeyEntryRequest = Empty.getDefaultInstance()
- else -> error("cannot determine pairing variant, since transport is unknown")
+ // SSP / LE Numeric Comparison
+ BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION ->
+ eventBuilder.numericComparison =
+ intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
+ BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY -> {
+ val passkey =
+ intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
+ eventBuilder.passkeyEntryNotification = passkey
}
- BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS ->
- eventBuilder.pinCodeRequest = Empty.getDefaultInstance()
- // Legacy PIN entry or LE legacy passkey entry, except we just generate the PIN in the
- // stack and display it to the user for convenience
- BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN -> {
- val passkey =
- intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
- when (device.type) {
- DEVICE_TYPE_CLASSIC ->
- eventBuilder.pinCodeNotification =
- ByteString.copyFrom(passkey.toString().toByteArray())
- DEVICE_TYPE_LE -> eventBuilder.passkeyEntryNotification = passkey
- else -> error("cannot determine pairing variant, since transport is unknown")
+ // Out-Of-Band not currently supported
+ BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT ->
+ error("Received OOB pairing confirmation (UNSUPPORTED)")
+
+ // Legacy PIN entry, or LE legacy passkey entry, depending on transport
+ BluetoothDevice.PAIRING_VARIANT_PIN ->
+ when (device.type) {
+ DEVICE_TYPE_CLASSIC -> eventBuilder.pinCodeRequest = Empty.getDefaultInstance()
+ DEVICE_TYPE_LE -> eventBuilder.passkeyEntryRequest = Empty.getDefaultInstance()
+ else -> error("cannot determine pairing variant, since transport is unknown")
+ }
+ BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS ->
+ eventBuilder.pinCodeRequest = Empty.getDefaultInstance()
+
+ // Legacy PIN entry or LE legacy passkey entry, except we just generate the PIN in
+ // the
+ // stack and display it to the user for convenience
+ BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN -> {
+ val passkey =
+ intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
+ when (device.type) {
+ DEVICE_TYPE_CLASSIC ->
+ eventBuilder.pinCodeNotification =
+ ByteString.copyFrom(passkey.toString().toByteArray())
+ DEVICE_TYPE_LE -> eventBuilder.passkeyEntryNotification = passkey
+ else -> error("cannot determine pairing variant, since transport is unknown")
+ }
+ }
+ else -> {
+ error("Received unknown pairing variant $variant")
}
}
- else -> {
- error("Received unknown pairing variant $variant")
- }
+ eventBuilder.build()
}
- eventBuilder.build()
- }
}
}
diff --git a/android/pandora/server/src/com/android/pandora/Utils.kt b/android/pandora/server/src/com/android/pandora/Utils.kt
index d2d27db..a59deb0 100644
--- a/android/pandora/server/src/com/android/pandora/Utils.kt
+++ b/android/pandora/server/src/com/android/pandora/Utils.kt
@@ -53,6 +53,8 @@
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import pandora.HostProto.Connection
+import pandora.HostProto.InternalConnectionRef
+import pandora.HostProto.Transport
fun shell(cmd: String): String {
val fd = InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd)
@@ -284,7 +286,7 @@
}
fun Intent.getBluetoothDeviceExtra(): BluetoothDevice =
- this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
+ this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)!!
fun ByteString.decodeAsMacAddressToString(): String =
MacAddress.fromBytes(this.toByteArray()).toString().uppercase()
@@ -293,6 +295,24 @@
adapter.getRemoteDevice(this.decodeAsMacAddressToString())
fun Connection.toBluetoothDevice(adapter: BluetoothAdapter): BluetoothDevice =
- adapter.getRemoteDevice(this.cookie.toByteArray().decodeToString())
+ adapter.getRemoteDevice(address)
-fun BluetoothDevice.toByteArray(): ByteArray = MacAddress.fromString(this.address).toByteArray()
+val Connection.address: String
+ get() = InternalConnectionRef.parseFrom(this.cookie).address.decodeAsMacAddressToString()
+
+val Connection.transport: Transport
+ get() = InternalConnectionRef.parseFrom(this.cookie).transport
+
+fun newConnection(device: BluetoothDevice, transport: Transport) =
+ Connection.newBuilder()
+ .setCookie(
+ InternalConnectionRef.newBuilder()
+ .setAddress(device.toByteString())
+ .setTransport(transport)
+ .build()
+ .toByteString()
+ )
+ .build()!!
+
+fun BluetoothDevice.toByteString() =
+ ByteString.copyFrom(MacAddress.fromString(this.address).toByteArray())!!
diff --git a/system/bta/gatt/bta_gattc_act.cc b/system/bta/gatt/bta_gattc_act.cc
index 9de15c9..ecb9a87 100644
--- a/system/bta/gatt/bta_gattc_act.cc
+++ b/system/bta/gatt/bta_gattc_act.cc
@@ -768,6 +768,26 @@
p_clcb->p_srcb->update_count = 0;
p_clcb->p_srcb->state = BTA_GATTC_SERV_DISC_ACT;
+ /* This is workaround for the embedded devices being already on the market
+ * and having a serious problem with handle Read By Type with
+ * GATT_UUID_DATABASE_HASH. With this workaround, Android will assume that
+ * embedded device having LMP version lower than 5.1 (0x0a), it does not
+ * support GATT Caching.
+ */
+ uint8_t lmp_version = 0;
+ if (!BTM_ReadRemoteVersion(p_clcb->bda, &lmp_version, nullptr, nullptr)) {
+ LOG_WARN("Could not read remote version for %s",
+ p_clcb->bda.ToString().c_str());
+ }
+
+ if (lmp_version < 0x0a) {
+ LOG_WARN(
+ " Device LMP version 0x%02x < Bluetooth 5.1. Ignore database cache "
+ "read.",
+ lmp_version);
+ p_clcb->p_srcb->srvc_hdl_db_hash = false;
+ }
+
/* read db hash if db hash characteristic exists */
if (bta_gattc_is_robust_caching_enabled() &&
p_clcb->p_srcb->srvc_hdl_db_hash &&
diff --git a/system/bta/hf_client/bta_hf_client_rfc.cc b/system/bta/hf_client/bta_hf_client_rfc.cc
index d964003b..2999eb9 100644
--- a/system/bta/hf_client/bta_hf_client_rfc.cc
+++ b/system/bta/hf_client/bta_hf_client_rfc.cc
@@ -116,6 +116,7 @@
if (client_cb == NULL) {
APPL_TRACE_ERROR("%s: error allocating a new handle", __func__);
p_buf->hdr.event = BTA_HF_CLIENT_RFC_CLOSE_EVT;
+ RFCOMM_RemoveConnection(port_handle);
} else {
// Set the connection fields for this new CB
client_cb->conn_handle = port_handle;
diff --git a/system/bta/vc/vc.cc b/system/bta/vc/vc.cc
index 403b93d..1d406a9 100644
--- a/system/bta/vc/vc.cc
+++ b/system/bta/vc/vc.cc
@@ -743,13 +743,23 @@
int group_id, bool is_autonomous,
uint8_t opcode,
std::vector<uint8_t>& arguments) {
- DLOG(INFO) << __func__ << " num of devices: " << devices.size()
- << " group_id: " << group_id
- << " is_autonomous: " << is_autonomous << " opcode: " << +opcode
- << " arg size: " << arguments.size();
+ LOG_DEBUG(
+ "num of devices: %zu, group_id: %d, is_autonomous: %s opcode: %d, arg "
+ "size: %zu",
+ devices.size(), group_id, is_autonomous ? "true" : "false", +opcode,
+ arguments.size());
- ongoing_operations_.emplace_back(latest_operation_id_++, group_id,
- is_autonomous, opcode, arguments, devices);
+ if (std::find_if(ongoing_operations_.begin(), ongoing_operations_.end(),
+ [opcode, &arguments](const VolumeOperation& op) {
+ return (op.opcode_ == opcode) &&
+ std::equal(op.arguments_.begin(),
+ op.arguments_.end(),
+ arguments.begin());
+ }) == ongoing_operations_.end()) {
+ ongoing_operations_.emplace_back(latest_operation_id_++, group_id,
+ is_autonomous, opcode, arguments,
+ devices);
+ }
}
void MuteUnmute(std::variant<RawAddress, int> addr_or_group_id, bool mute) {
@@ -760,11 +770,13 @@
if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
LOG_DEBUG("Address: %s: ",
(std::get<RawAddress>(addr_or_group_id)).ToString().c_str());
- std::vector<RawAddress> devices = {
- std::get<RawAddress>(addr_or_group_id)};
-
- PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown,
- false, opcode, arg);
+ VolumeControlDevice* dev = volume_control_devices_.FindByAddress(
+ std::get<RawAddress>(addr_or_group_id));
+ if (dev && dev->IsConnected()) {
+ std::vector<RawAddress> devices = {dev->address};
+ PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown,
+ false, opcode, arg);
+ }
} else {
/* Handle group change */
auto group_id = std::get<int>(addr_or_group_id);
@@ -815,14 +827,17 @@
uint8_t opcode = kControlPointOpcodeSetAbsoluteVolume;
if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
- DLOG(INFO) << __func__ << " " << std::get<RawAddress>(addr_or_group_id);
- std::vector<RawAddress> devices = {
- std::get<RawAddress>(addr_or_group_id)};
-
- RemovePendingVolumeControlOperations(devices,
- bluetooth::groups::kGroupUnknown);
- PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown,
- false, opcode, arg);
+ LOG_DEBUG("Address: %s: ",
+ std::get<RawAddress>(addr_or_group_id).ToString().c_str());
+ VolumeControlDevice* dev = volume_control_devices_.FindByAddress(
+ std::get<RawAddress>(addr_or_group_id));
+ if (dev && dev->IsConnected() && (dev->volume != volume)) {
+ std::vector<RawAddress> devices = {dev->address};
+ RemovePendingVolumeControlOperations(devices,
+ bluetooth::groups::kGroupUnknown);
+ PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown,
+ false, opcode, arg);
+ }
} else {
/* Handle group change */
auto group_id = std::get<int>(addr_or_group_id);
diff --git a/system/bta/vc/vc_test.cc b/system/bta/vc/vc_test.cc
index bdc93e1..71fca36 100644
--- a/system/bta/vc/vc_test.cc
+++ b/system/bta/vc/vc_test.cc
@@ -915,10 +915,37 @@
};
TEST_F(VolumeControlValueSetTest, test_set_volume) {
- std::vector<uint8_t> expected_data({0x04, 0x00, 0x10});
- EXPECT_CALL(gatt_queue, WriteCharacteristic(conn_id, 0x0024, expected_data,
- GATT_WRITE, _, _));
+ ON_CALL(gatt_queue, WriteCharacteristic(conn_id, 0x0024, _, GATT_WRITE, _, _))
+ .WillByDefault([this](uint16_t conn_id, uint16_t handle,
+ std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb,
+ void* cb_data) {
+ std::vector<uint8_t> ntf_value({
+ value[2], // volume level
+ 0, // muted
+ static_cast<uint8_t>(value[1] + 1), // change counter
+ });
+ GetNotificationEvent(0x0021, ntf_value);
+ });
+
+ const std::vector<uint8_t> vol_x10({0x04, 0x00, 0x10});
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(conn_id, 0x0024, vol_x10, GATT_WRITE, _, _))
+ .Times(1);
VolumeControl::Get()->SetVolume(test_address, 0x10);
+
+ // Same volume level should not be applied twice
+ const std::vector<uint8_t> vol_x10_2({0x04, 0x01, 0x10});
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(conn_id, 0x0024, vol_x10_2, GATT_WRITE, _, _))
+ .Times(0);
+ VolumeControl::Get()->SetVolume(test_address, 0x10);
+
+ const std::vector<uint8_t> vol_x20({0x04, 0x01, 0x20});
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(conn_id, 0x0024, vol_x20, GATT_WRITE, _, _))
+ .Times(1);
+ VolumeControl::Get()->SetVolume(test_address, 0x20);
}
TEST_F(VolumeControlValueSetTest, test_mute) {
diff --git a/system/gd/rust/common/src/init_flags.rs b/system/gd/rust/common/src/init_flags.rs
index 4fabae0..87a68dc 100644
--- a/system/gd/rust/common/src/init_flags.rs
+++ b/system/gd/rust/common/src/init_flags.rs
@@ -74,6 +74,7 @@
init_flags!(
flags: {
+ sdp_serialization,
gd_core,
gd_security,
gd_l2cap,
diff --git a/system/gd/rust/shim/src/init_flags.rs b/system/gd/rust/shim/src/init_flags.rs
index fd3015c..d993240 100644
--- a/system/gd/rust/shim/src/init_flags.rs
+++ b/system/gd/rust/shim/src/init_flags.rs
@@ -4,6 +4,7 @@
fn load(flags: Vec<String>);
fn set_all_for_testing();
+ fn sdp_serialization_is_enabled() -> bool;
fn gd_core_is_enabled() -> bool;
fn gd_security_is_enabled() -> bool;
fn gd_l2cap_is_enabled() -> bool;
diff --git a/system/stack/sdp/sdp_main.cc b/system/stack/sdp/sdp_main.cc
index 1cdeb96..9719d0f 100644
--- a/system/stack/sdp/sdp_main.cc
+++ b/system/stack/sdp/sdp_main.cc
@@ -22,8 +22,10 @@
*
******************************************************************************/
+#include <base/logging.h>
#include <string.h> // memset
+#include "gd/common/init_flags.h"
#include "osi/include/allocator.h"
#include "osi/include/osi.h" // UNUSED_ATTR
#include "stack/include/bt_hdr.h"
@@ -34,8 +36,6 @@
#include "stack/sdp/sdpint.h"
#include "types/raw_address.h"
-#include <base/logging.h>
-
/******************************************************************************/
/* G L O B A L S D P D A T A */
/******************************************************************************/
@@ -351,7 +351,8 @@
// Look for any active sdp connection on the remote device
cid = sdpu_get_active_ccb_cid(p_bd_addr);
- if (cid == 0) {
+ if (!bluetooth::common::init_flags::sdp_serialization_is_enabled() ||
+ cid == 0) {
p_ccb->con_state = SDP_STATE_CONN_SETUP;
cid = L2CA_ConnectReq2(BT_PSM_SDP, p_bd_addr, BTM_SEC_NONE);
} else {
diff --git a/system/stack/test/sdp/stack_sdp_test.cc b/system/stack/test/sdp/stack_sdp_test.cc
index 0251aab..98bb136 100644
--- a/system/stack/test/sdp/stack_sdp_test.cc
+++ b/system/stack/test/sdp/stack_sdp_test.cc
@@ -110,6 +110,8 @@
}
TEST_F(StackSdpMainTest, sdp_service_search_request_queuing) {
+ bluetooth::common::InitFlags::SetAllForTesting();
+
ASSERT_TRUE(SDP_ServiceSearchRequest(addr, sdp_db, nullptr));
const int cid = L2CA_ConnectReq2_cid;
tCONN_CB* p_ccb1 = find_ccb(cid, SDP_STATE_CONN_SETUP);
diff --git a/system/tools/scripts/dump_le_audio.py b/system/tools/scripts/dump_le_audio.py
index 806cdfb..048facb 100755
--- a/system/tools/scripts/dump_le_audio.py
+++ b/system/tools/scripts/dump_le_audio.py
@@ -79,6 +79,13 @@
# opcode for hci command
OPCODE_HCI_CREATE_CIS = 0x2064
OPCODE_REMOVE_ISO_DATA_PATH = 0x206F
+OPCODE_LE_SET_PERIODIC_ADVERTISING_DATA = 0x203F
+OPCODE_LE_CREATE_BIG = 0x2068
+OPCODE_LE_SETUP_ISO_DATA_PATH = 0x206E
+
+# HCI event
+EVENT_CODE_LE_META_EVENT = 0x3E
+SUBEVENT_CODE_LE_CREATE_BIG_COMPLETE = 0x1B
TYPE_STREAMING_AUDIO_CONTEXTS = 0x02
@@ -114,6 +121,9 @@
AUDIO_LOCATION_RIGHT = 0x02
AUDIO_LOCATION_CENTER = 0x04
+AD_TYPE_SERVICE_DATA_16_BIT = 0x16
+BASIC_AUDIO_ANNOUNCEMENT_SERVICE = 0x1851
+
packet_number = 0
debug_enable = False
add_header = False
@@ -158,35 +168,77 @@
print("octets_per_frame: " + str(self.octets_per_frame))
+class Broadcast:
+
+ def __init__(self):
+ self.num_of_bis = defaultdict(int) # subgroup - num_of_bis
+ self.bis = defaultdict(BisStream) # bis_index - codec_config
+ self.bis_index_handle_map = defaultdict(int) # bis_index - bis_handle
+ self.bis_index_list = []
+
+ def dump(self):
+ for bis_index, iso_stream in self.bis.items():
+ print("bis_index: " + str(bis_index) + " bis handle: " + str(self.bis_index_handle_map[bis_index]))
+ iso_stream.dump()
+
+
+class BisStream:
+
+ def __init__(self):
+ self.sampling_frequencies = 0xFF
+ self.frame_duration = 0xFF
+ self.channel_allocation = 0xFFFFFFFF
+ self.octets_per_frame = 0xFFFF
+ self.output_dump = []
+ self.start_time = 0xFFFFFFFF
+
+ def dump(self):
+ print("start_time: " + str(self.start_time))
+ print("sampling_frequencies: " + str(self.sampling_frequencies))
+ print("frame_duration: " + str(self.frame_duration))
+ print("channel_allocation: " + str(self.channel_allocation))
+ print("octets_per_frame: " + str(self.octets_per_frame))
+
+
connection_map = defaultdict(Connection)
cis_acl_map = defaultdict(int)
+broadcast_map = defaultdict(Broadcast)
+big_adv_map = defaultdict(int)
+bis_stream_map = defaultdict(BisStream)
-def generate_header(file, connection):
+def generate_header(file, stream, is_cis):
+ sf_case = {
+ SAMPLE_FREQUENCY_8000: 80,
+ SAMPLE_FREQUENCY_11025: 110,
+ SAMPLE_FREQUENCY_16000: 160,
+ SAMPLE_FREQUENCY_22050: 220,
+ SAMPLE_FREQUENCY_24000: 240,
+ SAMPLE_FREQUENCY_32000: 320,
+ SAMPLE_FREQUENCY_44100: 441,
+ SAMPLE_FREQUENCY_48000: 480,
+ SAMPLE_FREQUENCY_88200: 882,
+ SAMPLE_FREQUENCY_96000: 960,
+ SAMPLE_FREQUENCY_176400: 1764,
+ SAMPLE_FREQUENCY_192000: 1920,
+ SAMPLE_FREQUENCY_384000: 2840,
+ }
+ fd_case = {FRAME_DURATION_7_5: 7.5, FRAME_DURATION_10: 10}
+ al_case = {AUDIO_LOCATION_MONO: 1, AUDIO_LOCATION_LEFT: 1, AUDIO_LOCATION_RIGHT: 1, AUDIO_LOCATION_CENTER: 2}
+
header = bytearray.fromhex('1ccc1200')
- for ase in connection.ase.values():
- sf_case = {
- SAMPLE_FREQUENCY_8000: 80,
- SAMPLE_FREQUENCY_11025: 110,
- SAMPLE_FREQUENCY_16000: 160,
- SAMPLE_FREQUENCY_22050: 220,
- SAMPLE_FREQUENCY_24000: 240,
- SAMPLE_FREQUENCY_32000: 320,
- SAMPLE_FREQUENCY_44100: 441,
- SAMPLE_FREQUENCY_48000: 480,
- SAMPLE_FREQUENCY_88200: 882,
- SAMPLE_FREQUENCY_96000: 960,
- SAMPLE_FREQUENCY_176400: 1764,
- SAMPLE_FREQUENCY_192000: 1920,
- SAMPLE_FREQUENCY_384000: 2840,
- }
- header = header + struct.pack("<H", sf_case[ase.sampling_frequencies])
- fd_case = {FRAME_DURATION_7_5: 7.5, FRAME_DURATION_10: 10}
- header = header + struct.pack("<H", int(ase.octets_per_frame * 8 * 10 / fd_case[ase.frame_duration]))
- al_case = {AUDIO_LOCATION_MONO: 1, AUDIO_LOCATION_LEFT: 1, AUDIO_LOCATION_RIGHT: 1, AUDIO_LOCATION_CENTER: 2}
- header = header + struct.pack("<HHHL", al_case[ase.channel_allocation], fd_case[ase.frame_duration] * 100, 0,
- 48000000)
- break
+ if is_cis:
+ for ase in stream.ase.values():
+ header = header + struct.pack("<H", sf_case[ase.sampling_frequencies])
+ header = header + struct.pack("<H", int(ase.octets_per_frame * 8 * 10 / fd_case[ase.frame_duration]))
+ header = header + struct.pack("<HHHL", al_case[ase.channel_allocation], fd_case[ase.frame_duration] * 100,
+ 0, 48000000)
+ break
+ else:
+ header = header + struct.pack("<H", sf_case[stream.sampling_frequencies])
+ header = header + struct.pack("<H", int(stream.octets_per_frame * 8 * 10 / fd_case[stream.frame_duration]))
+ header = header + struct.pack("<HHHL", al_case[stream.channel_allocation], fd_case[stream.frame_duration] * 100,
+ 0, 48000000)
file.write(header)
@@ -206,7 +258,7 @@
ase.frame_duration = value
elif config_type == TYPE_CHANNEL_ALLOCATION:
ase.channel_allocation = value
- elif TYPE_OCTETS_PER_FRAME:
+ elif config_type == TYPE_OCTETS_PER_FRAME:
ase.octets_per_frame = value
length -= (config_length + 1)
@@ -284,6 +336,64 @@
packet_handle.get((opcode, flags), lambda x, y, z: None)(packet, connection_handle, timestamp)
+def parse_big_codec_information(adv_handle, packet):
+ # Ignore presentation delay
+ packet = unpack_data(packet, 3, True)
+ number_of_subgroup, packet = unpack_data(packet, 1, False)
+ for subgroup in range(number_of_subgroup):
+ num_of_bis, packet = unpack_data(packet, 1, False)
+ broadcast_map[adv_handle].num_of_bis[subgroup] = num_of_bis
+ # Ignore codec id
+ packet = unpack_data(packet, 5, True)
+ length, packet = unpack_data(packet, 1, False)
+ if len(packet) < length:
+ print("Invalid subgroup codec information length")
+ return
+
+ while length > 0:
+ config_length, packet = unpack_data(packet, 1, False)
+ config_type, packet = unpack_data(packet, 1, False)
+ value, packet = unpack_data(packet, config_length - 1, False)
+ if config_type == TYPE_SAMPLING_FREQUENCIES:
+ sampling_frequencies = value
+ elif config_type == TYPE_FRAME_DURATION:
+ frame_duration = value
+ elif config_type == TYPE_OCTETS_PER_FRAME:
+ octets_per_frame = value
+ else:
+ print("Unknown config type")
+ length -= (config_length + 1)
+
+ # Ignore metadata
+ metadata_length, packet = unpack_data(packet, 1, False)
+ packet = unpack_data(packet, metadata_length, True)
+
+ for count in range(num_of_bis):
+ bis_index, packet = unpack_data(packet, 1, False)
+ broadcast_map[adv_handle].bis_index_list.append(bis_index)
+ length, packet = unpack_data(packet, 1, False)
+ if len(packet) < length:
+ print("Invalid level 3 codec information length")
+ return
+
+ while length > 0:
+ config_length, packet = unpack_data(packet, 1, False)
+ config_type, packet = unpack_data(packet, 1, False)
+ value, packet = unpack_data(packet, config_length - 1, False)
+ if config_type == TYPE_CHANNEL_ALLOCATION:
+ channel_allocation = value
+ else:
+ print("Ignored config type")
+ length -= (config_length + 1)
+
+ broadcast_map[adv_handle].bis[bis_index].sampling_frequencies = sampling_frequencies
+ broadcast_map[adv_handle].bis[bis_index].frame_duration = frame_duration
+ broadcast_map[adv_handle].bis[bis_index].octets_per_frame = octets_per_frame
+ broadcast_map[adv_handle].bis[bis_index].channel_allocation = channel_allocation
+
+ return packet
+
+
def debug_print(log):
global packet_number
print("#" + str(packet_number) + ": " + log)
@@ -303,7 +413,7 @@
return value, data[byte:]
-def parse_command_packet(packet):
+def parse_command_packet(packet, timestamp):
opcode, packet = unpack_data(packet, 2, False)
if opcode == OPCODE_HCI_CREATE_CIS:
debug_print("OPCODE_HCI_CREATE_CIS")
@@ -330,9 +440,96 @@
debug_print("Invalid cmd length")
return
- cis_handle, packet = unpack_data(packet, 2, False)
- acl_handle = cis_acl_map[cis_handle]
- dump_audio_data_to_file(acl_handle)
+ iso_handle, packet = unpack_data(packet, 2, False)
+ # CIS stream
+ if iso_handle in cis_acl_map:
+ acl_handle = cis_acl_map[iso_handle]
+ dump_cis_audio_data_to_file(acl_handle)
+ # To Do: BIS stream
+ elif iso_handle in bis_stream_map:
+ dump_bis_audio_data_to_file(iso_handle)
+ elif opcode == OPCODE_LE_SET_PERIODIC_ADVERTISING_DATA:
+ debug_print("OPCODE_LE_SET_PERIODIC_ADVERTISING_DATA")
+
+ length, packet = unpack_data(packet, 1, False)
+ if length != len(packet):
+ debug_print("Invalid cmd length")
+ return
+
+ if length < 21:
+ debug_print("Ignored. Not basic audio announcement")
+ return
+
+ adv_hdl, packet = unpack_data(packet, 1, False)
+ #ignore operation, advertising_data_length
+ packet = unpack_data(packet, 2, True)
+ length, packet = unpack_data(packet, 1, False)
+ if length != len(packet):
+ debug_print("Invalid AD element length")
+ return
+
+ ad_type, packet = unpack_data(packet, 1, False)
+ service, packet = unpack_data(packet, 2, False)
+ if ad_type != AD_TYPE_SERVICE_DATA_16_BIT or service != BASIC_AUDIO_ANNOUNCEMENT_SERVICE:
+ debug_print("Ignored. Not basic audio announcement")
+ return
+
+ packet = parse_big_codec_information(adv_hdl, packet)
+ elif opcode == OPCODE_LE_CREATE_BIG:
+ debug_print("OPCODE_LE_CREATE_BIG")
+
+ length, packet = unpack_data(packet, 1, False)
+ if length != len(packet) and length < 31:
+ debug_print("Invalid Create BIG command length")
+ return
+
+ big_handle, packet = unpack_data(packet, 1, False)
+ adv_handle, packet = unpack_data(packet, 1, False)
+ big_adv_map[big_handle] = adv_handle
+ elif opcode == OPCODE_LE_SETUP_ISO_DATA_PATH:
+ debug_print("OPCODE_LE_SETUP_ISO_DATA_PATH")
+ length, packet = unpack_data(packet, 1, False)
+ if len(packet) != length:
+ debug_print("Invalid LE SETUP ISO DATA PATH command length")
+ return
+
+ iso_handle, packet = unpack_data(packet, 2, False)
+ if iso_handle in bis_stream_map:
+ bis_stream_map[iso_handle].start_time = timestamp
+
+
+def parse_event_packet(packet):
+ event_code, packet = unpack_data(packet, 1, False)
+ if event_code != EVENT_CODE_LE_META_EVENT:
+ return
+
+ length, packet = unpack_data(packet, 1, False)
+ if len(packet) != length:
+ print("Invalid LE mata event length")
+ return
+
+ subevent_code, packet = unpack_data(packet, 1, False)
+ if subevent_code != SUBEVENT_CODE_LE_CREATE_BIG_COMPLETE:
+ return
+
+ status, packet = unpack_data(packet, 1, False)
+ if status != 0x00:
+ debug_print("Create_BIG failed")
+ return
+
+ big_handle, packet = unpack_data(packet, 1, False)
+ if big_handle not in big_adv_map:
+ print("Invalid BIG handle")
+ return
+ adv_handle = big_adv_map[big_handle]
+ # Ignore, we don't care these parameter
+ packet = unpack_data(packet, 15, True)
+ num_of_bis, packet = unpack_data(packet, 1, False)
+ for count in range(num_of_bis):
+ bis_handle, packet = unpack_data(packet, 2, False)
+ bis_index = broadcast_map[adv_handle].bis_index_list[count]
+ broadcast_map[adv_handle].bis_index_handle_map[bis_index] = bis_handle
+ bis_stream_map[bis_handle] = broadcast_map[adv_handle].bis[bis_index]
def convert_time_str(timestamp):
@@ -348,7 +545,7 @@
return full_str_format
-def dump_audio_data_to_file(acl_handle):
+def dump_cis_audio_data_to_file(acl_handle):
if debug_enable:
connection_map[acl_handle].dump()
file_name = ""
@@ -389,20 +586,20 @@
break
if connection_map[acl_handle].input_dump != []:
- debug_print("Dump input...")
+ debug_print("Dump unicast input...")
f = open(file_name + "_input.bin", 'wb')
if add_header == True:
- generate_header(f, connection_map[acl_handle])
+ generate_header(f, connection_map[acl_handle], True)
arr = bytearray(connection_map[acl_handle].input_dump)
f.write(arr)
f.close()
connection_map[acl_handle].input_dump = []
if connection_map[acl_handle].output_dump != []:
- debug_print("Dump output...")
+ debug_print("Dump unicast output...")
f = open(file_name + "_output.bin", 'wb')
if add_header == True:
- generate_header(f, connection_map[acl_handle])
+ generate_header(f, connection_map[acl_handle], True)
arr = bytearray(connection_map[acl_handle].output_dump)
f.write(arr)
f.close()
@@ -411,6 +608,51 @@
return
+def dump_bis_audio_data_to_file(iso_handle):
+ if debug_enable:
+ bis_stream_map[iso_handle].dump()
+ file_name = "broadcast"
+ sf_case = {
+ SAMPLE_FREQUENCY_8000: "8000",
+ SAMPLE_FREQUENCY_11025: "11025",
+ SAMPLE_FREQUENCY_16000: "16000",
+ SAMPLE_FREQUENCY_22050: "22050",
+ SAMPLE_FREQUENCY_24000: "24000",
+ SAMPLE_FREQUENCY_32000: "32000",
+ SAMPLE_FREQUENCY_44100: "44100",
+ SAMPLE_FREQUENCY_48000: "48000",
+ SAMPLE_FREQUENCY_88200: "88200",
+ SAMPLE_FREQUENCY_96000: "96000",
+ SAMPLE_FREQUENCY_176400: "176400",
+ SAMPLE_FREQUENCY_192000: "192000",
+ SAMPLE_FREQUENCY_384000: "284000"
+ }
+ file_name += ("_sf" + sf_case[bis_stream_map[iso_handle].sampling_frequencies])
+ fd_case = {FRAME_DURATION_7_5: "7_5", FRAME_DURATION_10: "10"}
+ file_name += ("_fd" + fd_case[bis_stream_map[iso_handle].frame_duration])
+ al_case = {
+ AUDIO_LOCATION_MONO: "mono",
+ AUDIO_LOCATION_LEFT: "left",
+ AUDIO_LOCATION_RIGHT: "right",
+ AUDIO_LOCATION_CENTER: "center"
+ }
+ file_name += ("_" + al_case[bis_stream_map[iso_handle].channel_allocation])
+ file_name += ("_frame" + str(bis_stream_map[iso_handle].octets_per_frame))
+ file_name += ("_" + convert_time_str(bis_stream_map[iso_handle].start_time))
+
+ if bis_stream_map[iso_handle].output_dump != []:
+ debug_print("Dump broadcast output...")
+ f = open(file_name + "_output.bin", 'wb')
+ if add_header == True:
+ generate_header(f, bis_stream_map[iso_handle], False)
+ arr = bytearray(bis_stream_map[iso_handle].output_dump)
+ f.write(arr)
+ f.close()
+ bis_stream_map[iso_handle].output_dump = []
+
+ return
+
+
def parse_acl_packet(packet, flags, timestamp):
# Check the minimum acl length, HCI leader (4 bytes)
# + L2CAP header (4 bytes)
@@ -441,8 +683,8 @@
def parse_iso_packet(packet, flags):
- cis_handle, packet = unpack_data(packet, 2, False)
- cis_handle &= 0x0EFF
+ iso_handle, packet = unpack_data(packet, 2, False)
+ iso_handle &= 0x0EFF
iso_data_load_length, packet = unpack_data(packet, 2, False)
if iso_data_load_length != len(packet):
debug_print("Invalid iso data load length")
@@ -457,13 +699,18 @@
debug_print("Invalid iso sdu length")
return
- acl_handle = cis_acl_map[cis_handle]
- if flags == SENT:
- connection_map[acl_handle].output_dump.extend(struct.pack("<H", len(packet)))
- connection_map[acl_handle].output_dump.extend(list(packet))
- elif flags == RECEIVED:
- connection_map[acl_handle].input_dump.extend(struct.pack("<H", len(packet)))
- connection_map[acl_handle].input_dump.extend(list(packet))
+ # CIS stream
+ if iso_handle in cis_acl_map:
+ acl_handle = cis_acl_map[iso_handle]
+ if flags == SENT:
+ connection_map[acl_handle].output_dump.extend(struct.pack("<H", len(packet)))
+ connection_map[acl_handle].output_dump.extend(list(packet))
+ elif flags == RECEIVED:
+ connection_map[acl_handle].input_dump.extend(struct.pack("<H", len(packet)))
+ connection_map[acl_handle].input_dump.extend(list(packet))
+ elif iso_handle in bis_stream_map:
+ bis_stream_map[iso_handle].output_dump.extend(struct.pack("<H", len(packet)))
+ bis_stream_map[iso_handle].output_dump.extend(list(packet))
def parse_next_packet(btsnoop_file):
@@ -490,10 +737,10 @@
return False
packet_handle = {
- COMMADN_PACKET: (lambda x, y, z: parse_command_packet(x)),
+ COMMADN_PACKET: (lambda x, y, z: parse_command_packet(x, z)),
ACL_PACKET: (lambda x, y, z: parse_acl_packet(x, y, z)),
SCO_PACKET: (lambda x, y, z: None),
- EVENT_PACKET: (lambda x, y, z: None),
+ EVENT_PACKET: (lambda x, y, z: parse_event_packet(x)),
ISO_PACKET: (lambda x, y, z: parse_iso_packet(x, y))
}
packet_handle.get(type, lambda x, y, z: None)(packet, flags, timestamp)
@@ -535,7 +782,10 @@
break
for handle in connection_map.keys():
- dump_audio_data_to_file(handle)
+ dump_cis_audio_data_to_file(handle)
+
+ for handle in bis_stream_map.keys():
+ dump_bis_audio_data_to_file(handle)
if __name__ == "__main__":