diff options
76 files changed, 3577 insertions, 754 deletions
diff --git a/android/app/jni/com_android_bluetooth_gatt.cpp b/android/app/jni/com_android_bluetooth_gatt.cpp index 963eb56f74..bd46bf5fd4 100644 --- a/android/app/jni/com_android_bluetooth_gatt.cpp +++ b/android/app/jni/com_android_bluetooth_gatt.cpp @@ -2441,7 +2441,7 @@ static JNINativeMethod sScanMethods[] = { (void*)gattSetScanParametersNative}, }; -// JNI functions defined in GattService class. +// JNI functions defined in GattNativeInterface class. static JNINativeMethod sMethods[] = { {"classInitNative", "()V", (void*)classInitNative}, {"initializeNative", "()V", (void*)initializeNative}, @@ -2526,7 +2526,8 @@ int register_com_android_bluetooth_gatt(JNIEnv* env) { env, "com/android/bluetooth/gatt/PeriodicScanManager", sPeriodicScanMethods, NELEM(sPeriodicScanMethods)); return register_success & - jniRegisterNativeMethods(env, "com/android/bluetooth/gatt/GattService", - sMethods, NELEM(sMethods)); + jniRegisterNativeMethods( + env, "com/android/bluetooth/gatt/GattNativeInterface", sMethods, + NELEM(sMethods)); } } // namespace android diff --git a/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java b/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java index b320b8a2c8..4b2ca367ef 100755 --- a/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java +++ b/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java @@ -772,9 +772,11 @@ public class BassClientStateMachine extends StateMachine { log("processBroadcastReceiverState: characteristic:" + characteristic); BluetoothLeBroadcastReceiveState recvState = parseBroadcastReceiverState( receiverState); - if (recvState == null || recvState.getSourceId() == -1) { - log("Null recvState or processBroadcastReceiverState: invalid index: " - + recvState.getSourceId()); + if (recvState == null) { + log("processBroadcastReceiverState: Null recvState"); + return; + } else if (recvState.getSourceId() == -1) { + log("processBroadcastReceiverState: invalid index: " + recvState.getSourceId()); return; } BluetoothLeBroadcastReceiveState oldRecvState = diff --git a/android/app/src/com/android/bluetooth/gatt/GattNativeInterface.java b/android/app/src/com/android/bluetooth/gatt/GattNativeInterface.java new file mode 100644 index 0000000000..8786890a71 --- /dev/null +++ b/android/app/src/com/android/bluetooth/gatt/GattNativeInterface.java @@ -0,0 +1,635 @@ +/* + * 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.gatt; + +import android.bluetooth.BluetoothDevice; +import android.os.RemoteException; + +import java.util.ArrayList; +import java.util.List; + +/** + * GATT Profile Native Interface to/from JNI. + */ +public class GattNativeInterface { + private static final String TAG = GattNativeInterface.class.getSimpleName(); + + static { + classInitNative(); + } + + private static GattNativeInterface sInterface; + private static final Object INSTANCE_LOCK = new Object(); + + private GattService mGattService; + + private GattNativeInterface() {} + + GattService getGattService() { + return mGattService; + } + + /** + * This class is a singleton because native library should only be loaded once + * + * @return default instance + */ + public static GattNativeInterface getInstance() { + synchronized (INSTANCE_LOCK) { + if (sInterface == null) { + sInterface = new GattNativeInterface(); + } + } + return sInterface; + } + + + /* Callbacks */ + + void onScanResult(int eventType, int addressType, String address, int primaryPhy, + int secondaryPhy, int advertisingSid, int txPower, int rssi, int periodicAdvInt, + byte[] advData, String originalAddress) { + getGattService().onScanResult(eventType, addressType, address, primaryPhy, secondaryPhy, + advertisingSid, txPower, rssi, periodicAdvInt, advData, originalAddress); + } + + void onScannerRegistered(int status, int scannerId, long uuidLsb, long uuidMsb) + throws RemoteException { + getGattService().onScannerRegistered(status, scannerId, uuidLsb, uuidMsb); + } + + void onClientRegistered(int status, int clientIf, long uuidLsb, long uuidMsb) + throws RemoteException { + getGattService().onClientRegistered(status, clientIf, uuidLsb, uuidMsb); + } + + void onConnected(int clientIf, int connId, int status, String address) throws RemoteException { + getGattService().onConnected(clientIf, connId, status, address); + } + + void onDisconnected(int clientIf, int connId, int status, String address) + throws RemoteException { + getGattService().onDisconnected(clientIf, connId, status, address); + } + + void onClientPhyUpdate(int connId, int txPhy, int rxPhy, int status) throws RemoteException { + getGattService().onClientPhyUpdate(connId, txPhy, rxPhy, status); + } + + void onClientPhyRead(int clientIf, String address, int txPhy, int rxPhy, int status) + throws RemoteException { + getGattService().onClientPhyRead(clientIf, address, txPhy, rxPhy, status); + } + + void onClientConnUpdate(int connId, int interval, int latency, int timeout, int status) + throws RemoteException { + getGattService().onClientConnUpdate(connId, interval, latency, timeout, status); + } + + void onServiceChanged(int connId) throws RemoteException { + getGattService().onServiceChanged(connId); + } + + void onServerPhyUpdate(int connId, int txPhy, int rxPhy, int status) throws RemoteException { + getGattService().onServerPhyUpdate(connId, txPhy, rxPhy, status); + } + + void onServerPhyRead(int serverIf, String address, int txPhy, int rxPhy, int status) + throws RemoteException { + getGattService().onServerPhyRead(serverIf, address, txPhy, rxPhy, status); + } + + void onServerConnUpdate(int connId, int interval, int latency, int timeout, int status) + throws RemoteException { + getGattService().onServerConnUpdate(connId, interval, latency, timeout, status); + } + + void onSearchCompleted(int connId, int status) throws RemoteException { + getGattService().onSearchCompleted(connId, status); + } + + GattDbElement getSampleGattDbElement() { + return getGattService().getSampleGattDbElement(); + } + + void onGetGattDb(int connId, ArrayList<GattDbElement> db) throws RemoteException { + getGattService().onGetGattDb(connId, db); + } + + void onRegisterForNotifications(int connId, int status, int registered, int handle) { + getGattService().onRegisterForNotifications(connId, status, registered, handle); + } + + void onNotify(int connId, String address, int handle, boolean isNotify, byte[] data) + throws RemoteException { + getGattService().onNotify(connId, address, handle, isNotify, data); + } + + void onReadCharacteristic(int connId, int status, int handle, byte[] data) + throws RemoteException { + getGattService().onReadCharacteristic(connId, status, handle, data); + } + + void onWriteCharacteristic(int connId, int status, int handle, byte[] data) + throws RemoteException { + getGattService().onWriteCharacteristic(connId, status, handle, data); + } + + void onExecuteCompleted(int connId, int status) throws RemoteException { + getGattService().onExecuteCompleted(connId, status); + } + + void onReadDescriptor(int connId, int status, int handle, byte[] data) throws RemoteException { + getGattService().onReadDescriptor(connId, status, handle, data); + } + + void onWriteDescriptor(int connId, int status, int handle, byte[] data) throws RemoteException { + getGattService().onWriteDescriptor(connId, status, handle, data); + } + + void onReadRemoteRssi(int clientIf, String address, int rssi, int status) + throws RemoteException { + getGattService().onReadRemoteRssi(clientIf, address, rssi, status); + } + + void onScanFilterEnableDisabled(int action, int status, int clientIf) { + getGattService().onScanFilterEnableDisabled(action, status, clientIf); + } + + void onScanFilterParamsConfigured(int action, int status, int clientIf, int availableSpace) { + getGattService().onScanFilterParamsConfigured(action, status, clientIf, availableSpace); + } + + void onScanFilterConfig(int action, int status, int clientIf, int filterType, + int availableSpace) { + getGattService().onScanFilterConfig(action, status, clientIf, filterType, availableSpace); + } + + void onBatchScanStorageConfigured(int status, int clientIf) { + getGattService().onBatchScanStorageConfigured(status, clientIf); + } + + void onBatchScanStartStopped(int startStopAction, int status, int clientIf) { + getGattService().onBatchScanStartStopped(startStopAction, status, clientIf); + } + + void onBatchScanReports(int status, int scannerId, int reportType, int numRecords, + byte[] recordData) throws RemoteException { + getGattService().onBatchScanReports(status, scannerId, reportType, numRecords, recordData); + } + + void onBatchScanThresholdCrossed(int clientIf) { + getGattService().onBatchScanThresholdCrossed(clientIf); + } + + AdvtFilterOnFoundOnLostInfo createOnTrackAdvFoundLostObject(int clientIf, int advPktLen, + byte[] advPkt, int scanRspLen, byte[] scanRsp, int filtIndex, int advState, + int advInfoPresent, String address, int addrType, int txPower, int rssiValue, + int timeStamp) { + return getGattService().createOnTrackAdvFoundLostObject(clientIf, advPktLen, advPkt, + scanRspLen, scanRsp, filtIndex, advState, advInfoPresent, address, addrType, + txPower, rssiValue, timeStamp); + } + + void onTrackAdvFoundLost(AdvtFilterOnFoundOnLostInfo trackingInfo) throws RemoteException { + getGattService().onTrackAdvFoundLost(trackingInfo); + } + + void onScanParamSetupCompleted(int status, int scannerId) throws RemoteException { + getGattService().onScanParamSetupCompleted(status, scannerId); + } + + void onConfigureMTU(int connId, int status, int mtu) throws RemoteException { + getGattService().onConfigureMTU(connId, status, mtu); + } + + void onClientCongestion(int connId, boolean congested) throws RemoteException { + getGattService().onClientCongestion(connId, congested); + } + + /* Server callbacks */ + + void onServerRegistered(int status, int serverIf, long uuidLsb, long uuidMsb) + throws RemoteException { + getGattService().onServerRegistered(status, serverIf, uuidLsb, uuidMsb); + } + + void onServiceAdded(int status, int serverIf, List<GattDbElement> service) + throws RemoteException { + getGattService().onServiceAdded(status, serverIf, service); + } + + void onServiceStopped(int status, int serverIf, int srvcHandle) throws RemoteException { + getGattService().onServiceStopped(status, serverIf, srvcHandle); + } + + void onServiceDeleted(int status, int serverIf, int srvcHandle) { + getGattService().onServiceDeleted(status, serverIf, srvcHandle); + } + + void onClientConnected(String address, boolean connected, int connId, int serverIf) + throws RemoteException { + getGattService().onClientConnected(address, connected, connId, serverIf); + } + + void onServerReadCharacteristic(String address, int connId, int transId, int handle, int offset, + boolean isLong) throws RemoteException { + getGattService().onServerReadCharacteristic(address, connId, transId, handle, offset, + isLong); + } + + void onServerReadDescriptor(String address, int connId, int transId, int handle, int offset, + boolean isLong) throws RemoteException { + getGattService().onServerReadDescriptor(address, connId, transId, handle, offset, isLong); + } + + void onServerWriteCharacteristic(String address, int connId, int transId, int handle, + int offset, int length, boolean needRsp, boolean isPrep, byte[] data) + throws RemoteException { + getGattService().onServerWriteCharacteristic(address, connId, transId, handle, offset, + length, needRsp, isPrep, data); + } + + void onServerWriteDescriptor(String address, int connId, int transId, int handle, int offset, + int length, boolean needRsp, boolean isPrep, byte[] data) throws RemoteException { + getGattService().onServerWriteDescriptor(address, connId, transId, handle, offset, length, + needRsp, isPrep, data); + } + + void onExecuteWrite(String address, int connId, int transId, int execWrite) + throws RemoteException { + getGattService().onExecuteWrite(address, connId, transId, execWrite); + } + + void onResponseSendCompleted(int status, int attrHandle) { + getGattService().onResponseSendCompleted(status, attrHandle); + } + + void onNotificationSent(int connId, int status) throws RemoteException { + getGattService().onNotificationSent(connId, status); + } + + void onServerCongestion(int connId, boolean congested) throws RemoteException { + getGattService().onServerCongestion(connId, congested); + } + + void onMtuChanged(int connId, int mtu) throws RemoteException { + getGattService().onMtuChanged(connId, mtu); + } + + /* Native methods */ + private static native void classInitNative(); + private native void initializeNative(); + private native void cleanupNative(); + private native int gattClientGetDeviceTypeNative(String address); + private native void gattClientRegisterAppNative(long appUuidLsb, long appUuidMsb, + boolean eattSupport); + private native void gattClientUnregisterAppNative(int clientIf); + private native void gattClientConnectNative(int clientIf, String address, boolean isDirect, + int transport, boolean opportunistic, int initiatingPhys); + private native void gattClientDisconnectNative(int clientIf, String address, int connId); + private native void gattClientSetPreferredPhyNative(int clientIf, String address, int txPhy, + int rxPhy, int phyOptions); + private native void gattClientReadPhyNative(int clientIf, String address); + private native void gattClientRefreshNative(int clientIf, String address); + private native void gattClientSearchServiceNative(int connId, boolean searchAll, + long serviceUuidLsb, long serviceUuidMsb); + private native void gattClientDiscoverServiceByUuidNative(int connId, long serviceUuidLsb, + long serviceUuidMsb); + private native void gattClientGetGattDbNative(int connId); + private native void gattClientReadCharacteristicNative(int connId, int handle, int authReq); + private native void gattClientReadUsingCharacteristicUuidNative(int connId, long uuidMsb, + long uuidLsb, int sHandle, int eHandle, int authReq); + private native void gattClientReadDescriptorNative(int connId, int handle, int authReq); + private native void gattClientWriteCharacteristicNative(int connId, int handle, int writeType, + int authReq, byte[] value); + private native void gattClientWriteDescriptorNative(int connId, int handle, int authReq, + byte[] value); + private native void gattClientExecuteWriteNative(int connId, boolean execute); + private native void gattClientRegisterForNotificationsNative(int clientIf, String address, + int handle, boolean enable); + private native void gattClientReadRemoteRssiNative(int clientIf, String address); + private native void gattClientConfigureMTUNative(int connId, int mtu); + private native void gattConnectionParameterUpdateNative(int clientIf, String address, + int minInterval, int maxInterval, int latency, int timeout, int minConnectionEventLen, + int maxConnectionEventLen); + private native void gattServerRegisterAppNative(long appUuidLsb, long appUuidMsb, + boolean eattSupport); + private native void gattServerUnregisterAppNative(int serverIf); + private native void gattServerConnectNative(int serverIf, String address, boolean isDirect, + int transport); + private native void gattServerDisconnectNative(int serverIf, String address, int connId); + private native void gattServerSetPreferredPhyNative(int clientIf, String address, int txPhy, + int rxPhy, int phyOptions); + private native void gattServerReadPhyNative(int clientIf, String address); + private native void gattServerAddServiceNative(int serverIf, List<GattDbElement> service); + private native void gattServerStopServiceNative(int serverIf, int svcHandle); + private native void gattServerDeleteServiceNative(int serverIf, int svcHandle); + private native void gattServerSendIndicationNative(int serverIf, int attrHandle, int connId, + byte[] val); + private native void gattServerSendNotificationNative(int serverIf, int attrHandle, int connId, + byte[] val); + private native void gattServerSendResponseNative(int serverIf, int connId, int transId, + int status, int handle, int offset, byte[] val, int authReq); + private native void gattTestNative(int command, long uuid1Lsb, long uuid1Msb, String bda1, + int p1, int p2, int p3, int p4, int p5); + + /** + * Initialize the native interface and native components + */ + public void init(GattService gattService) { + mGattService = gattService; + initializeNative(); + } + + /** + * Clean up the native interface and native components + */ + public void cleanup() { + cleanupNative(); + mGattService = null; + } + + /** + * Get the type of Bluetooth device + * + * @param address address of the Bluetooth device + * @return type of Bluetooth device 0 for BR/EDR, 1 for BLE, 2 for DUAL mode (To be confirmed) + */ + public int gattClientGetDeviceType(String address) { + return gattClientGetDeviceTypeNative(address); + } + + /** + * Register the given client + * It will invoke {@link #onClientRegistered(int, int, long, long)}. + */ + public void gattClientRegisterApp(long appUuidLsb, long appUuidMsb, boolean eattSupport) { + gattClientRegisterAppNative(appUuidLsb, appUuidMsb, eattSupport); + } + + /** + * Unregister the client + */ + public void gattClientUnregisterApp(int clientIf) { + gattClientUnregisterAppNative(clientIf); + } + + /** + * Connect to the remote Gatt server + * @see {@link BluetoothDevice#connectGatt} for parameters. + */ + public void gattClientConnect(int clientIf, String address, boolean isDirect, int transport, + boolean opportunistic, int initiatingPhys) { + gattClientConnectNative(clientIf, address, isDirect, transport, opportunistic, + initiatingPhys); + } + + /** + * Disconnect from the remote Gatt server + */ + public void gattClientDisconnect(int clientIf, String address, int connId) { + gattClientDisconnectNative(clientIf, address, connId); + } + + /** + * Set the preferred connection PHY for the client + */ + public void gattClientSetPreferredPhy(int clientIf, String address, int txPhy, + int rxPhy, int phyOptions) { + gattClientSetPreferredPhyNative(clientIf, address, txPhy, rxPhy, phyOptions); + } + + /** + * Read the current transmitter PHY and receiver PHY of the client + */ + public void gattClientReadPhy(int clientIf, String address) { + gattClientReadPhyNative(clientIf, address); + } + + /** + * Clear the internal cache and force a refresh of the services from the remote device + */ + public void gattClientRefresh(int clientIf, String address) { + gattClientRefreshNative(clientIf, address); + } + + /** + * Discover GATT services + */ + public void gattClientSearchService(int connId, boolean searchAll, long serviceUuidLsb, + long serviceUuidMsb) { + gattClientSearchServiceNative(connId, searchAll, serviceUuidLsb, serviceUuidMsb); + } + + /** + * Discover the GATT service by the given UUID + */ + public void gattClientDiscoverServiceByUuid(int connId, long serviceUuidLsb, + long serviceUuidMsb) { + gattClientDiscoverServiceByUuidNative(connId, serviceUuidLsb, serviceUuidMsb); + } + + /** + * Get GATT DB of the remote device + */ + public void gattClientGetGattDb(int connId) { + gattClientGetGattDbNative(connId); + } + + /** + * Read a characteristic by the given handle + */ + public void gattClientReadCharacteristic(int connId, int handle, int authReq) { + gattClientReadCharacteristicNative(connId, handle, authReq); + } + + + /** + * Read a characteristic by the given UUID + */ + public void gattClientReadUsingCharacteristicUuid(int connId, long uuidMsb, + long uuidLsb, int sHandle, int eHandle, int authReq) { + gattClientReadUsingCharacteristicUuidNative(connId, uuidMsb, uuidLsb, sHandle, eHandle, + authReq); + } + + /** + * Read a descriptor by the given handle + */ + public void gattClientReadDescriptor(int connId, int handle, int authReq) { + gattClientReadDescriptorNative(connId, handle, authReq); + } + + /** + * Write a characteristic by the given handle + */ + public void gattClientWriteCharacteristic(int connId, int handle, int writeType, + int authReq, byte[] value) { + gattClientWriteCharacteristicNative(connId, handle, writeType, authReq, value); + } + + /** + * Write a descriptor by the given handle + */ + public void gattClientWriteDescriptor(int connId, int handle, int authReq, + byte[] value) { + gattClientWriteDescriptorNative(connId, handle, authReq, value); + } + + /** + * Execute a reliable write transaction + * @param connId + * @param execute + */ + public void gattClientExecuteWrite(int connId, boolean execute) { + gattClientExecuteWriteNative(connId, execute); + } + + /** + * Register notification for the characteristic + */ + public void gattClientRegisterForNotifications(int clientIf, String address, + int handle, boolean enable) { + gattClientRegisterForNotificationsNative(clientIf, address, handle, enable); + } + + /** + * Read the RSSI for a connected remote device + * @param clientIf + * @param address + */ + public void gattClientReadRemoteRssi(int clientIf, String address) { + gattClientReadRemoteRssiNative(clientIf, address); + } + + /** + * Configure MTU size used for the connection + */ + public void gattClientConfigureMTU(int connId, int mtu) { + gattClientConfigureMTUNative(connId, mtu); + } + + /** + * Update connection parameter. + */ + public void gattConnectionParameterUpdate(int clientIf, String address, + int minInterval, int maxInterval, int latency, int timeout, int minConnectionEventLen, + int maxConnectionEventLen) { + gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, latency, + timeout, minConnectionEventLen, maxConnectionEventLen); + } + + /** + * Register GATT server + */ + public void gattServerRegisterApp(long appUuidLsb, long appUuidMsb, boolean eattSupport) { + gattServerRegisterAppNative(appUuidLsb, appUuidMsb, eattSupport); + } + + /** + * Unregister GATT server + */ + public void gattServerUnregisterApp(int serverIf) { + gattServerUnregisterAppNative(serverIf); + } + + /** + * Connect to a remote device as a GATT server role + */ + public void gattServerConnect(int serverIf, String address, boolean isDirect, + int transport) { + gattServerConnectNative(serverIf, address, isDirect, transport); + } + + /** + * Disconnects from a remote device as a GATT server role + */ + public void gattServerDisconnect(int serverIf, String address, int connId) { + gattServerDisconnectNative(serverIf, address, connId); + } + + /** + * Set the preferred connection PHY as a GATT server role + */ + public void gattServerSetPreferredPhy(int clientIf, String address, int txPhy, + int rxPhy, int phyOptions) { + gattServerSetPreferredPhyNative(clientIf, address, txPhy, rxPhy, phyOptions); + } + + /** + * Read the current transmitter PHY and receiver PHY of the connection + */ + public void gattServerReadPhy(int clientIf, String address) { + gattServerReadPhyNative(clientIf, address); + } + + /** + * Add a service to the list of services to be hosted. + */ + public void gattServerAddService(int serverIf, List<GattDbElement> service) { + gattServerAddServiceNative(serverIf, service); + } + + /** + * Stop a service + */ + public void gattServerStopService(int serverIf, int svcHandle) { + gattServerStopServiceNative(serverIf, svcHandle); + } + + /** + * Removes a service from the list of services to be provided + */ + public void gattServerDeleteService(int serverIf, int svcHandle) { + gattServerDeleteServiceNative(serverIf, svcHandle); + } + + /** + * Send an indication of the characteristic + */ + public void gattServerSendIndication(int serverIf, int attrHandle, int connId, + byte[] val) { + gattServerSendIndicationNative(serverIf, attrHandle, connId, val); + } + + /** + * Send a notification of the characteristic + */ + public void gattServerSendNotification(int serverIf, int attrHandle, int connId, + byte[] val) { + gattServerSendNotificationNative(serverIf, attrHandle, connId, val); + } + + /** + * Send a response as a GATT server role + */ + public void gattServerSendResponse(int serverIf, int connId, int transId, + int status, int handle, int offset, byte[] val, int authReq) { + gattServerSendResponseNative(serverIf, connId, transId, status, handle, offset, val, + authReq); + } + + /** + * Send a test command + */ + public void gattTest(int command, long uuid1Lsb, long uuid1Msb, String bda1, + int p1, int p2, int p3, int p4, int p5) { + gattTestNative(command, uuid1Lsb, uuid1Msb, bda1, p1, p2, p3, p4, p5); + } +} + diff --git a/android/app/src/com/android/bluetooth/gatt/GattObjectsFactory.java b/android/app/src/com/android/bluetooth/gatt/GattObjectsFactory.java new file mode 100644 index 0000000000..85e54eada9 --- /dev/null +++ b/android/app/src/com/android/bluetooth/gatt/GattObjectsFactory.java @@ -0,0 +1,63 @@ +/* + * 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.gatt; + +import android.util.Log; + +import com.android.bluetooth.Utils; +/** + * Factory class for object initialization to help with unit testing + */ +public class GattObjectsFactory { + private static final String TAG = GattObjectsFactory.class.getSimpleName(); + private static GattObjectsFactory sInstance; + private static final Object INSTANCE_LOCK = new Object(); + + private GattObjectsFactory() { + } + + /** + * Get the singleton instance of object factory + * + * @return the singleton instance, guaranteed not null + */ + public static GattObjectsFactory getInstance() { + synchronized (INSTANCE_LOCK) { + if (sInstance == null) { + sInstance = new GattObjectsFactory(); + } + } + return sInstance; + } + + /** + * Allow unit tests to substitute GattObjectsFactory with a test instance + * + * @param objectsFactory a test instance of the GattObjectsFactory + */ + static void setInstanceForTesting(GattObjectsFactory objectsFactory) { + Utils.enforceInstrumentationTestMode(); + synchronized (INSTANCE_LOCK) { + Log.d(TAG, "setInstanceForTesting(), set to " + objectsFactory); + sInstance = objectsFactory; + } + } + + public GattNativeInterface getNativeInterface() { + return GattNativeInterface.getInstance(); + } +} diff --git a/android/app/src/com/android/bluetooth/gatt/GattService.java b/android/app/src/com/android/bluetooth/gatt/GattService.java index 12d3bf1dd4..0d4cfa95b9 100644 --- a/android/app/src/com/android/bluetooth/gatt/GattService.java +++ b/android/app/src/com/android/bluetooth/gatt/GattService.java @@ -301,9 +301,7 @@ public class GattService extends ProfileService { */ private Set<String> mReliableQueue = new HashSet<String>(); - static { - classInitNative(); - } + private GattNativeInterface mNativeInterface; @Override protected IProfileServiceBinder initBinder() { @@ -319,7 +317,8 @@ public class GattService extends ProfileService { Settings.Global.putInt( getContentResolver(), "bluetooth_sanitized_exposure_notification_supported", 1); - initializeNative(); + mNativeInterface = GattObjectsFactory.getInstance().getNativeInterface(); + mNativeInterface.init(this); mAdapterService = AdapterService.getAdapterService(); mBluetoothAdapterProxy = BluetoothAdapterProxy.getInstance(); mCompanionManager = getSystemService(CompanionDeviceManager.class); @@ -348,15 +347,8 @@ public class GattService extends ProfileService { mServerMap.clear(); mHandleMap.clear(); mReliableQueue.clear(); - if (mAdvertiseManager != null) { - mAdvertiseManager.cleanup(); - } - if (mScanManager != null) { - mScanManager.cleanup(); - } - if (mPeriodicScanManager != null) { - mPeriodicScanManager.cleanup(); - } + cleanup(); + return true; } @@ -365,7 +357,10 @@ public class GattService extends ProfileService { if (DBG) { Log.d(TAG, "cleanup()"); } - cleanupNative(); + if (mNativeInterface != null) { + mNativeInterface.cleanup(); + mNativeInterface = null; + } if (mAdvertiseManager != null) { mAdvertiseManager.cleanup(); } @@ -2212,7 +2207,7 @@ public class GattService extends ProfileService { Thread t = new Thread(new Runnable() { @Override public void run() { - gattClientGetGattDbNative(connId); + mNativeInterface.gattClientGetGattDb(connId); } }); t.start(); @@ -3380,7 +3375,8 @@ public class GattService extends ProfileService { Log.d(TAG, "registerClient() - UUID=" + uuid); } mClientMap.add(uuid, null, callback, null, this); - gattClientRegisterAppNative(uuid.getLeastSignificantBits(), uuid.getMostSignificantBits(), eatt_support); + mNativeInterface.gattClientRegisterApp(uuid.getLeastSignificantBits(), + uuid.getMostSignificantBits(), eatt_support); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3394,7 +3390,7 @@ public class GattService extends ProfileService { Log.d(TAG, "unregisterClient() - clientIf=" + clientIf); } mClientMap.remove(clientIf); - gattClientUnregisterAppNative(clientIf); + mNativeInterface.gattClientUnregisterApp(clientIf); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3413,7 +3409,8 @@ public class GattService extends ProfileService { statsLogGattConnectionStateChange( BluetoothProfile.GATT, address, clientIf, BluetoothProtoEnums.CONNECTION_STATE_CONNECTING); - gattClientConnectNative(clientIf, address, isDirect, transport, opportunistic, phy); + mNativeInterface.gattClientConnect(clientIf, address, isDirect, transport, opportunistic, + phy); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3430,7 +3427,7 @@ public class GattService extends ProfileService { statsLogGattConnectionStateChange( BluetoothProfile.GATT, address, clientIf, BluetoothProtoEnums.CONNECTION_STATE_DISCONNECTING); - gattClientDisconnectNative(clientIf, address, connId != null ? connId : 0); + mNativeInterface.gattClientDisconnect(clientIf, address, connId != null ? connId : 0); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3452,7 +3449,7 @@ public class GattService extends ProfileService { if (DBG) { Log.d(TAG, "clientSetPreferredPhy() - address=" + address + ", connId=" + connId); } - gattClientSetPreferredPhyNative(clientIf, address, txPhy, rxPhy, phyOptions); + mNativeInterface.gattClientSetPreferredPhy(clientIf, address, txPhy, rxPhy, phyOptions); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3473,7 +3470,7 @@ public class GattService extends ProfileService { if (DBG) { Log.d(TAG, "clientReadPhy() - address=" + address + ", connId=" + connId); } - gattClientReadPhyNative(clientIf, address); + mNativeInterface.gattClientReadPhy(clientIf, address); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3523,7 +3520,7 @@ public class GattService extends ProfileService { if (DBG) { Log.d(TAG, "refreshDevice() - address=" + address); } - gattClientRefreshNative(clientIf, address); + mNativeInterface.gattClientRefresh(clientIf, address); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3539,7 +3536,7 @@ public class GattService extends ProfileService { } if (connId != null) { - gattClientSearchServiceNative(connId, true, 0, 0); + mNativeInterface.gattClientSearchService(connId, true, 0, 0); } else { Log.e(TAG, "discoverServices() - No connection for " + address + "..."); } @@ -3555,7 +3552,7 @@ public class GattService extends ProfileService { Integer connId = mClientMap.connIdByAddress(clientIf, address); if (connId != null) { - gattClientDiscoverServiceByUuidNative(connId, uuid.getLeastSignificantBits(), + mNativeInterface.gattClientDiscoverServiceByUuid(connId, uuid.getLeastSignificantBits(), uuid.getMostSignificantBits()); } else { Log.e(TAG, "discoverServiceByUuid() - No connection for " + address + "..."); @@ -3592,7 +3589,7 @@ public class GattService extends ProfileService { return; } - gattClientReadCharacteristicNative(connId, handle, authReq); + mNativeInterface.gattClientReadCharacteristic(connId, handle, authReq); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3625,8 +3622,9 @@ public class GattService extends ProfileService { return; } - gattClientReadUsingCharacteristicUuidNative(connId, uuid.getLeastSignificantBits(), - uuid.getMostSignificantBits(), startHandle, endHandle, authReq); + mNativeInterface.gattClientReadUsingCharacteristicUuid(connId, + uuid.getLeastSignificantBits(), uuid.getMostSignificantBits(), startHandle, + endHandle, authReq); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3669,7 +3667,7 @@ public class GattService extends ProfileService { atomicBoolean.set(false); } - gattClientWriteCharacteristicNative(connId, handle, writeType, authReq, value); + mNativeInterface.gattClientWriteCharacteristic(connId, handle, writeType, authReq, value); return BluetoothStatusCodes.SUCCESS; } @@ -3703,7 +3701,7 @@ public class GattService extends ProfileService { return; } - gattClientReadDescriptorNative(connId, handle, authReq); + mNativeInterface.gattClientReadDescriptor(connId, handle, authReq); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3724,7 +3722,7 @@ public class GattService extends ProfileService { } permissionCheck(connId, handle); - gattClientWriteDescriptorNative(connId, handle, authReq, value); + mNativeInterface.gattClientWriteDescriptor(connId, handle, authReq, value); return BluetoothStatusCodes.SUCCESS; } @@ -3756,7 +3754,7 @@ public class GattService extends ProfileService { Integer connId = mClientMap.connIdByAddress(clientIf, address); if (connId != null) { - gattClientExecuteWriteNative(connId, execute); + mNativeInterface.gattClientExecuteWrite(connId, execute); } } @@ -3790,7 +3788,7 @@ public class GattService extends ProfileService { return; } - gattClientRegisterForNotificationsNative(clientIf, address, handle, enable); + mNativeInterface.gattClientRegisterForNotifications(clientIf, address, handle, enable); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3803,7 +3801,7 @@ public class GattService extends ProfileService { if (DBG) { Log.d(TAG, "readRemoteRssi() - address=" + address); } - gattClientReadRemoteRssiNative(clientIf, address); + mNativeInterface.gattClientReadRemoteRssi(clientIf, address); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3818,7 +3816,7 @@ public class GattService extends ProfileService { } Integer connId = mClientMap.connIdByAddress(clientIf, address); if (connId != null) { - gattClientConfigureMTUNative(connId, mtu); + mNativeInterface.gattClientConfigureMTU(connId, mtu); } else { Log.e(TAG, "configureMTU() - No connection for " + address + "..."); } @@ -3868,8 +3866,8 @@ public class GattService extends ProfileService { Log.d(TAG, "connectionParameterUpdate() - address=" + address + "params=" + connectionPriority + " interval=" + minInterval + "/" + maxInterval); } - gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, latency, - timeout, 0, 0); + mNativeInterface.gattConnectionParameterUpdate(clientIf, address, minInterval, maxInterval, + latency, timeout, 0, 0); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -3890,7 +3888,7 @@ public class GattService extends ProfileService { } - gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, + mNativeInterface.gattConnectionParameterUpdate(clientIf, address, minInterval, maxInterval, peripheralLatency, supervisionTimeout, minConnectionEventLen, maxConnectionEventLen); } @@ -4199,7 +4197,8 @@ public class GattService extends ProfileService { Log.d(TAG, "registerServer() - UUID=" + uuid); } mServerMap.add(uuid, null, callback, null, this); - gattServerRegisterAppNative(uuid.getLeastSignificantBits(), uuid.getMostSignificantBits(), eatt_support); + mNativeInterface.gattServerRegisterApp(uuid.getLeastSignificantBits(), + uuid.getMostSignificantBits(), eatt_support); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -4216,7 +4215,7 @@ public class GattService extends ProfileService { deleteServices(serverIf); mServerMap.remove(serverIf); - gattServerUnregisterAppNative(serverIf); + mNativeInterface.gattServerUnregisterApp(serverIf); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -4230,7 +4229,7 @@ public class GattService extends ProfileService { if (DBG) { Log.d(TAG, "serverConnect() - address=" + address); } - gattServerConnectNative(serverIf, address, isDirect, transport); + mNativeInterface.gattServerConnect(serverIf, address, isDirect, transport); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -4245,7 +4244,7 @@ public class GattService extends ProfileService { Log.d(TAG, "serverDisconnect() - address=" + address + ", connId=" + connId); } - gattServerDisconnectNative(serverIf, address, connId != null ? connId : 0); + mNativeInterface.gattServerDisconnect(serverIf, address, connId != null ? connId : 0); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -4267,7 +4266,7 @@ public class GattService extends ProfileService { if (DBG) { Log.d(TAG, "serverSetPreferredPhy() - address=" + address + ", connId=" + connId); } - gattServerSetPreferredPhyNative(serverIf, address, txPhy, rxPhy, phyOptions); + mNativeInterface.gattServerSetPreferredPhy(serverIf, address, txPhy, rxPhy, phyOptions); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -4288,7 +4287,7 @@ public class GattService extends ProfileService { if (DBG) { Log.d(TAG, "serverReadPhy() - address=" + address + ", connId=" + connId); } - gattServerReadPhyNative(serverIf, address); + mNativeInterface.gattServerReadPhy(serverIf, address); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -4335,7 +4334,7 @@ public class GattService extends ProfileService { } } - gattServerAddServiceNative(serverIf, db); + mNativeInterface.gattServerAddService(serverIf, db); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -4349,7 +4348,7 @@ public class GattService extends ProfileService { Log.d(TAG, "removeService() - handle=" + handle); } - gattServerDeleteServiceNative(serverIf, handle); + mNativeInterface.gattServerDeleteService(serverIf, handle); } @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -4384,7 +4383,7 @@ public class GattService extends ProfileService { } Integer connId = mServerMap.connIdByAddress(serverIf, address); - gattServerSendResponseNative(serverIf, connId != null ? connId : 0, requestId, + mNativeInterface.gattServerSendResponse(serverIf, connId != null ? connId : 0, requestId, (byte) status, handle, offset, value, (byte) 0); mHandleMap.deleteRequest(requestId); } @@ -4407,9 +4406,9 @@ public class GattService extends ProfileService { } if (confirm) { - gattServerSendIndicationNative(serverIf, handle, connId, value); + mNativeInterface.gattServerSendIndication(serverIf, handle, connId, value); } else { - gattServerSendNotificationNative(serverIf, handle, connId, value); + mNativeInterface.gattServerSendNotification(serverIf, handle, connId, value); } return BluetoothStatusCodes.SUCCESS; @@ -4457,7 +4456,7 @@ public class GattService extends ProfileService { } private int getDeviceType(BluetoothDevice device) { - int type = gattClientGetDeviceTypeNative(device.getAddress()); + int type = mNativeInterface.gattClientGetDeviceType(device.getAddress()); if (DBG) { Log.d(TAG, "getDeviceType() - device=" + device + ", type=" + type); } @@ -4596,7 +4595,7 @@ public class GattService extends ProfileService { continue; } - gattServerStopServiceNative(serverIf, entry.handle); + mNativeInterface.gattServerStopService(serverIf, entry.handle); return; } } @@ -4622,7 +4621,7 @@ public class GattService extends ProfileService { /* Now actually delete the services.... */ for (Integer handle : handleList) { - gattServerDeleteServiceNative(serverIf, handle); + mNativeInterface.gattServerDeleteService(serverIf, handle); } } @@ -4712,111 +4711,16 @@ public class GattService extends ProfileService { /************************************************************************** * GATT Test functions *************************************************************************/ - void gattTestCommand(int command, UUID uuid1, String bda1, int p1, int p2, int p3, int p4, int p5) { if (bda1 == null) { bda1 = "00:00:00:00:00:00"; } if (uuid1 != null) { - gattTestNative(command, uuid1.getLeastSignificantBits(), uuid1.getMostSignificantBits(), - bda1, p1, p2, p3, p4, p5); + mNativeInterface.gattTest(command, uuid1.getLeastSignificantBits(), + uuid1.getMostSignificantBits(), bda1, p1, p2, p3, p4, p5); } else { - gattTestNative(command, 0, 0, bda1, p1, p2, p3, p4, p5); + mNativeInterface.gattTest(command, 0, 0, bda1, p1, p2, p3, p4, p5); } } - - private native void gattTestNative(int command, long uuid1Lsb, long uuid1Msb, String bda1, - int p1, int p2, int p3, int p4, int p5); - - /************************************************************************** - * Native functions prototypes - *************************************************************************/ - - private static native void classInitNative(); - - private native void initializeNative(); - - private native void cleanupNative(); - - private native int gattClientGetDeviceTypeNative(String address); - - private native void gattClientRegisterAppNative(long appUuidLsb, long appUuidMsb, boolean eatt_support); - - private native void gattClientUnregisterAppNative(int clientIf); - - private native void gattClientConnectNative(int clientIf, String address, boolean isDirect, - int transport, boolean opportunistic, int initiatingPhys); - - private native void gattClientDisconnectNative(int clientIf, String address, int connId); - - private native void gattClientSetPreferredPhyNative(int clientIf, String address, int txPhy, - int rxPhy, int phyOptions); - - private native void gattClientReadPhyNative(int clientIf, String address); - - private native void gattClientRefreshNative(int clientIf, String address); - - private native void gattClientSearchServiceNative(int connId, boolean searchAll, - long serviceUuidLsb, long serviceUuidMsb); - - private native void gattClientDiscoverServiceByUuidNative(int connId, long serviceUuidLsb, - long serviceUuidMsb); - - private native void gattClientGetGattDbNative(int connId); - - private native void gattClientReadCharacteristicNative(int connId, int handle, int authReq); - - private native void gattClientReadUsingCharacteristicUuidNative(int connId, long uuidMsb, - long uuidLsb, int sHandle, int eHandle, int authReq); - - private native void gattClientReadDescriptorNative(int connId, int handle, int authReq); - - private native void gattClientWriteCharacteristicNative(int connId, int handle, int writeType, - int authReq, byte[] value); - - private native void gattClientWriteDescriptorNative(int connId, int handle, int authReq, - byte[] value); - - private native void gattClientExecuteWriteNative(int connId, boolean execute); - - private native void gattClientRegisterForNotificationsNative(int clientIf, String address, - int handle, boolean enable); - - private native void gattClientReadRemoteRssiNative(int clientIf, String address); - - private native void gattClientConfigureMTUNative(int connId, int mtu); - - private native void gattConnectionParameterUpdateNative(int clientIf, String address, - int minInterval, int maxInterval, int latency, int timeout, int minConnectionEventLen, - int maxConnectionEventLen); - - private native void gattServerRegisterAppNative(long appUuidLsb, long appUuidMsb, boolean eatt_support); - - private native void gattServerUnregisterAppNative(int serverIf); - - private native void gattServerConnectNative(int serverIf, String address, boolean isDirect, - int transport); - - private native void gattServerDisconnectNative(int serverIf, String address, int connId); - - private native void gattServerSetPreferredPhyNative(int clientIf, String address, int txPhy, - int rxPhy, int phyOptions); - - private native void gattServerReadPhyNative(int clientIf, String address); - - private native void gattServerAddServiceNative(int serverIf, List<GattDbElement> service); - - private native void gattServerStopServiceNative(int serverIf, int svcHandle); - - private native void gattServerDeleteServiceNative(int serverIf, int svcHandle); - - private native void gattServerSendIndicationNative(int serverIf, int attrHandle, int connId, - byte[] val); - - private native void gattServerSendNotificationNative(int serverIf, int attrHandle, int connId, - byte[] val); - - private native void gattServerSendResponseNative(int serverIf, int connId, int transId, - int status, int handle, int offset, byte[] val, int authReq); } 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 76f1c3f004..a60987ccf2 100644 --- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java @@ -633,7 +633,11 @@ public class LeAudioService extends ProfileService { private Integer getAudioDirectionsFromActiveContextsMap(Integer activeContexts) { Integer supportedAudioDirections = 0; - if ((activeContexts & mContextSupportingInputAudio) != 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) { diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java index 90a1a560c4..9856fb4d02 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java @@ -2,6 +2,10 @@ package com.android.bluetooth.gatt; import static org.mockito.Mockito.*; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; import android.content.Context; import androidx.test.InstrumentationRegistry; @@ -22,6 +26,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * Test cases for {@link GattService}. @@ -30,12 +38,17 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class GattServiceTest { private static final int TIMES_UP_AND_DOWN = 3; + private static final int TIMEOUT_MS = 5_000; private Context mTargetContext; private GattService mService; @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule(); + private BluetoothAdapter mAdapter; @Mock private AdapterService mAdapterService; + @Mock private GattObjectsFactory mFactory; + @Mock private GattNativeInterface mNativeInterface; + private BluetoothDevice mCurrentDevice; @Before public void setUp() throws Exception { @@ -44,6 +57,12 @@ public class GattServiceTest { MockitoAnnotations.initMocks(this); TestUtils.setAdapterService(mAdapterService); doReturn(true).when(mAdapterService).isStartedProfile(anyString()); + + GattObjectsFactory.setInstanceForTesting(mFactory); + doReturn(mNativeInterface).when(mFactory).getNativeInterface(); + + mAdapter = BluetoothAdapter.getDefaultAdapter(); + TestUtils.startService(mServiceRule, GattService.class); mService = GattService.getGattService(); Assert.assertNotNull(mService); @@ -59,11 +78,13 @@ public class GattServiceTest { mService = GattService.getGattService(); Assert.assertNull(mService); TestUtils.clearAdapterService(mAdapterService); + GattObjectsFactory.setInstanceForTesting(null); } @Test public void testInitialize() { - Assert.assertNotNull(GattService.getGattService()); + Assert.assertEquals(mService, GattService.getGattService()); + verify(mNativeInterface).init(eq(mService)); } @Test diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java index 589eceaa93..2cac893bfd 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java @@ -416,10 +416,6 @@ public class BluetoothProxy { LeAudioDeviceStateWrapper valid_device = valid_device_opt.get(); LeAudioDeviceStateWrapper.BassData svc_data = valid_device.bassData; - // TODO: Is the receiver_id same with BluetoothLeBroadcastReceiveState.getSourceId()? - // If not, find getSourceId() usages and fix the issues. -// rstate.receiver_id = intent.getIntExtra( -// BluetoothBroadcastAudioScan.EXTRA_BASS_RECEIVER_ID, -1); /** * From "Introducing-Bluetooth-LE-Audio-book" 8.6.3.1: * @@ -435,8 +431,6 @@ public class BluetoothProxy { */ /** - * From BluetoothBroadcastAudioScan.EXTRA_BASS_RECEIVER_ID: - * * Broadcast receiver's endpoint identifier. */ synchronized(this) { diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java index 0b2eb4c809..2296d21693 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java @@ -42,11 +42,6 @@ import java.util.List; public class BroadcastScanActivity extends AppCompatActivity { - // Integer key used for sending/receiving receiver ID. - public static final String EXTRA_BASS_RECEIVER_ID = "receiver_id"; - - private static final int BIS_ALL = 0xFFFFFFFF; - private BluetoothDevice device; private BroadcastScanViewModel mViewModel; private BroadcastItemsAdapter adapter; diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java index 6d188d9dc6..4be87caae6 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java @@ -26,8 +26,6 @@ import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_I import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED; import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCINFO_REQUEST; -import static com.android.bluetooth.leaudio.BroadcastScanActivity.EXTRA_BASS_RECEIVER_ID; - import android.animation.ObjectAnimator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapClient; @@ -1810,27 +1808,27 @@ public class LeAudioRecycleViewAdapter alert.setTitle("Scan and add a source or remove the currently set one."); BluetoothDevice device = devices.get(ViewHolder.this.getAdapterPosition()).device; - if (bassReceiverIdSpinner.getSelectedItem() == null) { - Toast.makeText(view.getContext(), "Not available", - Toast.LENGTH_SHORT).show(); - return; + int receiver_id = -1; + if (bassReceiverIdSpinner.getSelectedItem() != null) { + receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString()); } - int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString()); alert.setPositiveButton("Scan", (dialog, whichButton) -> { // Scan for new announcements Intent intent = new Intent(this.itemView.getContext(), BroadcastScanActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - intent.putExtra(EXTRA_BASS_RECEIVER_ID, receiver_id); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, devices.get(ViewHolder.this.getAdapterPosition()).device); parent.startActivityForResult(intent, 666); }); alert.setNeutralButton("Cancel", (dialog, whichButton) -> { // Do nothing }); - alert.setNegativeButton("Remove", (dialog, whichButton) -> { - bassInteractionListener.onRemoveSourceReq(device, receiver_id); - }); + if (receiver_id != -1) { + final int remove_receiver_id = receiver_id; + alert.setNegativeButton("Remove", (dialog, whichButton) -> { + bassInteractionListener.onRemoveSourceReq(device, remove_receiver_id); + }); + } alert.show(); } else if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_code_required))) { @@ -1887,16 +1885,9 @@ public class LeAudioRecycleViewAdapter AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext()); alert.setTitle("Retry broadcast audio announcement scan?"); - if (bassReceiverIdSpinner.getSelectedItem() == null) { - Toast.makeText(view.getContext(), "Not available", - Toast.LENGTH_SHORT).show(); - return; - } - int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString()); alert.setPositiveButton("Yes", (dialog, whichButton) -> { // Scan for new announcements Intent intent = new Intent(view.getContext(), BroadcastScanActivity.class); - intent.putExtra(EXTRA_BASS_RECEIVER_ID, receiver_id); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, devices.get(ViewHolder.this.getAdapterPosition()).device); parent.startActivityForResult(intent, 666); }); diff --git a/android/pandora/mmi2grpc/mmi2grpc/gatt.py b/android/pandora/mmi2grpc/mmi2grpc/gatt.py index f997494e0a..ba933213f7 100644 --- a/android/pandora/mmi2grpc/mmi2grpc/gatt.py +++ b/android/pandora/mmi2grpc/mmi2grpc/gatt.py @@ -13,12 +13,14 @@ # limitations under the License. import re +import sys from mmi2grpc._helpers import assert_description from mmi2grpc._proxy import ProfileProxy from pandora.gatt_grpc import GATT from pandora.host_grpc import Host +from pandora.gatt_pb2 import AttStatusCode # Tests that need GATT cache cleared before discovering services. NEEDS_CACHE_CLEARED = { @@ -37,6 +39,7 @@ class GATTProxy(ProfileProxy): self.services = None self.characteristics = None self.descriptors = None + self.read_value = None @assert_description def MMI_IUT_INITIATE_CONNECTION(self, test, pts_addr: bytes, **kwargs): @@ -69,6 +72,7 @@ class GATTProxy(ProfileProxy): self.services = None self.characteristics = None self.descriptors = None + self.read_value = None return "OK" @assert_description @@ -229,7 +233,7 @@ class GATTProxy(ProfileProxy): assert self.connection is not None assert self.services is not None for service in self.services: - assert len(service.included_services) is 0 + assert len(service.included_services) == 0 return "OK" def MMI_CONFIRM_INCLUDE_SERVICE(self, description: str, **kwargs): @@ -262,8 +266,9 @@ class GATTProxy(ProfileProxy): stringHandleToInt(all_matches[i + 1]),\ formatUuid(all_matches[i + 3])): found_services += 1 - assert found_services == (len(all_matches) / 4) - return "OK" + if found_services == (len(all_matches) / 4): + return "Yes" + return "No" def MMI_IUT_DISCOVER_SERVICE_UUID(self, description: str, **kwargs): """ @@ -405,6 +410,245 @@ class GATTProxy(ProfileProxy): return "Yes" return "No" + def MMI_IUT_SEND_READ_CHARACTERISTIC_HANDLE(self, description: str, **kwargs): + """ + Please send read characteristic handle = 'XXXX'O to the PTS. + Description: Verify that the Implementation Under Test (IUT) can send + Read characteristic. + """ + + assert self.connection is not None + handle = stringHandleToInt(re.findall("'([a0-Z9]*)'O", description)[0]) + self.read_value = self.gatt.ReadCharacteristicFromHandle(\ + connection=self.connection, handle=handle) + return "OK" + + @assert_description + def MMI_IUT_CONFIRM_READ_INVALID_HANDLE(self, **kwargs): + """ + Please confirm IUT received Invalid handle error. Click Yes if IUT + received it, otherwise click No. + + Description: Verify that the + Implementation Under Test (IUT) indicate Invalid handle error when read + a characteristic. + """ + + assert self.read_value is not None + if self.read_value.status == AttStatusCode.INVALID_HANDLE: + return "Yes" + return "No" + + @assert_description + def MMI_IUT_CONFIRM_READ_NOT_PERMITTED(self, **kwargs): + """ + Please confirm IUT received read is not permitted error. Click Yes if + IUT received it, otherwise click No. + + Description: Verify that the + Implementation Under Test (IUT) indicate read is not permitted error + when read a characteristic. + """ + + assert self.read_value is not None + # Android read error doesn't return an error code so we have to also + # compare to the generic error code here. + if self.read_value.status == AttStatusCode.READ_NOT_PERMITTED or\ + self.read_value.status == AttStatusCode.UNKNOWN_ERROR: + return "Yes" + return "No" + + @assert_description + def MMI_IUT_CONFIRM_READ_AUTHENTICATION(self, **kwargs): + """ + Please confirm IUT received authentication error. Click Yes if IUT + received it, otherwise click No. + + Description: Verify that the + Implementation Under Test (IUT) indicate authentication error when read + a characteristic. + """ + + assert self.read_value is not None + if self.read_value.status == AttStatusCode.INSUFFICIENT_AUTHENTICATION: + return "Yes" + return "No" + + def MMI_IUT_SEND_READ_CHARACTERISTIC_UUID(self, description: str, **kwargs): + """ + Please send read using characteristic UUID = 'XXXX'O handle range = + 'XXXX'O to 'XXXX'O to the PTS. + + Description: Verify that the + Implementation Under Test (IUT) can send Read characteristic by UUID. + """ + + assert self.connection is not None + matches = re.findall("'([a0-Z9]*)'O", description) + self.read_value = self.gatt.ReadCharacteristicFromUuid(\ + connection=self.connection, uuid=formatUuid(matches[0]),\ + start_handle=stringHandleToInt(matches[1]),\ + end_handle=stringHandleToInt(matches[2])) + return "OK" + + @assert_description + def MMI_IUT_CONFIRM_ATTRIBUTE_NOT_FOUND(self, **kwargs): + """ + Please confirm IUT received attribute not found error. Click Yes if IUT + received it, otherwise click No. + + Description: Verify that the + Implementation Under Test (IUT) indicate attribute not found error when + read a characteristic. + """ + + assert self.read_value is not None + # Android read error doesn't return an error code so we have to also + # compare to the generic error code here. + if self.read_value.status == AttStatusCode.ATTRIBUTE_NOT_FOUND or\ + self.read_value.status == AttStatusCode.UNKNOWN_ERROR: + return "Yes" + return "No" + + def MMI_IUT_SEND_READ_GREATER_OFFSET(self, description: str, **kwargs): + """ + Please send read to handle = 'XXXX'O and offset greater than 'XXXX'O to + the PTS. + + Description: Verify that the Implementation Under Test (IUT) + can send Read with invalid offset. + """ + + # Android handles the read offset internally, so we just do read with handle here. + # Unfortunately for testing, this will always work. + assert self.connection is not None + handle = stringHandleToInt(re.findall("'([a0-Z9]*)'O", description)[0]) + self.read_value = self.gatt.ReadCharacteristicFromHandle(\ + connection=self.connection, handle=handle) + return "OK" + + @assert_description + def MMI_IUT_CONFIRM_READ_INVALID_OFFSET(self, **kwargs): + """ + Please confirm IUT received Invalid offset error. Click Yes if IUT + received it, otherwise click No. + + Description: Verify that the + Implementation Under Test (IUT) indicate Invalid offset error when read + a characteristic. + """ + + # Android handles read offset internally, so we can't read with wrong offset. + return "Yes" + + @assert_description + def MMI_IUT_CONFIRM_READ_APPLICATION(self, **kwargs): + """ + Please confirm IUT received Application error. Click Yes if IUT received + it, otherwise click No. + + Description: Verify that the Implementation + Under Test (IUT) indicate Application error when read a characteristic. + """ + + assert self.read_value is not None + if self.read_value.status == AttStatusCode.APPLICATION_ERROR: + return "Yes" + return "No" + + def MMI_IUT_CONFIRM_READ_CHARACTERISTIC_VALUE(self, description: str, **kwargs): + """ + Please confirm IUT received characteristic value='XX'O in random + selected adopted database. Click Yes if IUT received it, otherwise click + No. + + Description: Verify that the Implementation Under Test (IUT) can + send Read characteristic to PTS random select adopted database. + """ + + assert self.read_value is not None + characteristic_value = bytes.fromhex(re.findall("'([a0-Z9]*)'O", description)[0]) + if characteristic_value[0] in self.read_value.value: + return "Yes" + return "No" + + def MMI_IUT_READ_BY_TYPE_UUID(self, description: str, **kwargs): + """ + Please send read by type characteristic UUID = 'XXXX'O to the PTS. + Description: Verify that the Implementation Under Test (IUT) can send + Read characteristic. + """ + + assert self.connection is not None + matches = re.findall("'([a0-Z9]*)'O", description) + self.read_value = self.gatt.ReadCharacteristicFromUuid(\ + connection=self.connection, uuid=formatUuid(matches[0]),\ + start_handle=0x0001,\ + end_handle=0xffff) + return "OK" + + def MMI_IUT_READ_BY_TYPE_UUID_ALT(self, description: str, **kwargs): + """ + Please send read by type characteristic UUID = + 'XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX'O to the PTS. + + Description: + Verify that the Implementation Under Test (IUT) can send Read + characteristic. + """ + + assert self.connection is not None + uuid = formatUuid(re.findall("'([a0-Z9-]*)'O", description)[0]) + self.read_value = self.gatt.ReadCharacteristicFromUuid(\ + connection=self.connection, uuid=uuid, start_handle=0x0001, end_handle=0xffff) + return "OK" + + def MMI_IUT_CONFIRM_READ_HANDLE_VALUE(self, description: str, **kwargs): + """ + Please confirm IUT Handle='XX'O characteristic + value='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'O in random + selected adopted database. Click Yes if it matches the IUT, otherwise + click No. + + Description: Verify that the Implementation Under Test (IUT) + can send Read long characteristic to PTS random select adopted database. + """ + + assert self.read_value is not None + bytes_value = bytes.fromhex(re.search("value='(.*)'O", description)[1]) + if self.read_value.value == bytes_value: + return "Yes" + return "No" + + def MMI_IUT_SEND_READ_DESCIPTOR_HANDLE(self, description: str, **kwargs): + """ + Please send read characteristic descriptor handle = 'XXXX'O to the PTS. + Description: Verify that the Implementation Under Test (IUT) can send + Read characteristic descriptor. + """ + + assert self.connection is not None + handle = stringHandleToInt(re.findall("'([a0-Z9]*)'O", description)[0]) + self.read_value = self.gatt.ReadCharacteristicDescriptorFromHandle(\ + connection=self.connection, handle=handle) + return "OK" + + def MMI_IUT_CONFIRM_READ_DESCRIPTOR_VALUE(self, description: str, **kwargs): + """ + Please confirm IUT received Descriptor value='XXXXXXXX'O in random + selected adopted database. Click Yes if IUT received it, otherwise click + No. + + Description: Verify that the Implementation Under Test (IUT) can + send Read Descriptor to PTS random select adopted database. + """ + + assert self.read_value is not None + bytes_value = bytes.fromhex(re.search("value='(.*)'O", description)[1]) + if self.read_value.value == bytes_value: + return "Yes" + return "No" + common_uuid = "0000XXXX-0000-1000-8000-00805f9b34fb" diff --git a/android/pandora/server/configs/PtsBotTest.xml b/android/pandora/server/configs/PtsBotTest.xml index 930980f100..fd51c13577 100644 --- a/android/pandora/server/configs/PtsBotTest.xml +++ b/android/pandora/server/configs/PtsBotTest.xml @@ -25,6 +25,7 @@ <option name="profile" value="AVRCP" /> <option name="profile" value="GATT/CL/GAC" /> <option name="profile" value="GATT/CL/GAD" /> + <option name="profile" value="GATT/CL/GAR" /> <option name="profile" value="HFP/AG/DIS" /> <option name="profile" value="HFP/AG/HFI" /> <option name="profile" value="HFP/AG/SLC" /> diff --git a/android/pandora/server/configs/pts_bot_tests_config.json b/android/pandora/server/configs/pts_bot_tests_config.json index 0db8d14133..ba1e8d45e4 100644 --- a/android/pandora/server/configs/pts_bot_tests_config.json +++ b/android/pandora/server/configs/pts_bot_tests_config.json @@ -15,7 +15,6 @@ "AVDTP/SRC/ACP/SIG/SMG/BI-26-C", "AVDTP/SRC/ACP/SIG/SMG/BI-33-C", "AVDTP/SRC/ACP/SIG/SMG/BV-06-C", - "AVDTP/SRC/ACP/SIG/SMG/BV-08-C", "AVDTP/SRC/ACP/SIG/SMG/BV-10-C", "AVDTP/SRC/ACP/SIG/SMG/BV-12-C", "AVDTP/SRC/ACP/SIG/SMG/BV-16-C", @@ -59,12 +58,18 @@ "GATT/CL/GAD/BV-06-C", "GATT/CL/GAD/BV-07-C", "GATT/CL/GAD/BV-08-C", - "HFP/AG/DIS/BV-01-I", - "HFP/AG/HFI/BI-03-I", - "HFP/AG/HFI/BV-02-I", - "HFP/AG/SLC/BV-09-I", - "HFP/AG/SLC/BV-10-I", - "HFP/AG/TCA/BV-04-I", + "GATT/CL/GAR/BI-01-C", + "GATT/CL/GAR/BI-02-C", + "GATT/CL/GAR/BI-06-C", + "GATT/CL/GAR/BI-07-C", + "GATT/CL/GAR/BI-12-C", + "GATT/CL/GAR/BI-13-C", + "GATT/CL/GAR/BI-14-C", + "GATT/CL/GAR/BI-35-C", + "GATT/CL/GAR/BV-01-C", + "GATT/CL/GAR/BV-04-C", + "GATT/CL/GAR/BV-06-C", + "GATT/CL/GAR/BV-07-C", "SDP/SR/BRW/BV-02-C", "SDP/SR/SA/BI-01-C", "SDP/SR/SA/BI-02-C", @@ -101,7 +106,6 @@ "SM/CEN/EKS/BI-01-C", "SM/CEN/EKS/BV-01-C", "SM/CEN/JW/BV-05-C", - "SM/CEN/KDU/BI-01-C", "SM/CEN/KDU/BV-04-C", "SM/CEN/KDU/BV-05-C", "SM/CEN/KDU/BI-02-C", @@ -123,6 +127,7 @@ "A2DP/SRC/SYN/BV-02-I", "AVDTP/SRC/ACP/SIG/SMG/BI-11-C", "AVDTP/SRC/ACP/SIG/SMG/BI-23-C", + "AVDTP/SRC/ACP/SIG/SMG/BV-08-C", "AVDTP/SRC/ACP/SIG/SMG/BV-14-C", "AVDTP/SRC/ACP/SIG/SMG/ESR05/BV-14-C", "AVDTP/SRC/INT/SIG/SMG/BI-35-C", @@ -182,6 +187,16 @@ "AVRCP/CT/CRC/BV-01-I", "AVRCP/TG/CRC/BV-02-I", "AVRCP/CT/CEC/BV-01-I", + "GATT/CL/GAR/BI-04-C", + "GATT/CL/GAR/BI-05-C", + "GATT/CL/GAR/BI-10-C", + "GATT/CL/GAR/BI-11-C", + "GATT/CL/GAR/BI-16-C", + "GATT/CL/GAR/BI-17-C", + "GATT/CL/GAR/BV-03-C", + "HFP/AG/DIS/BV-01-I", + "HFP/AG/HFI/BI-03-I", + "HFP/AG/HFI/BV-02-I", "HFP/AG/SLC/BV-01-C", "HFP/AG/SLC/BV-02-C", "HFP/AG/SLC/BV-03-C", @@ -189,12 +204,16 @@ "HFP/AG/SLC/BV-05-I", "HFP/AG/SLC/BV-06-I", "HFP/AG/SLC/BV-07-I", + "HFP/AG/SLC/BV-09-I", + "HFP/AG/SLC/BV-10-I", "HFP/AG/TCA/BV-01-I", "HFP/AG/TCA/BV-02-I", "HFP/AG/TCA/BV-03-I", + "HFP/AG/TCA/BV-04-I", "HFP/AG/TCA/BV-05-I", "SM/CEN/JW/BI-01-C", - "SM/CEN/JW/BI-04-C" + "SM/CEN/JW/BI-04-C", + "SM/CEN/KDU/BI-01-C" ], "ics": { "TSPC_4.0HCI_1a_2": true, @@ -1219,4 +1238,4 @@ "SPP": {}, "SUM ICS": {} } -}
\ No newline at end of file +} diff --git a/android/pandora/server/proto/pandora/gatt.proto b/android/pandora/server/proto/pandora/gatt.proto index 0f8c8f5217..23e3f5725e 100644 --- a/android/pandora/server/proto/pandora/gatt.proto +++ b/android/pandora/server/proto/pandora/gatt.proto @@ -25,6 +25,26 @@ service GATT { // Clears DUT GATT cache. rpc ClearCache(ClearCacheRequest) returns (google.protobuf.Empty); + + // Reads characteristic with given handle. + rpc ReadCharacteristicFromHandle(ReadCharacteristicRequest) returns (ReadCharacteristicResponse); + + // Reads characteristic with given uuid, start and end handles. + rpc ReadCharacteristicFromUuid(ReadCharacteristicFromUuidRequest) returns (ReadCharacteristicResponse); + + // Reads characteristic with given descriptor handle. + rpc ReadCharacteristicDescriptorFromHandle(ReadCharacteristicDescriptorRequest) returns (ReadCharacteristicDescriptorResponse); +} + +enum AttStatusCode { + SUCCESS = 0x00; + UNKNOWN_ERROR = 0x101; + INVALID_HANDLE = 0x01; + READ_NOT_PERMITTED = 0x02; + INSUFFICIENT_AUTHENTICATION = 0x05; + INVALID_OFFSET = 0x07; + ATTRIBUTE_NOT_FOUND = 0x0A; + APPLICATION_ERROR = 0x80; } // A message representing a GATT service. @@ -58,7 +78,7 @@ message ExchangeMTURequest { int32 mtu = 2; } -// Request for the `writeCharacteristicFromHandle` rpc. +// Request for the `WriteCharacteristicFromHandle` rpc. message WriteCharacteristicRequest { Connection connection = 1; uint32 handle = 2; @@ -95,3 +115,35 @@ message DiscoverServicesSdpResponse { message ClearCacheRequest { Connection connection = 1; } + +// Request for the `ReadCharacteristicFromHandle` rpc. +message ReadCharacteristicRequest { + Connection connection = 1; + uint32 handle = 2; +} + +// Request for the `ReadCharacteristicFromUuid` rpc. +message ReadCharacteristicFromUuidRequest { + Connection connection = 1; + string uuid = 2; + uint32 start_handle = 3; + uint32 end_handle = 4; +} + +// Response for the `ReadCharacteristicFromHandle` and `ReadCharacteristicFromUuid` rpc. +message ReadCharacteristicResponse { + bytes value = 1; + AttStatusCode status = 2; +} + +// Request for the `ReadCharacteristicDescriptorFromHandle` rpc. +message ReadCharacteristicDescriptorRequest { + Connection connection = 1; + uint32 handle = 2; +} + +// Response for the `ReadCharacteristicDescriptorFromHandle` rpc. +message ReadCharacteristicDescriptorResponse { + bytes value = 1; + AttStatusCode status = 2; +} diff --git a/android/pandora/server/src/com/android/pandora/Gatt.kt b/android/pandora/server/src/com/android/pandora/Gatt.kt index 1a48c1e41f..a22f28d06b 100644 --- a/android/pandora/server/src/com/android/pandora/Gatt.kt +++ b/android/pandora/server/src/com/android/pandora/Gatt.kt @@ -18,16 +18,20 @@ package com.android.pandora import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothStatusCodes +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import com.google.protobuf.Empty +import com.google.protobuf.ByteString import io.grpc.Status import io.grpc.stub.StreamObserver @@ -74,9 +78,9 @@ class Gatt(private val context: Context) : GATTImplBase() { responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(mScope, responseObserver) { val mtu = request.mtu - val addr = request.connection.cookie.toByteArray().decodeToString() - if (!GattInstance.get(addr).mGatt.requestMtu(mtu)) { - Log.e(TAG, "Error on requesting MTU for $addr") + Log.i(TAG, "exchangeMTU MTU=$mtu") + if (!GattInstance.get(request.connection.cookie).mGatt.requestMtu(mtu)) { + Log.e(TAG, "Error on requesting MTU $mtu") throw Status.UNKNOWN.asException() } Empty.getDefaultInstance() @@ -86,16 +90,15 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun writeCharacteristicFromHandle(request: WriteCharacteristicRequest, responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(mScope, responseObserver) { - val addr = request.connection.cookie.toByteArray().decodeToString() - val gattInstance = GattInstance.get(addr) + val gattInstance = GattInstance.get(request.connection.cookie) val characteristic: BluetoothGattCharacteristic? = getCharacteristicWithHandle(request.handle, gattInstance) if (characteristic != null) { + Log.i(TAG, "writeCharacteristicFromHandle handle=${request.handle}") gattInstance.mGatt.writeCharacteristic(characteristic, request.value.toByteArray(), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) } else { - Log.e(TAG, - "Error while writing characteristic for $gattInstance") + Log.e(TAG, "Characteristic handle ${request.handle} not found.") throw Status.UNKNOWN.asException() } Empty.getDefaultInstance() @@ -105,12 +108,13 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun discoverServiceByUuid(request: DiscoverServiceByUuidRequest, responseObserver: StreamObserver<DiscoverServicesResponse>) { grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) { - val addr = request.connection.cookie.toByteArray().decodeToString() - val gattInstance = GattInstance.get(addr) + val gattInstance = GattInstance.get(request.connection.cookie) + 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. // This takes between 20s and 28s, and there is no way to know if the service is busy or not. - delay(30000L) + // Delay was originally 30s, but due to flakyness increased to 32s. + delay(32000L) check(gattInstance.mGatt.discoverServiceByUuid(UUID.fromString(request.uuid))) // BluetoothGatt#discoverServiceByUuid does not trigger any callback and does not return // any service, the API was made for PTS testing only. @@ -121,8 +125,8 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun discoverServices(request: DiscoverServicesRequest, responseObserver: StreamObserver<DiscoverServicesResponse>) { grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) { - val addr = request.connection.cookie.toByteArray().decodeToString() - val gattInstance = GattInstance.get(addr) + Log.i(TAG, "discoverServices") + val gattInstance = GattInstance.get(request.connection.cookie) check(gattInstance.mGatt.discoverServices()) gattInstance.waitForDiscoveryEnd() DiscoverServicesResponse.newBuilder() @@ -133,6 +137,7 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun discoverServicesSdp(request: DiscoverServicesSdpRequest, responseObserver: StreamObserver<DiscoverServicesSdpResponse>) { grpcUnary<DiscoverServicesSdpResponse>(mScope, responseObserver) { + Log.i(TAG, "discoverServicesSdp") val bluetoothDevice = request.address.toBluetoothDevice(mBluetoothAdapter) check(bluetoothDevice.fetchUuidsWithSdp()) flow @@ -151,23 +156,72 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun clearCache(request: ClearCacheRequest, responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(mScope, responseObserver) { - val addr = request.connection.cookie.toByteArray().decodeToString() - val gattInstance = GattInstance.get(addr) + Log.i(TAG, "clearCache") + val gattInstance = GattInstance.get(request.connection.cookie) check(gattInstance.mGatt.refresh()) Empty.getDefaultInstance() } } + override fun readCharacteristicFromHandle(request: ReadCharacteristicRequest, + responseObserver: StreamObserver<ReadCharacteristicResponse>) { + grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) { + Log.i(TAG, "readCharacteristicFromHandle handle=${request.handle}") + val gattInstance = GattInstance.get(request.connection.cookie) + val characteristic: BluetoothGattCharacteristic? = + getCharacteristicWithHandle(request.handle, gattInstance) + val readValue: GattInstance.GattInstanceValueRead? + checkNotNull(characteristic) { + "Characteristic handle ${request.handle} not found." + } + readValue = gattInstance.readCharacteristicBlocking(characteristic) + ReadCharacteristicResponse.newBuilder() + .setStatus(AttStatusCode.forNumber(readValue.status)) + .setValue(ByteString.copyFrom(readValue.value)).build() + } + } + + override fun readCharacteristicFromUuid(request: ReadCharacteristicFromUuidRequest, + responseObserver: StreamObserver<ReadCharacteristicResponse>) { + grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) { + Log.i(TAG, "readCharacteristicFromUuid uuid=${request.uuid}") + val gattInstance = GattInstance.get(request.connection.cookie) + tryDiscoverServices(gattInstance) + val readValue = gattInstance.readCharacteristicUuidBlocking(UUID.fromString(request.uuid), + request.startHandle, request.endHandle) + ReadCharacteristicResponse.newBuilder() + .setStatus(AttStatusCode.forNumber(readValue.status)) + .setValue(ByteString.copyFrom(readValue.value)).build() + } + } + + override fun readCharacteristicDescriptorFromHandle(request: ReadCharacteristicDescriptorRequest, + responseObserver: StreamObserver<ReadCharacteristicDescriptorResponse>) { + grpcUnary<ReadCharacteristicDescriptorResponse>(mScope, responseObserver) { + Log.i(TAG, "readCharacteristicDescriptorFromHandle handle=${request.handle}") + val gattInstance = GattInstance.get(request.connection.cookie) + val descriptor: BluetoothGattDescriptor? = + getDescriptorWithHandle(request.handle, gattInstance) + val readValue: GattInstance.GattInstanceValueRead? + checkNotNull(descriptor) { + "Descriptor handle ${request.handle} not found." + } + readValue = gattInstance.readDescriptorBlocking(descriptor) + ReadCharacteristicDescriptorResponse.newBuilder() + .setStatus(AttStatusCode.forNumber(readValue.status)) + .setValue(ByteString.copyFrom(readValue.value)).build() + } + } + + /** + * Discovers services, then returns characteristic with given handle. + * BluetoothGatt API is package-private so we have to redefine it here. + */ private suspend fun getCharacteristicWithHandle(handle: Int, gattInstance: GattInstance): BluetoothGattCharacteristic? { - if (!gattInstance.servicesDiscovered() && !gattInstance.mGatt.discoverServices()) { - Log.e(TAG, "Error on discovering services for $gattInstance") - throw Status.UNKNOWN.asException() - } else { - gattInstance.waitForDiscoveryEnd() - } + tryDiscoverServices(gattInstance) for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) { - for (characteristic : BluetoothGattCharacteristic in service.characteristics) { + for (characteristic: BluetoothGattCharacteristic in service.characteristics) { if (characteristic.instanceId == handle) { return characteristic } @@ -177,6 +231,25 @@ class Gatt(private val context: Context) : GATTImplBase() { } /** + * Discovers services, then returns descriptor with given handle. + * BluetoothGatt API is package-private so we have to redefine it here. + */ + private suspend fun getDescriptorWithHandle(handle: Int, + gattInstance: GattInstance): BluetoothGattDescriptor? { + tryDiscoverServices(gattInstance) + for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) { + for (characteristic: BluetoothGattCharacteristic in service.characteristics) { + for (descriptor: BluetoothGattDescriptor in characteristic.descriptors) { + if (descriptor.getInstanceId() == handle) { + return descriptor + } + } + } + } + return null + } + + /** * Generates a list of GattService from a list of BluetoothGattService. */ private fun generateServicesList(servicesList: List<BluetoothGattService>, dpth: Int) @@ -227,4 +300,13 @@ class Gatt(private val context: Context) : GATTImplBase() { } return newDescriptorsList } + + private suspend fun tryDiscoverServices(gattInstance: GattInstance) { + if (!gattInstance.servicesDiscovered() && !gattInstance.mGatt.discoverServices()) { + Log.e(TAG, "Error on discovering services for $gattInstance") + throw Status.UNKNOWN.asException() + } else { + gattInstance.waitForDiscoveryEnd() + } + } }
\ No newline at end of file diff --git a/android/pandora/server/src/com/android/pandora/GattInstance.kt b/android/pandora/server/src/com/android/pandora/GattInstance.kt index 69edf290fb..93c8799d78 100644 --- a/android/pandora/server/src/com/android/pandora/GattInstance.kt +++ b/android/pandora/server/src/com/android/pandora/GattInstance.kt @@ -19,14 +19,21 @@ package com.android.pandora import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothProfile import android.content.Context import android.util.Log +import com.google.protobuf.ByteString + +import java.util.UUID + import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull /** * GattInstance extends and simplifies Android GATT APIs without re-implementing them. @@ -38,6 +45,17 @@ class GattInstance(val mDevice: BluetoothDevice, val mTransport: Int, val mConte private var mServiceDiscovered = MutableStateFlow(false) private var mConnectionState = MutableStateFlow(BluetoothProfile.STATE_DISCONNECTED) + private var mValueRead = MutableStateFlow(false) + + /** + * Wrapper for characteristic and descriptor reading. + * Uuid, startHandle and endHandle are used to compare with the callback returned object. + * Value and status can be read once the read has been done. + */ + class GattInstanceValueRead(var uuid: UUID?, var startHandle: Int, var endHandle: Int, + var value: ByteArray?, var status: Int) {} + private var mGattInstanceValueRead = GattInstanceValueRead( + null, 0, 0, byteArrayOf(), BluetoothGatt.GATT_FAILURE) companion object GattManager { val gattInstances: MutableMap<String, GattInstance> = mutableMapOf<String, GattInstance>() @@ -48,6 +66,13 @@ class GattInstance(val mDevice: BluetoothDevice, val mTransport: Int, val mConte } return instance } + fun get(address: ByteString): GattInstance { + val instance = gattInstances.get(address.toByteArray().decodeToString()) + requireNotNull(instance) { + "Unable to find GATT instance for $address" + } + return instance + } } private val mCallback = object : BluetoothGattCallback() { @@ -66,6 +91,30 @@ class GattInstance(val mDevice: BluetoothDevice, val mTransport: Int, val mConte mServiceDiscovered.value = true } } + + override fun onCharacteristicRead(bluetoothGatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, value: ByteArray, status: Int) { + Log.i(TAG, "onCharacteristicRead, status: $status") + if (characteristic.getUuid() == mGattInstanceValueRead.uuid + && characteristic.getInstanceId() >= mGattInstanceValueRead.startHandle + && characteristic.getInstanceId() <= mGattInstanceValueRead.endHandle) { + mGattInstanceValueRead.value = value + mGattInstanceValueRead.status = status + mValueRead.value = true + } + } + + override fun onDescriptorRead(bluetoothGatt: BluetoothGatt, + descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray) { + Log.i(TAG, "onDescriptorRead, status: $status") + if (descriptor.getUuid() == mGattInstanceValueRead.uuid + && descriptor.getInstanceId() >= mGattInstanceValueRead.startHandle + && descriptor.getInstanceId() <= mGattInstanceValueRead.endHandle) { + mGattInstanceValueRead.value = value + mGattInstanceValueRead.status = status + mValueRead.value = true + } + } } init { @@ -118,6 +167,52 @@ class GattInstance(val mDevice: BluetoothDevice, val mTransport: Int, val mConte } } + public suspend fun waitForValueReadEnd() { + if (mValueRead.value != true) { + mValueRead.first { it == true } + } + mValueRead.value = false + } + + public suspend fun readCharacteristicBlocking( + characteristic: BluetoothGattCharacteristic): GattInstanceValueRead { + // Init mGattInstanceValueRead with characteristic values. + mGattInstanceValueRead = GattInstanceValueRead( + characteristic.getUuid(), characteristic.getInstanceId(), characteristic.getInstanceId(), + byteArrayOf(), BluetoothGatt.GATT_FAILURE) + if (mGatt.readCharacteristic(characteristic)){ + waitForValueReadEnd() + } + return mGattInstanceValueRead + } + + public suspend fun readCharacteristicUuidBlocking( + uuid: UUID, startHandle: Int, endHandle: Int): GattInstanceValueRead { + // Init mGattInstanceValueRead with characteristic values. + mGattInstanceValueRead = GattInstanceValueRead( + uuid, startHandle, endHandle, byteArrayOf(), BluetoothGatt.GATT_FAILURE) + if (mGatt.readUsingCharacteristicUuid(uuid, startHandle, endHandle)){ + // We have to timeout here as one test will try to read on an inexistant + // characteristic. We don't discover services when reading by uuid so we + // can't check if the characteristic exists beforehand. PTS is also waiting + // for the read to happen so we have to read anyway. + withTimeoutOrNull(1000L) { waitForValueReadEnd() } + } + return mGattInstanceValueRead + } + + public suspend fun readDescriptorBlocking( + descriptor: BluetoothGattDescriptor): GattInstanceValueRead { + // Init mGattInstanceValueRead with descriptor values. + mGattInstanceValueRead = GattInstanceValueRead( + descriptor.getUuid(), descriptor.getInstanceId(), descriptor.getInstanceId(), + byteArrayOf(), BluetoothGatt.GATT_FAILURE) + if (mGatt.readDescriptor(descriptor)){ + waitForValueReadEnd() + } + return mGattInstanceValueRead + } + public fun disconnectInstance() { require(isConnected()) { "Trying to disconnect an already disconnected device $mDevice" diff --git a/framework/java/android/bluetooth/BluetoothAdapter.java b/framework/java/android/bluetooth/BluetoothAdapter.java index dcd2989fe1..043880cc13 100644 --- a/framework/java/android/bluetooth/BluetoothAdapter.java +++ b/framework/java/android/bluetooth/BluetoothAdapter.java @@ -1269,7 +1269,7 @@ public final class BluetoothAdapter { IpcDataCache.invalidateCache(IpcDataCache.MODULE_BLUETOOTH, api); } - private final IpcDataCache.QueryHandler<IBluetooth, Integer> mBluetoothGetStateQuery = + private static final IpcDataCache.QueryHandler<IBluetooth, Integer> sBluetoothGetStateQuery = new IpcDataCache.QueryHandler<>() { @RequiresLegacyBluetoothPermission @RequiresNoPermission @@ -1288,13 +1288,13 @@ public final class BluetoothAdapter { private static final String GET_STATE_API = "BluetoothAdapter_getState"; - private final IpcDataCache<IBluetooth, Integer> mBluetoothGetStateCache = - new BluetoothCache<>(GET_STATE_API, mBluetoothGetStateQuery); + private static final IpcDataCache<IBluetooth, Integer> sBluetoothGetStateCache = + new BluetoothCache<>(GET_STATE_API, sBluetoothGetStateQuery); /** @hide */ @RequiresNoPermission public void disableBluetoothGetStateCache() { - mBluetoothGetStateCache.disableForCurrentProcess(); + sBluetoothGetStateCache.disableForCurrentProcess(); } /** @hide */ @@ -1310,7 +1310,7 @@ public final class BluetoothAdapter { mServiceLock.readLock().lock(); try { if (mService != null) { - return mBluetoothGetStateCache.query(mService); + return sBluetoothGetStateCache.query(mService); } } catch (RuntimeException e) { if (!(e.getCause() instanceof TimeoutException) @@ -2422,7 +2422,7 @@ public final class BluetoothAdapter { } } - private final IpcDataCache.QueryHandler<IBluetooth, Boolean> mBluetoothFilteringQuery = + private static final IpcDataCache.QueryHandler<IBluetooth, Boolean> sBluetoothFilteringQuery = new IpcDataCache.QueryHandler<>() { @RequiresLegacyBluetoothPermission @RequiresNoPermission @@ -2439,13 +2439,13 @@ public final class BluetoothAdapter { private static final String FILTERING_API = "BluetoothAdapter_isOffloadedFilteringSupported"; - private final IpcDataCache<IBluetooth, Boolean> mBluetoothFilteringCache = - new BluetoothCache<>(FILTERING_API, mBluetoothFilteringQuery); + private static final IpcDataCache<IBluetooth, Boolean> sBluetoothFilteringCache = + new BluetoothCache<>(FILTERING_API, sBluetoothFilteringQuery); /** @hide */ @RequiresNoPermission public void disableIsOffloadedFilteringSupportedCache() { - mBluetoothFilteringCache.disableForCurrentProcess(); + sBluetoothFilteringCache.disableForCurrentProcess(); } /** @hide */ @@ -2466,7 +2466,7 @@ public final class BluetoothAdapter { } mServiceLock.readLock().lock(); try { - if (mService != null) return mBluetoothFilteringCache.query(mService); + if (mService != null) return sBluetoothFilteringCache.query(mService); } catch (RuntimeException e) { if (!(e.getCause() instanceof TimeoutException) && !(e.getCause() instanceof RemoteException)) { @@ -2982,8 +2982,8 @@ public final class BluetoothAdapter { return supportedProfiles; } - private final IpcDataCache.QueryHandler<IBluetooth, Integer> - mBluetoothGetAdapterConnectionStateQuery = new IpcDataCache.QueryHandler<>() { + private static final IpcDataCache.QueryHandler<IBluetooth, Integer> + sBluetoothGetAdapterConnectionStateQuery = new IpcDataCache.QueryHandler<>() { @RequiresLegacyBluetoothPermission @RequiresNoPermission @Override @@ -3002,13 +3002,14 @@ public final class BluetoothAdapter { private static final String GET_CONNECTION_API = "BluetoothAdapter_getConnectionState"; - private final IpcDataCache<IBluetooth, Integer> mBluetoothGetAdapterConnectionStateCache = - new BluetoothCache<>(GET_CONNECTION_API, mBluetoothGetAdapterConnectionStateQuery); + private static final IpcDataCache<IBluetooth, Integer> + sBluetoothGetAdapterConnectionStateCache = new BluetoothCache<>(GET_CONNECTION_API, + sBluetoothGetAdapterConnectionStateQuery); /** @hide */ @RequiresNoPermission public void disableGetAdapterConnectionStateCache() { - mBluetoothGetAdapterConnectionStateCache.disableForCurrentProcess(); + sBluetoothGetAdapterConnectionStateCache.disableForCurrentProcess(); } /** @hide */ @@ -3035,7 +3036,7 @@ public final class BluetoothAdapter { } mServiceLock.readLock().lock(); try { - if (mService != null) return mBluetoothGetAdapterConnectionStateCache.query(mService); + if (mService != null) return sBluetoothGetAdapterConnectionStateCache.query(mService); } catch (RuntimeException e) { if (!(e.getCause() instanceof TimeoutException) && !(e.getCause() instanceof RemoteException)) { @@ -3048,8 +3049,8 @@ public final class BluetoothAdapter { return STATE_DISCONNECTED; } - private final IpcDataCache.QueryHandler<Pair<IBluetooth, Integer>, Integer> - mBluetoothProfileQuery = new IpcDataCache.QueryHandler<>() { + private static final IpcDataCache.QueryHandler<Pair<IBluetooth, Integer>, Integer> + sBluetoothProfileQuery = new IpcDataCache.QueryHandler<>() { @RequiresNoPermission @Override public Integer apply(Pair<IBluetooth, Integer> pairQuery) { @@ -3067,15 +3068,16 @@ public final class BluetoothAdapter { private static final String PROFILE_API = "BluetoothAdapter_getProfileConnectionState"; - private final IpcDataCache<Pair<IBluetooth, Integer>, Integer> mGetProfileConnectionStateCache = - new BluetoothCache<>(PROFILE_API, mBluetoothProfileQuery); + private static final IpcDataCache<Pair<IBluetooth, Integer>, Integer> + sGetProfileConnectionStateCache = new BluetoothCache<>(PROFILE_API, + sBluetoothProfileQuery); /** * @hide */ @RequiresNoPermission public void disableGetProfileConnectionStateCache() { - mGetProfileConnectionStateCache.disableForCurrentProcess(); + sGetProfileConnectionStateCache.disableForCurrentProcess(); } /** @@ -3104,7 +3106,7 @@ public final class BluetoothAdapter { mServiceLock.readLock().lock(); try { if (mService != null) { - return mGetProfileConnectionStateCache.query(new Pair<>(mService, profile)); + return sGetProfileConnectionStateCache.query(new Pair<>(mService, profile)); } } catch (RuntimeException e) { if (!(e.getCause() instanceof TimeoutException) diff --git a/framework/java/android/bluetooth/BluetoothDevice.java b/framework/java/android/bluetooth/BluetoothDevice.java index 5c91c9480b..aa60fd1273 100644 --- a/framework/java/android/bluetooth/BluetoothDevice.java +++ b/framework/java/android/bluetooth/BluetoothDevice.java @@ -1903,21 +1903,25 @@ public final class BluetoothDevice implements Parcelable, Attributable { IpcDataCache.invalidateCache(IpcDataCache.MODULE_BLUETOOTH, api); } - private final IpcDataCache.QueryHandler<Pair<IBluetooth, BluetoothDevice>, Integer> - mBluetoothBondQuery = new IpcDataCache.QueryHandler<>() { + private static final IpcDataCache + .QueryHandler<Pair<IBluetooth, Pair<AttributionSource, BluetoothDevice>>, Integer> + sBluetoothBondQuery = new IpcDataCache.QueryHandler<>() { @RequiresLegacyBluetoothPermission @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @Override - public Integer apply(Pair<IBluetooth, BluetoothDevice> pairQuery) { + public Integer apply(Pair<IBluetooth, + Pair<AttributionSource, BluetoothDevice>> pairQuery) { + IBluetooth service = pairQuery.first; + AttributionSource source = pairQuery.second.first; + BluetoothDevice device = pairQuery.second.second; if (DBG) { - log("getConnectionState(" + pairQuery.second.getAnonymizedAddress() - + ") uncached"); + log("getBondState(" + device.getAnonymizedAddress() + ") uncached"); } try { final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get(); - pairQuery.first.getBondState(pairQuery.second, mAttributionSource, recv); + service.getBondState(device, source, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(BOND_NONE); } catch (RemoteException | TimeoutException e) { throw new RuntimeException(e); @@ -1927,12 +1931,13 @@ public final class BluetoothDevice implements Parcelable, Attributable { private static final String GET_BOND_STATE_API = "BluetoothDevice_getBondState"; - private final BluetoothCache<Pair<IBluetooth, BluetoothDevice>, Integer> mBluetoothBondCache = - new BluetoothCache<>(GET_BOND_STATE_API, mBluetoothBondQuery); + private static final + BluetoothCache<Pair<IBluetooth, Pair<AttributionSource, BluetoothDevice>>, Integer> + sBluetoothBondCache = new BluetoothCache<>(GET_BOND_STATE_API, sBluetoothBondQuery); /** @hide */ public void disableBluetoothGetBondStateCache() { - mBluetoothBondCache.disableForCurrentProcess(); + sBluetoothBondCache.disableForCurrentProcess(); } /** @hide */ @@ -1961,7 +1966,8 @@ public final class BluetoothDevice implements Parcelable, Attributable { if (DBG) log(Log.getStackTraceString(new Throwable())); } else { try { - return mBluetoothBondCache.query(new Pair<>(service, BluetoothDevice.this)); + return sBluetoothBondCache.query( + new Pair<>(service, new Pair<>(mAttributionSource, BluetoothDevice.this))); } catch (RuntimeException e) { if (!(e.getCause() instanceof TimeoutException) && !(e.getCause() instanceof RemoteException)) { diff --git a/framework/java/android/bluetooth/BluetoothMap.java b/framework/java/android/bluetooth/BluetoothMap.java index 2d4bc568ca..9e606f9d2e 100644 --- a/framework/java/android/bluetooth/BluetoothMap.java +++ b/framework/java/android/bluetooth/BluetoothMap.java @@ -367,7 +367,7 @@ public final class BluetoothMap implements BluetoothProfile, AutoCloseable { /** @hide */ public void disableBluetoothGetConnectionStateCache() { - mBluetoothConnectionCache.disableForCurrentProcess(); + sBluetoothConnectionCache.disableForCurrentProcess(); } /** @hide */ @@ -383,20 +383,23 @@ public final class BluetoothMap implements BluetoothProfile, AutoCloseable { IpcDataCache.invalidateCache(IpcDataCache.MODULE_BLUETOOTH, api); } - private final IpcDataCache.QueryHandler<Pair<IBluetoothMap, BluetoothDevice>, Integer> - mBluetoothConnectionQuery = new IpcDataCache.QueryHandler<>() { + private static final IpcDataCache + .QueryHandler<Pair<IBluetoothMap, Pair<AttributionSource, BluetoothDevice>>, Integer> + sBluetoothConnectionQuery = new IpcDataCache.QueryHandler<>() { @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @Override - public Integer apply(Pair<IBluetoothMap, BluetoothDevice> pairQuery) { + public Integer apply(Pair<IBluetoothMap, + Pair<AttributionSource, BluetoothDevice>> pairQuery) { + IBluetoothMap service = pairQuery.first; + AttributionSource source = pairQuery.second.first; + BluetoothDevice device = pairQuery.second.second; if (DBG) { - log("getConnectionState(" + pairQuery.second.getAnonymizedAddress() - + ") uncached"); + log("getConnectionState(" + device.getAnonymizedAddress() + ") uncached"); } final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get(); try { - pairQuery.first - .getConnectionState(pairQuery.second, mAttributionSource, recv); + service.getConnectionState(device, source, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()) .getValue(BluetoothProfile.STATE_DISCONNECTED); } catch (RemoteException | TimeoutException e) { @@ -407,9 +410,10 @@ public final class BluetoothMap implements BluetoothProfile, AutoCloseable { private static final String GET_CONNECTION_STATE_API = "BluetoothMap_getConnectionState"; - private final BluetoothCache<Pair<IBluetoothMap, BluetoothDevice>, Integer> - mBluetoothConnectionCache = new BluetoothCache<>(GET_CONNECTION_STATE_API, - mBluetoothConnectionQuery); + private static final + BluetoothCache<Pair<IBluetoothMap, Pair<AttributionSource, BluetoothDevice>>, Integer> + sBluetoothConnectionCache = new BluetoothCache<>(GET_CONNECTION_STATE_API, + sBluetoothConnectionQuery); /** * Get connection state of device @@ -427,7 +431,8 @@ public final class BluetoothMap implements BluetoothProfile, AutoCloseable { if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled() && isValidDevice(device)) { try { - return mBluetoothConnectionCache.query(new Pair<>(service, device)); + return sBluetoothConnectionCache.query( + new Pair<>(service, new Pair<>(mAttributionSource, device))); } catch (RuntimeException e) { if (!(e.getCause() instanceof TimeoutException) && !(e.getCause() instanceof RemoteException)) { diff --git a/framework/java/android/bluetooth/BluetoothSap.java b/framework/java/android/bluetooth/BluetoothSap.java index 137c25b29e..774df33d79 100644 --- a/framework/java/android/bluetooth/BluetoothSap.java +++ b/framework/java/android/bluetooth/BluetoothSap.java @@ -364,7 +364,7 @@ public final class BluetoothSap implements BluetoothProfile, AutoCloseable { /** @hide */ public void disableBluetoothGetConnectionStateCache() { - mBluetoothConnectionCache.disableForCurrentProcess(); + sBluetoothConnectionCache.disableForCurrentProcess(); } /** @hide */ @@ -380,20 +380,23 @@ public final class BluetoothSap implements BluetoothProfile, AutoCloseable { IpcDataCache.invalidateCache(IpcDataCache.MODULE_BLUETOOTH, api); } - private final IpcDataCache.QueryHandler<Pair<IBluetoothSap, BluetoothDevice>, Integer> - mBluetoothConnectionQuery = new IpcDataCache.QueryHandler<>() { + private static final IpcDataCache + .QueryHandler<Pair<IBluetoothSap, Pair<AttributionSource, BluetoothDevice>>, Integer> + sBluetoothConnectionQuery = new IpcDataCache.QueryHandler<>() { @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @Override - public Integer apply(Pair<IBluetoothSap, BluetoothDevice> pairQuery) { + public Integer apply(Pair<IBluetoothSap, + Pair<AttributionSource, BluetoothDevice>> pairQuery) { + IBluetoothSap service = pairQuery.first; + AttributionSource source = pairQuery.second.first; + BluetoothDevice device = pairQuery.second.second; if (DBG) { - log("getConnectionState(" + pairQuery.second.getAnonymizedAddress() - + ") uncached"); + log("getConnectionState(" + device.getAnonymizedAddress() + ") uncached"); } final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get(); try { - pairQuery.first - .getConnectionState(pairQuery.second, mAttributionSource, recv); + service.getConnectionState(device, source, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()) .getValue(BluetoothProfile.STATE_DISCONNECTED); } catch (RemoteException | TimeoutException e) { @@ -404,9 +407,10 @@ public final class BluetoothSap implements BluetoothProfile, AutoCloseable { private static final String GET_CONNECTION_STATE_API = "BluetoothSap_getConnectionState"; - private final BluetoothCache<Pair<IBluetoothSap, BluetoothDevice>, Integer> - mBluetoothConnectionCache = new BluetoothCache<>(GET_CONNECTION_STATE_API, - mBluetoothConnectionQuery); + private static final + BluetoothCache<Pair<IBluetoothSap, Pair<AttributionSource, BluetoothDevice>>, Integer> + sBluetoothConnectionCache = new BluetoothCache<>(GET_CONNECTION_STATE_API, + sBluetoothConnectionQuery); /** * Get connection state of device @@ -424,7 +428,8 @@ public final class BluetoothSap implements BluetoothProfile, AutoCloseable { if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled() && isValidDevice(device)) { try { - return mBluetoothConnectionCache.query(new Pair<>(service, device)); + return sBluetoothConnectionCache.query( + new Pair<>(service, new Pair<>(mAttributionSource, device))); } catch (RuntimeException e) { if (!(e.getCause() instanceof TimeoutException) && !(e.getCause() instanceof RemoteException)) { diff --git a/service/Android.bp b/service/Android.bp index 2a5e1adef4..c43ac23bfd 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -68,6 +68,7 @@ java_library { "modules-utils-shell-command-handler", "bluetooth-manager-service-proto-java-gen", "bluetooth-proto-enums-java-gen", + "bluetooth-nano-protos", ], apex_available: [ @@ -128,6 +129,23 @@ java_library { min_sdk_version: "Tiramisu" } +java_library { + name: "bluetooth-nano-protos", + sdk_version: "system_current", + min_sdk_version: "Tiramisu", + proto: { + type: "nano", + }, + srcs: [ + ":system-messages-proto-src", + ], + libs: ["libprotobuf-java-nano"], + apex_available: [ + "com.android.bluetooth", + ], + lint: { strict_updatability_linting: true }, +} + platform_compat_config { name: "bluetooth-compat-config", diff --git a/service/java/com/android/server/bluetooth/BluetoothDeviceConfigChangeTracker.java b/service/java/com/android/server/bluetooth/BluetoothDeviceConfigChangeTracker.java new file mode 100644 index 0000000000..a035338b4f --- /dev/null +++ b/service/java/com/android/server/bluetooth/BluetoothDeviceConfigChangeTracker.java @@ -0,0 +1,85 @@ +/* + * 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.server.bluetooth; + +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * The BluetoothDeviceConfigChangeTracker receives changes to the DeviceConfig for + * NAMESPACE_BLUETOOTH, and determines whether we should queue a restart, if any Bluetooth-related + * INIT_ flags have been changed. + * + * <p>The initialProperties should be fetched from the BLUETOOTH namespace in DeviceConfig + */ +public final class BluetoothDeviceConfigChangeTracker { + private static final String TAG = "BluetoothDeviceConfigChangeTracker"; + + private final HashMap<String, String> mCurrFlags; + + public BluetoothDeviceConfigChangeTracker(Properties initialProperties) { + mCurrFlags = getFlags(initialProperties); + } + + /** + * Updates the instance state tracking the latest init flag values, and determines whether an + * init flag has changed (requiring a restart at some point) + */ + public boolean shouldRestartWhenPropertiesUpdated(Properties newProperties) { + if (!newProperties.getNamespace().equals(DeviceConfig.NAMESPACE_BLUETOOTH)) { + return false; + } + ArrayList<String> flags = new ArrayList<>(); + for (String name : newProperties.getKeyset()) { + flags.add(name + "='" + newProperties.getString(name, "") + "'"); + } + Log.d(TAG, "shouldRestartWhenPropertiesUpdated: " + String.join(",", flags)); + boolean shouldRestart = false; + for (String name : newProperties.getKeyset()) { + if (!isInitFlag(name)) { + continue; + } + var oldValue = mCurrFlags.get(name); + var newValue = newProperties.getString(name, ""); + if (newValue.equals(oldValue)) { + continue; + } + Log.d(TAG, "Property " + name + " changed from " + oldValue + " -> " + newValue); + mCurrFlags.put(name, newValue); + shouldRestart = true; + } + return shouldRestart; + } + + private HashMap<String, String> getFlags(Properties initialProperties) { + var out = new HashMap(); + for (var name : initialProperties.getKeyset()) { + if (isInitFlag(name)) { + out.put(name, initialProperties.getString(name, "")); + } + } + return out; + } + + private Boolean isInitFlag(String flagName) { + return flagName.startsWith("INIT_"); + } +} diff --git a/service/java/com/android/server/bluetooth/BluetoothDeviceConfigListener.java b/service/java/com/android/server/bluetooth/BluetoothDeviceConfigListener.java index 0c6eac13e2..6b985a7bc5 100644 --- a/service/java/com/android/server/bluetooth/BluetoothDeviceConfigListener.java +++ b/service/java/com/android/server/bluetooth/BluetoothDeviceConfigListener.java @@ -24,8 +24,6 @@ import android.provider.DeviceConfig; import android.provider.Settings; import android.util.Log; -import java.util.ArrayList; - /** * The BluetoothDeviceConfigListener handles system device config change callback and checks * whether we need to inform BluetoothManagerService on this change. @@ -38,11 +36,13 @@ import java.util.ArrayList; public class BluetoothDeviceConfigListener { private static final String TAG = "BluetoothDeviceConfigListener"; + private static final int DEFAULT_APM_ENHANCEMENT = 0; + private static final int DEFAULT_BT_APM_STATE = 0; + private final BluetoothManagerService mService; private final boolean mLogDebug; private final Context mContext; - private static final int DEFAULT_APM_ENHANCEMENT = 0; - private static final int DEFAULT_BT_APM_STATE = 0; + private final BluetoothDeviceConfigChangeTracker mConfigChangeTracker; private boolean mPrevApmEnhancement; private boolean mPrevBtApmState; @@ -56,6 +56,9 @@ public class BluetoothDeviceConfigListener { APM_ENHANCEMENT, DEFAULT_APM_ENHANCEMENT) == 1; mPrevBtApmState = Settings.Global.getInt(mContext.getContentResolver(), BT_DEFAULT_APM_STATE, DEFAULT_BT_APM_STATE) == 1; + mConfigChangeTracker = + new BluetoothDeviceConfigChangeTracker( + DeviceConfig.getProperties(DeviceConfig.NAMESPACE_BLUETOOTH)); DeviceConfig.addOnPropertiesChangedListener( DeviceConfig.NAMESPACE_BLUETOOTH, (Runnable r) -> r.run(), @@ -65,19 +68,8 @@ public class BluetoothDeviceConfigListener { private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener = new DeviceConfig.OnPropertiesChangedListener() { @Override - public void onPropertiesChanged(DeviceConfig.Properties properties) { - if (!properties.getNamespace().equals(DeviceConfig.NAMESPACE_BLUETOOTH)) { - return; - } - if (mLogDebug) { - ArrayList<String> flags = new ArrayList<>(); - for (String name : properties.getKeyset()) { - flags.add(name + "='" + properties.getString(name, "") + "'"); - } - Log.d(TAG, "onPropertiesChanged: " + String.join(",", flags)); - } - - boolean apmEnhancement = properties.getBoolean( + public void onPropertiesChanged(DeviceConfig.Properties newProperties) { + boolean apmEnhancement = newProperties.getBoolean( APM_ENHANCEMENT, mPrevApmEnhancement); if (apmEnhancement != mPrevApmEnhancement) { mPrevApmEnhancement = apmEnhancement; @@ -85,24 +77,20 @@ public class BluetoothDeviceConfigListener { APM_ENHANCEMENT, apmEnhancement ? 1 : 0); } - boolean btApmState = properties.getBoolean( + boolean btApmState = newProperties.getBoolean( BT_DEFAULT_APM_STATE, mPrevBtApmState); if (btApmState != mPrevBtApmState) { mPrevBtApmState = btApmState; Settings.Global.putInt(mContext.getContentResolver(), BT_DEFAULT_APM_STATE, btApmState ? 1 : 0); } - boolean foundInit = false; - for (String name : properties.getKeyset()) { - if (name.startsWith("INIT_")) { - foundInit = true; - break; - } - } - if (!foundInit) { - return; + + if (mConfigChangeTracker.shouldRestartWhenPropertiesUpdated(newProperties)) { + Log.d(TAG, "Properties changed, enqueuing restart"); + mService.onInitFlagsChanged(); + } else { + Log.d(TAG, "All properties unchanged, skipping restart"); } - mService.onInitFlagsChanged(); } }; } diff --git a/service/java/com/android/server/bluetooth/BluetoothNotificationManager.java b/service/java/com/android/server/bluetooth/BluetoothNotificationManager.java index d2abde092b..d6883824e4 100644 --- a/service/java/com/android/server/bluetooth/BluetoothNotificationManager.java +++ b/service/java/com/android/server/bluetooth/BluetoothNotificationManager.java @@ -26,6 +26,8 @@ import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.util.Log; +import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; + import java.util.ArrayList; import java.util.List; @@ -38,9 +40,6 @@ public class BluetoothNotificationManager { public static final String APM_NOTIFICATION_CHANNEL = "apm_notification_channel"; private static final String APM_NOTIFICATION_GROUP = "apm_notification_group"; - //TODO: b/239983569 remove hardcoded notification ID - private static final int NOTE_BT_APM_NOTIFICATION = -1000007; - private final Context mContext; private NotificationManager mNotificationManager; @@ -137,7 +136,7 @@ public class BluetoothNotificationManager { .setStyle(new Notification.BigTextStyle().bigText(message)) .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) .build(); - notify(NOTE_BT_APM_NOTIFICATION, notification); + notify(SystemMessage.NOTE_BT_APM_NOTIFICATION, notification); } /** diff --git a/service/tests/Android.bp b/service/tests/Android.bp index 3b5e866262..3ac0253278 100644 --- a/service/tests/Android.bp +++ b/service/tests/Android.bp @@ -32,6 +32,8 @@ android_test { "androidx.test.rules", "mockito-target-extended-minus-junit4", "platform-test-annotations", + "frameworks-base-testutils", + "truth-prebuilt", // Statically link service-bluetooth-pre-jarjar since we want to test the working copy of // service-uwb, not the on-device copy. diff --git a/service/tests/src/com/android/server/BluetoothDeviceConfigChangeTrackerTest.java b/service/tests/src/com/android/server/BluetoothDeviceConfigChangeTrackerTest.java new file mode 100644 index 0000000000..489a574168 --- /dev/null +++ b/service/tests/src/com/android/server/BluetoothDeviceConfigChangeTrackerTest.java @@ -0,0 +1,167 @@ +/* + * 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.server.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class BluetoothDeviceConfigChangeTrackerTest { + @Test + public void testNoProperties() { + BluetoothDeviceConfigChangeTracker changeTracker = + new BluetoothDeviceConfigChangeTracker( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH).build()); + + boolean shouldRestart = + changeTracker.shouldRestartWhenPropertiesUpdated( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH).build()); + + assertThat(shouldRestart).isFalse(); + } + + @Test + public void testNewFlag() { + BluetoothDeviceConfigChangeTracker changeTracker = + new BluetoothDeviceConfigChangeTracker( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("INIT_a", "true") + .build()); + + boolean shouldRestart = + changeTracker.shouldRestartWhenPropertiesUpdated( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("INIT_a", "true") + .setString("INIT_b", "true") + .build()); + + assertThat(shouldRestart).isTrue(); + } + + @Test + public void testChangedFlag() { + BluetoothDeviceConfigChangeTracker changeTracker = + new BluetoothDeviceConfigChangeTracker( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("INIT_a", "true") + .build()); + + boolean shouldRestart = + changeTracker.shouldRestartWhenPropertiesUpdated( + new Properties.Builder("another_namespace") + .setString("INIT_a", "false") + .build()); + + assertThat(shouldRestart).isTrue(); + } + + @Test + public void testUnchangedInitFlag() { + BluetoothDeviceConfigChangeTracker changeTracker = + new BluetoothDeviceConfigChangeTracker( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("INIT_a", "true") + .build()); + + boolean shouldRestart = + changeTracker.shouldRestartWhenPropertiesUpdated( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("INIT_a", "true") + .build()); + + assertThat(shouldRestart).isFalse(); + } + + @Test + public void testRepeatedChangeInitFlag() { + BluetoothDeviceConfigChangeTracker changeTracker = + new BluetoothDeviceConfigChangeTracker( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH).build()); + + changeTracker.shouldRestartWhenPropertiesUpdated( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("INIT_a", "true") + .build()); + + boolean shouldRestart = + changeTracker.shouldRestartWhenPropertiesUpdated( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("INIT_a", "true") + .build()); + + assertThat(shouldRestart).isFalse(); + } + + @Test + public void testWrongNamespace() { + BluetoothDeviceConfigChangeTracker changeTracker = + new BluetoothDeviceConfigChangeTracker( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH).build()); + + boolean shouldRestart = + changeTracker.shouldRestartWhenPropertiesUpdated( + new Properties.Builder("another_namespace") + .setString("INIT_a", "true") + .build()); + + assertThat(shouldRestart).isFalse(); + } + + @Test + public void testSkipProperty() { + BluetoothDeviceConfigChangeTracker changeTracker = + new BluetoothDeviceConfigChangeTracker( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("INIT_a", "true") + .setString("INIT_b", "false") + .build()); + + boolean shouldRestart = + changeTracker.shouldRestartWhenPropertiesUpdated( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("INIT_b", "false") + .build()); + + assertThat(shouldRestart).isFalse(); + } + + @Test + public void testNonInitFlag() { + BluetoothDeviceConfigChangeTracker changeTracker = + new BluetoothDeviceConfigChangeTracker( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("a", "true") + .build()); + + boolean shouldRestart = + changeTracker.shouldRestartWhenPropertiesUpdated( + new Properties.Builder(DeviceConfig.NAMESPACE_BLUETOOTH) + .setString("a", "false") + .build()); + + assertThat(shouldRestart).isFalse(); + } +} diff --git a/system/bta/Android.bp b/system/bta/Android.bp index f9640eb173..a24166a980 100644 --- a/system/bta/Android.bp +++ b/system/bta/Android.bp @@ -99,6 +99,7 @@ cc_library_static { "le_audio/state_machine.cc", "le_audio/client_parser.cc", "le_audio/client_audio.cc", + "le_audio/le_audio_utils.cc", "le_audio/le_audio_set_configuration_provider.cc", "le_audio/le_audio_set_configuration_provider_json.cc", "le_audio/le_audio_types.cc", @@ -629,6 +630,7 @@ cc_test { "le_audio/client_parser.cc", "le_audio/content_control_id_keeper.cc", "le_audio/devices.cc", + "le_audio/le_audio_utils.cc", "le_audio/le_audio_client_test.cc", "le_audio/le_audio_set_configuration_provider_json.cc", "le_audio/le_audio_types.cc", @@ -772,7 +774,9 @@ cc_test { "le_audio/broadcaster/mock_state_machine.cc", "le_audio/content_control_id_keeper.cc", "le_audio/client_audio.cc", + "le_audio/le_audio_utils.cc", "le_audio/le_audio_types.cc", + "le_audio/metrics_collector_linux.cc", "le_audio/mock_iso_manager.cc", "test/common/mock_controller.cc", "le_audio/mock_codec_manager.cc", diff --git a/system/bta/ag/bta_ag_act.cc b/system/bta/ag/bta_ag_act.cc index 1ba68bcc44..1fbf17e9bb 100644 --- a/system/bta/ag/bta_ag_act.cc +++ b/system/bta/ag/bta_ag_act.cc @@ -471,7 +471,7 @@ void bta_ag_rfc_open(tBTA_AG_SCB* p_scb, const tBTA_AG_DATA& data) { bool sdp_wbs_support = p_scb->peer_sdp_features & BTA_AG_FEAT_WBS_SUPPORT; if (!p_scb->received_at_bac && sdp_wbs_support) { p_scb->codec_updated = true; - p_scb->peer_codecs = BTM_SCO_CODEC_CVSD & BTM_SCO_CODEC_MSBC; + p_scb->peer_codecs = BTM_SCO_CODEC_CVSD | BTM_SCO_CODEC_MSBC; p_scb->sco_codec = UUID_CODEC_MSBC; } } else { diff --git a/system/bta/ag/bta_ag_sdp.cc b/system/bta/ag/bta_ag_sdp.cc index bb39a43226..01dd35303f 100644 --- a/system/bta/ag/bta_ag_sdp.cc +++ b/system/bta/ag/bta_ag_sdp.cc @@ -369,7 +369,7 @@ bool bta_ag_sdp_find_attr(tBTA_AG_SCB* p_scb, tBTA_SERVICE_MASK service) { // 2. But do not send required AT+BAC command // Will assume mSBC is enabled and try codec negotiation by default p_scb->codec_updated = true; - p_scb->peer_codecs = BTM_SCO_CODEC_CVSD & BTM_SCO_CODEC_MSBC; + p_scb->peer_codecs = BTM_SCO_CODEC_CVSD | BTM_SCO_CODEC_MSBC; p_scb->sco_codec = UUID_CODEC_MSBC; } if (sdp_features != p_scb->peer_sdp_features) { diff --git a/system/bta/csis/csis_client.cc b/system/bta/csis/csis_client.cc index 9ae6450cdc..2e1d0d5d1f 100644 --- a/system/bta/csis/csis_client.cc +++ b/system/bta/csis/csis_client.cc @@ -1224,7 +1224,7 @@ class CsisClientImpl : public CsisClient { auto csis_device = FindDeviceByAddress(result->bd_addr); if (csis_device) { - DLOG(INFO) << __func__ << " Drop same device .." << result->bd_addr; + LOG_DEBUG("Drop known device %s", result->bd_addr.ToString().c_str()); return; } @@ -1235,8 +1235,15 @@ class CsisClientImpl : public CsisClient { for (auto& group : csis_groups_) { for (auto& rsi : all_rsi) { if (group->IsRsiMatching(rsi)) { - DLOG(INFO) << " Found set member in background scan " - << result->bd_addr; + LOG_INFO("Device %s match to group id %d", + result->bd_addr.ToString().c_str(), group->GetGroupId()); + if (group->GetDesiredSize() > 0 && + (group->GetCurrentSize() == group->GetDesiredSize())) { + LOG_WARN( + "Group is already completed. Some other device use same SIRK"); + break; + } + callbacks_->OnSetMemberAvailable(result->bd_addr, group->GetGroupId()); break; diff --git a/system/bta/le_audio/audio_set_configurations.json b/system/bta/le_audio/audio_set_configurations.json index 4adda2edd4..b887ef4e3c 100644 --- a/system/bta/le_audio/audio_set_configurations.json +++ b/system/bta/le_audio/audio_set_configurations.json @@ -168,6 +168,11 @@ "qos_config_name": ["QoS_Config_16_1_2"] }, { + "name": "DualDev_OneChanMonoSnk_16_2_Server_Preferred", + "codec_config_name": "DualDev_OneChanMonoSnk_16_2", + "qos_config_name": ["QoS_Config_Server_Preferred"] + }, + { "name": "SingleDev_OneChanMonoSnk_16_2_Server_Preferred", "codec_config_name": "SingleDev_OneChanMonoSnk_16_2", "qos_config_name": ["QoS_Config_Server_Preferred"] @@ -1694,6 +1699,73 @@ ] }, { + "name": "DualDev_OneChanMonoSnk_16_2", + "subconfigurations": [ + { + "device_cnt": 2, + "ase_cnt": 2, + "direction": "SINK", + "configuration_strategy": "MONO_ONE_CIS_PER_DEVICE", + "codec_id": { + "coding_format": 6, + "vendor_company_id": 0, + "vendor_codec_id": 0 + }, + "codec_configuration": [ + { + "name": "sampling_frequency", + "type": 1, + "compound_value": { + "value": [ + 3 + ] + } + }, + { + "name": "frame_duration", + "type": 2, + "compound_value": { + "value": [ + 1 + ] + } + }, + { + "name": "audio_channel_allocation", + "type": 3, + "compound_value": { + "value": [ + 1, + 0, + 0, + 0 + ] + } + }, + { + "name": "octets_per_codec_frame", + "type": 4, + "compound_value": { + "value": [ + 40, + 0 + ] + } + }, + { + "name": "codec_frame_blocks_per_sdu", + "type": 5, + "compound_value": { + "value": [ + 1 + ] + } + } + ] + } + ] + }, + { "name": "SingleDev_OneChanMonoSnk_16_2", "subconfigurations": [ { diff --git a/system/bta/le_audio/audio_set_scenarios.json b/system/bta/le_audio/audio_set_scenarios.json index 58ad4f532b..d170ad1fbb 100644 --- a/system/bta/le_audio/audio_set_scenarios.json +++ b/system/bta/le_audio/audio_set_scenarios.json @@ -37,7 +37,10 @@ "SingleDev_OneChanMonoSnk_16_2_Server_Preferred", "SingleDev_OneChanMonoSnk_16_2_1", "SingleDev_OneChanMonoSnk_16_1_Server_Preferred", - "SingleDev_OneChanMonoSnk_16_1_1" + "SingleDev_OneChanMonoSnk_16_1_1", + "DualDev_OneChanMonoSrc_16_2_Server_Preferred", + "SingleDev_OneChanStereoSrc_16_2_Server_Preferred", + "SingleDev_OneChanMonoSrc_16_2_Server_Preferred" ] }, { @@ -102,7 +105,10 @@ "SingleDev_OneChanMonoSrc_16_2_Server_Preferred", "SingleDev_OneChanMonoSrc_16_1_Server_Preferred", "VND_SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32khz_Server_Prefered_1", - "VND_SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32khz_60oct_R3_L22_1" + "VND_SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32khz_60oct_R3_L22_1", + "DualDev_OneChanMonoSnk_16_2_Server_Preferred", + "SingleDev_OneChanStereoSnk_16_2_Server_Preferred", + "SingleDev_OneChanMonoSnk_16_2_Server_Preferred" ] }, { @@ -175,7 +181,10 @@ "VND_SingleDev_TwoChanStereoSnk_48khz_100octs_Server_Preferred_1", "VND_SingleDev_TwoChanStereoSnk_48khz_100octs_R15_L70_1", "VND_SingleDev_OneChanStereoSnk_48khz_100octs_Server_Preferred_1", - "VND_SingleDev_OneChanStereoSnk_48khz_100octs_R15_L70_1" + "VND_SingleDev_OneChanStereoSnk_48khz_100octs_R15_L70_1", + "DualDev_OneChanMonoSrc_16_2_Server_Preferred", + "SingleDev_OneChanStereoSrc_16_2_Server_Preferred", + "SingleDev_OneChanMonoSrc_16_2_Server_Preferred" ] }, { diff --git a/system/bta/le_audio/broadcaster/broadcaster.cc b/system/bta/le_audio/broadcaster/broadcaster.cc index 605f485aab..3b0a7fcec9 100644 --- a/system/bta/le_audio/broadcaster/broadcaster.cc +++ b/system/bta/le_audio/broadcaster/broadcaster.cc @@ -20,8 +20,9 @@ #include "bta/include/bta_le_audio_api.h" #include "bta/include/bta_le_audio_broadcaster_api.h" #include "bta/le_audio/broadcaster/state_machine.h" -#include "bta/le_audio/content_control_id_keeper.h" #include "bta/le_audio/le_audio_types.h" +#include "bta/le_audio/le_audio_utils.h" +#include "bta/le_audio/metrics_collector.h" #include "device/include/controller.h" #include "embdrv/lc3/include/lc3.h" #include "gd/common/strings.h" @@ -38,7 +39,6 @@ using bluetooth::hci::iso_manager::BigCallbacks; using bluetooth::le_audio::BasicAudioAnnouncementData; using bluetooth::le_audio::BroadcastId; using le_audio::CodecManager; -using le_audio::ContentControlIdKeeper; using le_audio::broadcaster::BigConfig; using le_audio::broadcaster::BroadcastCodecWrapper; using le_audio::broadcaster::BroadcastQosConfig; @@ -49,6 +49,8 @@ using le_audio::types::CodecLocation; using le_audio::types::kLeAudioCodingFormatLC3; using le_audio::types::LeAudioContextType; using le_audio::types::LeAudioLtvMap; +using le_audio::utils::GetAllCcids; +using le_audio::utils::GetAllowedAudioContextsFromSourceMetadata; namespace { class LeAudioBroadcasterImpl; @@ -165,6 +167,80 @@ class LeAudioBroadcasterImpl : public LeAudioBroadcaster, public BigCallbacks { return announcement; } + void UpdateStreamingContextTypeOnAllSubgroups(uint16_t context_type_map) { + LOG_DEBUG("%s context_type_map=%d", __func__, context_type_map); + + auto ccids = GetAllCcids(context_type_map); + if (ccids.empty()) { + LOG_WARN("%s No content providers available for context_type_map=%d.", + __func__, context_type_map); + } + + std::vector<uint8_t> stream_context_vec(2); + auto pp = stream_context_vec.data(); + UINT16_TO_STREAM(pp, context_type_map); + + for (auto const& kv_it : broadcasts_) { + auto& broadcast = kv_it.second; + if (broadcast->GetState() == BroadcastStateMachine::State::STREAMING) { + auto announcement = broadcast->GetBroadcastAnnouncement(); + bool broadcast_update = false; + + // Replace context type and CCID list + for (auto& subgroup : announcement.subgroup_configs) { + auto subgroup_ltv = LeAudioLtvMap(subgroup.metadata); + bool subgroup_update = false; + + auto existing_context = subgroup_ltv.Find( + le_audio::types::kLeAudioMetadataTypeStreamingAudioContext); + if (existing_context) { + if (memcmp(stream_context_vec.data(), existing_context->data(), + existing_context->size()) != 0) { + subgroup_ltv.Add( + le_audio::types::kLeAudioMetadataTypeStreamingAudioContext, + stream_context_vec); + subgroup_update = true; + } + } else { + subgroup_ltv.Add( + le_audio::types::kLeAudioMetadataTypeStreamingAudioContext, + stream_context_vec); + subgroup_update = true; + } + + auto existing_ccid_list = + subgroup_ltv.Find(le_audio::types::kLeAudioMetadataTypeCcidList); + if (existing_ccid_list) { + if (ccids.empty()) { + subgroup_ltv.Remove( + le_audio::types::kLeAudioMetadataTypeCcidList); + subgroup_update = true; + + } else if (!std::is_permutation(ccids.begin(), ccids.end(), + existing_ccid_list->begin())) { + subgroup_ltv.Add(le_audio::types::kLeAudioMetadataTypeCcidList, + ccids); + subgroup_update = true; + } + } else if (!ccids.empty()) { + subgroup_ltv.Add(le_audio::types::kLeAudioMetadataTypeCcidList, + ccids); + subgroup_update = true; + } + + if (subgroup_update) { + subgroup.metadata = subgroup_ltv.Values(); + broadcast_update = true; + } + } + + if (broadcast_update) { + broadcast->UpdateBroadcastAnnouncement(std::move(announcement)); + } + } + } + } + void UpdateMetadata(uint32_t broadcast_id, std::vector<uint8_t> metadata) override { if (broadcasts_.count(broadcast_id) == 0) { @@ -185,6 +261,22 @@ class LeAudioBroadcasterImpl : public LeAudioBroadcaster, public BigCallbacks { return; } + uint16_t context_type = + static_cast<std::underlying_type<LeAudioContextType>::type>( + LeAudioContextType::MEDIA); + auto stream_context_vec = + ltv.Find(le_audio::types::kLeAudioMetadataTypeStreamingAudioContext); + if (stream_context_vec) { + auto pp = stream_context_vec.value().data(); + STREAM_TO_UINT16(context_type, pp); + } + + // Append the CCID list + auto ccid_vec = GetAllCcids(context_type); + if (!ccid_vec.empty()) { + ltv.Add(le_audio::types::kLeAudioMetadataTypeCcidList, ccid_vec); + } + BasicAudioAnnouncementData announcement = prepareAnnouncement(codec_config, std::move(ltv)); @@ -219,12 +311,9 @@ class LeAudioBroadcasterImpl : public LeAudioBroadcaster, public BigCallbacks { } // Append the CCID list - // TODO: We currently support only one context (and CCID) at a time for both - // Unicast and broadcast. - auto ccid = ContentControlIdKeeper::GetInstance()->GetCcid(context_type); - if (ccid != -1) { - ltv.Add(le_audio::types::kLeAudioMetadataTypeCcidList, - {static_cast<uint8_t>(ccid)}); + auto ccid_vec = GetAllCcids(context_type); + if (!ccid_vec.empty()) { + ltv.Add(le_audio::types::kLeAudioMetadataTypeCcidList, ccid_vec); } if (CodecManager::GetInstance()->GetCodecLocation() == @@ -329,6 +418,7 @@ class LeAudioBroadcasterImpl : public LeAudioBroadcaster, public BigCallbacks { broadcasts_[broadcast_id]->ProcessMessage( BroadcastStateMachine::Message::START, nullptr); + le_audio::MetricsCollector::Get()->OnBroadcastStateChanged(true); } else { LOG_ERROR("No such broadcast_id=%d", broadcast_id); } @@ -346,6 +436,7 @@ class LeAudioBroadcasterImpl : public LeAudioBroadcaster, public BigCallbacks { broadcasts_[broadcast_id]->SetMuted(true); broadcasts_[broadcast_id]->ProcessMessage( BroadcastStateMachine::Message::STOP, nullptr); + le_audio::MetricsCollector::Get()->OnBroadcastStateChanged(false); } void DestroyAudioBroadcast(uint32_t broadcast_id) override { @@ -744,11 +835,24 @@ class LeAudioBroadcasterImpl : public LeAudioBroadcaster, public BigCallbacks { const source_metadata_t& source_metadata) override { LOG_INFO(); if (!instance) return; + + /* TODO: Should we take supported contexts from ASCS? */ + auto supported_context_types = le_audio::types::kLeAudioContextAllTypes; + auto contexts = GetAllowedAudioContextsFromSourceMetadata( + source_metadata, + static_cast<std::underlying_type<LeAudioContextType>::type>( + supported_context_types)); + if (contexts.any()) { + /* NOTICE: We probably don't want to change the stream configuration + * on each metadata change, so just update the context type metadata. + * Since we are not able to identify individual track streams and + * they are all mixed inside a single data stream, we will update + * the metadata of all BIS subgroups with the same combined context. + */ + instance->UpdateStreamingContextTypeOnAllSubgroups(contexts.to_ulong()); + } + do_update_metadata_promise.set_value(); - /* TODO: We probably don't want to change stream type or update the - * advertized metadata on each call. We should rather make sure we get - * only a single content audio stream from the media frameworks. - */ } private: diff --git a/system/bta/le_audio/broadcaster/broadcaster_test.cc b/system/bta/le_audio/broadcaster/broadcaster_test.cc index 8aa914797c..a566cad2b8 100644 --- a/system/bta/le_audio/broadcaster/broadcaster_test.cc +++ b/system/bta/le_audio/broadcaster/broadcaster_test.cc @@ -17,6 +17,7 @@ #include <gmock/gmock.h> #include <gtest/gtest.h> +#include <hardware/audio.h> #include <chrono> @@ -30,6 +31,8 @@ #include "device/include/controller.h" #include "stack/include/btm_iso_api.h" +using namespace std::chrono_literals; + using le_audio::types::LeAudioContextType; using testing::_; @@ -108,6 +111,7 @@ static void cleanup_message_loop_thread() { namespace le_audio { namespace broadcaster { namespace { +static constexpr uint8_t default_ccid = 0xDE; static constexpr auto default_context = static_cast<std::underlying_type<LeAudioContextType>::type>( LeAudioContextType::ALERTS); @@ -203,6 +207,7 @@ class BroadcasterTest : public Test { base::Bind([]() -> bool { return true; })); ContentControlIdKeeper::GetInstance()->Start(); + ContentControlIdKeeper::GetInstance()->SetCcid(0x0004, media_ccid); /* Simulate random generator */ uint8_t random[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; @@ -405,12 +410,29 @@ TEST_F(BroadcasterTest, GetBroadcastAllStates) { TEST_F(BroadcasterTest, UpdateMetadata) { auto broadcast_id = InstantiateBroadcast(); - + std::vector<uint8_t> ccid_list; EXPECT_CALL(*MockBroadcastStateMachine::GetLastInstance(), UpdateBroadcastAnnouncement) - .Times(1); + .WillOnce( + [&](bluetooth::le_audio::BasicAudioAnnouncementData announcement) { + for (auto subgroup : announcement.subgroup_configs) { + if (subgroup.metadata.count( + types::kLeAudioMetadataTypeCcidList)) { + ccid_list = + subgroup.metadata.at(types::kLeAudioMetadataTypeCcidList); + break; + } + } + }); + + ContentControlIdKeeper::GetInstance()->SetCcid(0x0400, default_ccid); LeAudioBroadcaster::Get()->UpdateMetadata( - broadcast_id, std::vector<uint8_t>({0x02, 0x01, 0x02})); + broadcast_id, + std::vector<uint8_t>({0x02, 0x01, 0x02, 0x03, 0x02, 0x04, 0x04})); + + ASSERT_EQ(2u, ccid_list.size()); + ASSERT_NE(0, std::count(ccid_list.begin(), ccid_list.end(), media_ccid)); + ASSERT_NE(0, std::count(ccid_list.begin(), ccid_list.end(), default_ccid)); } static BasicAudioAnnouncementData prepareAnnouncement( @@ -444,10 +466,87 @@ static BasicAudioAnnouncementData prepareAnnouncement( return announcement; } +TEST_F(BroadcasterTest, UpdateMetadataFromAudioTrackMetadata) { + ContentControlIdKeeper::GetInstance()->SetCcid(media_context, media_ccid); + auto broadcast_id = InstantiateBroadcast(); + + LeAudioClientAudioSinkReceiver* audio_receiver; + EXPECT_CALL(*mock_audio_source_, Start) + .WillOnce(DoAll(SaveArg<1>(&audio_receiver), Return(true))); + + LeAudioBroadcaster::Get()->StartAudioBroadcast(broadcast_id); + ASSERT_NE(audio_receiver, nullptr); + + auto sm = MockBroadcastStateMachine::GetLastInstance(); + std::vector<uint8_t> ccid_list; + std::vector<uint8_t> context_types_map; + EXPECT_CALL(*sm, UpdateBroadcastAnnouncement) + .WillOnce( + [&](bluetooth::le_audio::BasicAudioAnnouncementData announcement) { + for (auto subgroup : announcement.subgroup_configs) { + if (subgroup.metadata.count( + types::kLeAudioMetadataTypeCcidList)) { + ccid_list = + subgroup.metadata.at(types::kLeAudioMetadataTypeCcidList); + } + if (subgroup.metadata.count( + types::kLeAudioMetadataTypeStreamingAudioContext)) { + context_types_map = subgroup.metadata.at( + types::kLeAudioMetadataTypeStreamingAudioContext); + } + } + }); + + std::map<uint8_t, std::vector<uint8_t>> meta = {}; + BroadcastCodecWrapper codec_config( + {.coding_format = le_audio::types::kLeAudioCodingFormatLC3, + .vendor_company_id = le_audio::types::kLeAudioVendorCompanyIdUndefined, + .vendor_codec_id = le_audio::types::kLeAudioVendorCodecIdUndefined}, + {.num_channels = LeAudioCodecConfiguration::kChannelNumberMono, + .sample_rate = LeAudioCodecConfiguration::kSampleRate16000, + .bits_per_sample = LeAudioCodecConfiguration::kBitsPerSample16, + .data_interval_us = LeAudioCodecConfiguration::kInterval10000Us}, + 32000, 40); + auto announcement = prepareAnnouncement(codec_config, meta); + + ON_CALL(*sm, GetBroadcastAnnouncement()) + .WillByDefault(ReturnRef(announcement)); + + std::promise<void> do_update_metadata_promise; + struct playback_track_metadata tracks_[] = { + {AUDIO_USAGE_GAME, AUDIO_CONTENT_TYPE_SONIFICATION, 0}, + {AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, 0}, + {AUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING, AUDIO_CONTENT_TYPE_SPEECH, + 0}, + {AUDIO_USAGE_UNKNOWN, AUDIO_CONTENT_TYPE_UNKNOWN, 0}}; + + source_metadata_t multitrack_source_metadata = {.track_count = 4, + .tracks = &tracks_[0]}; + + auto do_update_metadata_future = do_update_metadata_promise.get_future(); + audio_receiver->OnAudioMetadataUpdate(std::move(do_update_metadata_promise), + multitrack_source_metadata); + do_update_metadata_future.wait_for(3s); + + // Verify ccid + ASSERT_NE(ccid_list.size(), 0u); + ASSERT_TRUE(std::find(ccid_list.begin(), ccid_list.end(), media_ccid) != + ccid_list.end()); + + // Verify context type + ASSERT_NE(context_types_map.size(), 0u); + uint16_t context_type; + auto pp = context_types_map.data(); + STREAM_TO_UINT16(context_type, pp); + ASSERT_NE(context_type & + static_cast<std::underlying_type<LeAudioContextType>::type>( + LeAudioContextType::MEDIA | LeAudioContextType::GAME), + 0); +} + TEST_F(BroadcasterTest, GetMetadata) { auto broadcast_id = InstantiateBroadcast(); bluetooth::le_audio::BroadcastMetadata metadata; - // bluetooth::le_audio::BasicAudioAnnouncementData announcement; static const uint8_t test_adv_sid = 0x14; std::optional<bluetooth::le_audio::BroadcastCode> test_broadcast_code = diff --git a/system/bta/le_audio/broadcaster/mock_state_machine.h b/system/bta/le_audio/broadcaster/mock_state_machine.h index 3b1532c21d..eb346736b4 100644 --- a/system/bta/le_audio/broadcaster/mock_state_machine.h +++ b/system/bta/le_audio/broadcaster/mock_state_machine.h @@ -131,6 +131,7 @@ class MockBroadcastStateMachine std::optional<le_audio::broadcaster::BigConfig> big_config_ = std::nullopt; le_audio::broadcaster::BroadcastStateMachineConfig cfg; le_audio::broadcaster::IBroadcastStateMachineCallbacks* cb; + void SetExpectedState(BroadcastStateMachine::State state) { SetState(state); } void SetExpectedResult(bool result) { result_ = result; } void SetExpectedBigConfig( std::optional<le_audio::broadcaster::BigConfig> big_cfg) { diff --git a/system/bta/le_audio/client.cc b/system/bta/le_audio/client.cc index 73da1fdf88..09cb59d8f5 100644 --- a/system/bta/le_audio/client.cc +++ b/system/bta/le_audio/client.cc @@ -40,6 +40,7 @@ #include "gd/common/strings.h" #include "le_audio_set_configuration_provider.h" #include "le_audio_types.h" +#include "le_audio_utils.h" #include "metrics_collector.h" #include "osi/include/log.h" #include "osi/include/osi.h" @@ -76,6 +77,9 @@ using le_audio::types::AudioStreamDataPathState; using le_audio::types::hdl_pair; using le_audio::types::kDefaultScanDurationS; using le_audio::types::LeAudioContextType; +using le_audio::utils::AudioContentToLeAudioContext; +using le_audio::utils::GetAllCcids; +using le_audio::utils::GetAllowedAudioContextsFromSourceMetadata; using le_audio::client_parser::ascs:: kCtpResponseCodeInvalidConfigurationParameterValue; @@ -141,72 +145,6 @@ std::ostream& operator<<(std::ostream& os, const AudioState& audio_state) { return os; } -static std::string usageToString(audio_usage_t usage) { - switch (usage) { - case AUDIO_USAGE_UNKNOWN: - return "USAGE_UNKNOWN"; - case AUDIO_USAGE_MEDIA: - return "USAGE_MEDIA"; - case AUDIO_USAGE_VOICE_COMMUNICATION: - return "USAGE_VOICE_COMMUNICATION"; - case AUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING: - return "USAGE_VOICE_COMMUNICATION_SIGNALLING"; - case AUDIO_USAGE_ALARM: - return "USAGE_ALARM"; - case AUDIO_USAGE_NOTIFICATION: - return "USAGE_NOTIFICATION"; - case AUDIO_USAGE_NOTIFICATION_TELEPHONY_RINGTONE: - return "USAGE_NOTIFICATION_TELEPHONY_RINGTONE"; - case AUDIO_USAGE_NOTIFICATION_COMMUNICATION_REQUEST: - return "USAGE_NOTIFICATION_COMMUNICATION_REQUEST"; - case AUDIO_USAGE_NOTIFICATION_COMMUNICATION_INSTANT: - return "USAGE_NOTIFICATION_COMMUNICATION_INSTANT"; - case AUDIO_USAGE_NOTIFICATION_COMMUNICATION_DELAYED: - return "USAGE_NOTIFICATION_COMMUNICATION_DELAYED"; - case AUDIO_USAGE_NOTIFICATION_EVENT: - return "USAGE_NOTIFICATION_EVENT"; - case AUDIO_USAGE_ASSISTANCE_ACCESSIBILITY: - return "USAGE_ASSISTANCE_ACCESSIBILITY"; - case AUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: - return "USAGE_ASSISTANCE_NAVIGATION_GUIDANCE"; - case AUDIO_USAGE_ASSISTANCE_SONIFICATION: - return "USAGE_ASSISTANCE_SONIFICATION"; - case AUDIO_USAGE_GAME: - return "USAGE_GAME"; - case AUDIO_USAGE_ASSISTANT: - return "USAGE_ASSISTANT"; - case AUDIO_USAGE_CALL_ASSISTANT: - return "USAGE_CALL_ASSISTANT"; - case AUDIO_USAGE_EMERGENCY: - return "USAGE_EMERGENCY"; - case AUDIO_USAGE_SAFETY: - return "USAGE_SAFETY"; - case AUDIO_USAGE_VEHICLE_STATUS: - return "USAGE_VEHICLE_STATUS"; - case AUDIO_USAGE_ANNOUNCEMENT: - return "USAGE_ANNOUNCEMENT"; - default: - return "unknown usage "; - } -} - -static std::string contentTypeToString(audio_content_type_t content_type) { - switch (content_type) { - case AUDIO_CONTENT_TYPE_UNKNOWN: - return "CONTENT_TYPE_UNKNOWN"; - case AUDIO_CONTENT_TYPE_SPEECH: - return "CONTENT_TYPE_SPEECH"; - case AUDIO_CONTENT_TYPE_MUSIC: - return "CONTENT_TYPE_MUSIC"; - case AUDIO_CONTENT_TYPE_MOVIE: - return "CONTENT_TYPE_MOVIE"; - case AUDIO_CONTENT_TYPE_SONIFICATION: - return "CONTENT_TYPE_SONIFICATION"; - default: - return "unknown content type "; - } -} - namespace { void le_audio_gattc_callback(tBTA_GATTC_EVT event, tBTA_GATTC* p_data); @@ -281,7 +219,10 @@ class LeAudioClientImpl : public LeAudioClient { : gatt_if_(0), callbacks_(callbacks_), active_group_id_(bluetooth::groups::kGroupUnknown), - current_context_type_(LeAudioContextType::MEDIA), + configuration_context_type_(LeAudioContextType::MEDIA), + metadata_context_types_( + static_cast<std::underlying_type<LeAudioContextType>::type>( + LeAudioContextType::MEDIA)), stream_setup_start_timestamp_(0), stream_setup_end_timestamp_(0), audio_receiver_state_(AudioState::IDLE), @@ -401,10 +342,6 @@ class LeAudioClientImpl : public LeAudioClient { group_remove_node(group, address); } - int GetCcid(uint16_t context_type) { - return ContentControlIdKeeper::GetInstance()->GetCcid(context_type); - } - /* This callback happens if kLeAudioDeviceSetStateTimeoutMs timeout happens * during transition from origin to target state */ @@ -681,7 +618,8 @@ class LeAudioClientImpl : public LeAudioClient { group_remove_node(group, address, true); } - bool InternalGroupStream(const int group_id, const uint16_t context_type) { + bool GroupStream(const int group_id, const uint16_t context_type, + AudioContexts metadata_context_type) { LeAudioDeviceGroup* group = aseGroups_.FindById(group_id); auto final_context_type = context_type; @@ -719,7 +657,7 @@ class LeAudioClientImpl : public LeAudioClient { bool result = groupStateMachine_->StartStream( group, static_cast<LeAudioContextType>(final_context_type), - GetCcid(final_context_type)); + metadata_context_type, GetAllCcids(metadata_context_type)); if (result) stream_setup_start_timestamp_ = bluetooth::common::time_get_os_boottime_us(); @@ -728,7 +666,7 @@ class LeAudioClientImpl : public LeAudioClient { } void GroupStream(const int group_id, const uint16_t context_type) override { - InternalGroupStream(group_id, context_type); + GroupStream(group_id, context_type, context_type); } void GroupSuspend(const int group_id) override { @@ -2683,7 +2621,8 @@ class LeAudioClientImpl : public LeAudioClient { void Dump(int fd) { dprintf(fd, " Active group: %d\n", active_group_id_); - dprintf(fd, " current content type: 0x%08hx\n", current_context_type_); + dprintf(fd, " configuration content type: 0x%08hx\n", + configuration_context_type_); dprintf( fd, " stream setup time if started: %d ms\n", (int)((stream_setup_end_timestamp_ - stream_setup_start_timestamp_) / @@ -2776,7 +2715,7 @@ class LeAudioClientImpl : public LeAudioClient { LOG_INFO(" Session reconfiguration needed group: %d for context type: %s", group->group_id_, ToString(context_type).c_str()); - current_context_type_ = context_type; + configuration_context_type_ = context_type; return AudioReconfigurationResult::RECONFIGURATION_NEEDED; } @@ -2784,8 +2723,9 @@ class LeAudioClientImpl : public LeAudioClient { if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) { return true; } - return InternalGroupStream(active_group_id_, - static_cast<uint16_t>(current_context_type_)); + return GroupStream(active_group_id_, + static_cast<uint16_t>(configuration_context_type_), + metadata_context_types_); } void OnAudioSuspend() { @@ -2864,9 +2804,10 @@ class LeAudioClientImpl : public LeAudioClient { /* Check if the device resume is expected */ if (!group->GetCodecConfigurationByDirection( - current_context_type_, le_audio::types::kLeAudioDirectionSink)) { + configuration_context_type_, + le_audio::types::kLeAudioDirectionSink)) { LOG(ERROR) << __func__ << ", invalid resume request for context type: " - << loghex(static_cast<int>(current_context_type_)); + << loghex(static_cast<int>(configuration_context_type_)); leAudioClientAudioSource->CancelStreamingRequest(); return; } @@ -2874,8 +2815,8 @@ class LeAudioClientImpl : public LeAudioClient { DLOG(INFO) << __func__ << " active_group_id: " << active_group_id_ << "\n" << " audio_receiver_state: " << audio_receiver_state_ << "\n" << " audio_sender_state: " << audio_sender_state_ << "\n" - << " current_context_type_: " - << static_cast<int>(current_context_type_) << "\n" + << " configuration_context_type_: " + << static_cast<int>(configuration_context_type_) << "\n" << " group " << (group ? " exist " : " does not exist ") << "\n"; switch (audio_sender_state_) { @@ -2940,7 +2881,7 @@ class LeAudioClientImpl : public LeAudioClient { alarm_cancel(suspend_timeout_); leAudioClientAudioSource->ConfirmStreamingRequest(); le_audio::MetricsCollector::Get()->OnStreamStarted( - active_group_id_, current_context_type_); + active_group_id_, configuration_context_type_); break; case AudioState::RELEASING: /* Keep wainting. After release is done, Audio Hal will be notified @@ -2989,9 +2930,9 @@ class LeAudioClientImpl : public LeAudioClient { << " audio_sender_state_: " << audio_sender_state_; } - bool IsAudioSourceAvailableForCurrentContentType() { - if (current_context_type_ == LeAudioContextType::CONVERSATIONAL || - current_context_type_ == LeAudioContextType::VOICEASSISTANTS) { + bool IsAudioSourceAvailableForCurrentConfiguration() { + if (configuration_context_type_ == LeAudioContextType::CONVERSATIONAL || + configuration_context_type_ == LeAudioContextType::VOICEASSISTANTS) { return true; } @@ -3014,9 +2955,10 @@ class LeAudioClientImpl : public LeAudioClient { /* Check if the device resume is expected */ if (!group->GetCodecConfigurationByDirection( - current_context_type_, le_audio::types::kLeAudioDirectionSource)) { + configuration_context_type_, + le_audio::types::kLeAudioDirectionSource)) { LOG(ERROR) << __func__ << ", invalid resume request for context type: " - << loghex(static_cast<int>(current_context_type_)); + << loghex(static_cast<int>(configuration_context_type_)); leAudioClientAudioSink->CancelStreamingRequest(); return; } @@ -3024,8 +2966,8 @@ class LeAudioClientImpl : public LeAudioClient { DLOG(INFO) << __func__ << " active_group_id: " << active_group_id_ << "\n" << " audio_receiver_state: " << audio_receiver_state_ << "\n" << " audio_sender_state: " << audio_sender_state_ << "\n" - << " current_context_type_: " - << static_cast<int>(current_context_type_) << "\n" + << " configuration_context_type_: " + << static_cast<int>(configuration_context_type_) << "\n" << " group " << (group ? " exist " : " does not exist ") << "\n"; switch (audio_receiver_state_) { @@ -3049,7 +2991,7 @@ class LeAudioClientImpl : public LeAudioClient { */ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) { - if (!IsAudioSourceAvailableForCurrentContentType()) { + if (!IsAudioSourceAvailableForCurrentConfiguration()) { StopStreamIfNeeded(group, LeAudioContextType::VOICEASSISTANTS); break; } @@ -3105,61 +3047,48 @@ class LeAudioClientImpl : public LeAudioClient { } } - LeAudioContextType AudioContentToLeAudioContext( - audio_content_type_t content_type, audio_usage_t usage) { - /* Check audio attribute usage of stream */ - switch (usage) { - case AUDIO_USAGE_MEDIA: - return LeAudioContextType::MEDIA; - case AUDIO_USAGE_VOICE_COMMUNICATION: - case AUDIO_USAGE_CALL_ASSISTANT: - return LeAudioContextType::CONVERSATIONAL; - case AUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING: - if (content_type == AUDIO_CONTENT_TYPE_SPEECH) - return LeAudioContextType::CONVERSATIONAL; - else - return LeAudioContextType::MEDIA; - case AUDIO_USAGE_GAME: - return LeAudioContextType::GAME; - case AUDIO_USAGE_NOTIFICATION: - return LeAudioContextType::NOTIFICATIONS; - case AUDIO_USAGE_NOTIFICATION_TELEPHONY_RINGTONE: - return LeAudioContextType::RINGTONE; - case AUDIO_USAGE_ALARM: - return LeAudioContextType::ALERTS; - case AUDIO_USAGE_EMERGENCY: - return LeAudioContextType::EMERGENCYALARM; - case AUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: - return LeAudioContextType::INSTRUCTIONAL; - default: - break; + LeAudioContextType ChooseConfigurationContextType( + le_audio::types::AudioContexts available_contexts) { + if (available_contexts.none()) { + LOG_WARN(" invalid/unknown context, using 'UNSPECIFIED'"); + return LeAudioContextType::UNSPECIFIED; } - return LeAudioContextType::MEDIA; - } + using T = std::underlying_type<LeAudioContextType>::type; - LeAudioContextType ChooseContextType( - std::vector<LeAudioContextType>& available_contents) { /* Mini policy. Voice is prio 1, game prio 2, media is prio 3 */ - auto iter = find(available_contents.begin(), available_contents.end(), - LeAudioContextType::CONVERSATIONAL); - if (iter != available_contents.end()) + if ((available_contexts & + AudioContexts(static_cast<T>(LeAudioContextType::CONVERSATIONAL))) + .any()) return LeAudioContextType::CONVERSATIONAL; - iter = find(available_contents.begin(), available_contents.end(), - LeAudioContextType::RINGTONE); - if (iter != available_contents.end()) return LeAudioContextType::RINGTONE; + if ((available_contexts & + AudioContexts(static_cast<T>(LeAudioContextType::GAME))) + .any()) + return LeAudioContextType::GAME; - iter = find(available_contents.begin(), available_contents.end(), - LeAudioContextType::GAME); - if (iter != available_contents.end()) return LeAudioContextType::GAME; + if ((available_contexts & + AudioContexts(static_cast<T>(LeAudioContextType::RINGTONE))) + .any()) + return LeAudioContextType::RINGTONE; - iter = find(available_contents.begin(), available_contents.end(), - LeAudioContextType::MEDIA); - if (iter != available_contents.end()) return LeAudioContextType::MEDIA; + if ((available_contexts & + AudioContexts(static_cast<T>(LeAudioContextType::MEDIA))) + .any()) + return LeAudioContextType::MEDIA; /*TODO do something smarter here */ - return available_contents[0]; + /* Get context for the first non-zero bit */ + uint16_t context_type = 0b1; + while (available_contexts != 0b1) { + available_contexts = available_contexts >> 1; + context_type = context_type << 1; + } + + if (context_type < static_cast<T>(LeAudioContextType::RFU)) { + return LeAudioContextType(context_type); + } + return LeAudioContextType::UNSPECIFIED; } bool StopStreamIfNeeded(LeAudioDeviceGroup* group, @@ -3194,9 +3123,6 @@ class LeAudioClientImpl : public LeAudioClient { } void OnAudioMetadataUpdate(const source_metadata_t& source_metadata) { - auto tracks = source_metadata.tracks; - auto track_count = source_metadata.track_count; - if (active_group_id_ == bluetooth::groups::kGroupUnknown) { LOG(WARNING) << ", cannot start streaming if no active group set"; return; @@ -3211,52 +3137,21 @@ class LeAudioClientImpl : public LeAudioClient { bool is_group_streaming = (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); - std::vector<LeAudioContextType> contexts; + metadata_context_types_ = GetAllowedAudioContextsFromSourceMetadata( + source_metadata, group->GetActiveContexts()); - auto supported_context_type = group->GetActiveContexts(); + auto new_configuration_context = + ChooseConfigurationContextType(metadata_context_types_); + LOG_DEBUG("new_configuration_context_type: %s", + ToString(new_configuration_context).c_str()); - while (track_count) { - if (tracks->content_type == 0 && tracks->usage == 0) { - --track_count; - ++tracks; - continue; - } - - LOG_INFO("%s: usage=%s(%d), content_type=%s(%d), gain=%f", __func__, - usageToString(tracks->usage).c_str(), tracks->usage, - contentTypeToString(tracks->content_type).c_str(), - tracks->content_type, tracks->gain); - - auto new_context = - AudioContentToLeAudioContext(tracks->content_type, tracks->usage); - - /* Check only supported context types.*/ - if (static_cast<int>(new_context) & supported_context_type.to_ulong()) { - contexts.push_back(new_context); - } else { - LOG_WARN(" Context type %s not supported by remote device", - ToString(new_context).c_str()); - } - - --track_count; - ++tracks; - } - - if (contexts.empty()) { - LOG_WARN(" invalid/unknown metadata update"); - return; - } - - auto new_context = ChooseContextType(contexts); - LOG_DEBUG("new_context_type: %s", ToString(new_context).c_str()); - - if (new_context == current_context_type_) { + if (new_configuration_context == configuration_context_type_) { LOG_INFO("Context did not changed."); return; } - current_context_type_ = new_context; - if (StopStreamIfNeeded(group, new_context)) { + configuration_context_type_ = new_configuration_context; + if (StopStreamIfNeeded(group, new_configuration_context)) { return; } @@ -3264,7 +3159,9 @@ class LeAudioClientImpl : public LeAudioClient { /* Configuration is the same for new context, just will do update * metadata of stream */ - GroupStream(active_group_id_, static_cast<uint16_t>(new_context)); + GroupStream(active_group_id_, + static_cast<uint16_t>(new_configuration_context), + metadata_context_types_); } } @@ -3300,25 +3197,35 @@ class LeAudioClientImpl : public LeAudioClient { */ if (is_audio_source_invalid || !group->IsContextSupported(LeAudioContextType::VOICEASSISTANTS) || - IsAudioSourceAvailableForCurrentContentType()) { + IsAudioSourceAvailableForCurrentConfiguration()) { return; } auto new_context = LeAudioContextType::VOICEASSISTANTS; + /* Add the new_context to the metadata */ + metadata_context_types_ = + metadata_context_types_.to_ulong() | static_cast<uint16_t>(new_context); + if (StopStreamIfNeeded(group, new_context)) return; if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) { + /* Add the new_context to the metadata */ + metadata_context_types_ = metadata_context_types_.to_ulong() | + static_cast<uint16_t>(new_context); + /* Configuration is the same for new context, just will do update - * metadata of stream + * metadata of stream. Consider using separate metadata for each + * direction. */ - GroupStream(active_group_id_, static_cast<uint16_t>(new_context)); + GroupStream(active_group_id_, static_cast<uint16_t>(new_context), + metadata_context_types_); } /* Audio sessions are not resumed yet and not streaming, let's pick voice * assistant as possible current context type. */ - current_context_type_ = new_context; + configuration_context_type_ = new_context; } static void OnGattReadRspStatic(uint16_t conn_id, tGATT_STATUS status, @@ -3555,7 +3462,7 @@ class LeAudioClientImpl : public LeAudioClient { stream_setup_end_timestamp_ = bluetooth::common::time_get_os_boottime_us(); le_audio::MetricsCollector::Get()->OnStreamStarted( - active_group_id_, current_context_type_); + active_group_id_, configuration_context_type_); break; case GroupStreamStatus::SUSPENDED: stream_setup_end_timestamp_ = 0; @@ -3584,10 +3491,8 @@ class LeAudioClientImpl : public LeAudioClient { if (group && group->IsPendingConfiguration()) { SuspendedForReconfiguration(); if (groupStateMachine_->ConfigureStream( - group, current_context_type_, - GetCcid(static_cast< - std::underlying_type<LeAudioContextType>::type>( - current_context_type_)))) { + group, configuration_context_type_, metadata_context_types_, + GetAllCcids(metadata_context_types_))) { /* If configuration succeed wait for new status. */ return; } @@ -3620,7 +3525,8 @@ class LeAudioClientImpl : public LeAudioClient { LeAudioDeviceGroups aseGroups_; LeAudioGroupStateMachine* groupStateMachine_; int active_group_id_; - LeAudioContextType current_context_type_; + LeAudioContextType configuration_context_type_; + AudioContexts metadata_context_types_; uint64_t stream_setup_start_timestamp_; uint64_t stream_setup_end_timestamp_; diff --git a/system/bta/le_audio/devices.cc b/system/bta/le_audio/devices.cc index 67c03985b8..1ed37f5e61 100644 --- a/system/bta/le_audio/devices.cc +++ b/system/bta/le_audio/devices.cc @@ -894,6 +894,15 @@ types::LeAudioConfigurationStrategy LeAudioDeviceGroup::GetGroupStrategy(void) { return types::LeAudioConfigurationStrategy::STEREO_ONE_CIS_PER_DEVICE; } +int LeAudioDeviceGroup::GetAseCount(uint8_t direction) { + int result = 0; + for (const auto& device_iter : leAudioDevices_) { + result += device_iter.lock()->GetAseCount(direction); + } + + return result; +} + void LeAudioDeviceGroup::CigGenerateCisIds( types::LeAudioContextType context_type) { LOG_INFO("Group %p, group_id: %d, context_type: %s", this, group_id_, @@ -910,7 +919,9 @@ void LeAudioDeviceGroup::CigGenerateCisIds( uint8_t cis_count_bidir = 0; uint8_t cis_count_unidir_sink = 0; uint8_t cis_count_unidir_source = 0; - get_cis_count(confs, GetGroupStrategy(), &cis_count_bidir, + get_cis_count(confs, GetGroupStrategy(), + GetAseCount(types::kLeAudioDirectionSink), + GetAseCount(types::kLeAudioDirectionSource), &cis_count_bidir, &cis_count_unidir_sink, &cis_count_unidir_source); uint8_t idx = 0; @@ -1397,7 +1408,8 @@ bool LeAudioDevice::ConfigureAses( uint8_t* number_of_already_active_group_ase, types::AudioLocations& group_snk_audio_locations, types::AudioLocations& group_src_audio_locations, bool reuse_cis_id, - int ccid) { + AudioContexts metadata_context_type, + const std::vector<uint8_t>& ccid_list) { struct ase* ase = GetFirstInactiveAse(ent.direction, reuse_cis_id); if (!ase) return false; @@ -1453,7 +1465,7 @@ bool LeAudioDevice::ConfigureAses( ase->retrans_nb = ent.qos.retransmission_number; ase->max_transport_latency = ent.qos.max_transport_latency; - ase->metadata = GetMetadata(context_type, ccid); + ase->metadata = GetMetadata(metadata_context_type, ccid_list); DLOG(INFO) << __func__ << " device=" << address_ << ", activated ASE id=" << +ase->id @@ -1474,7 +1486,8 @@ bool LeAudioDevice::ConfigureAses( */ bool LeAudioDeviceGroup::ConfigureAses( const set_configurations::AudioSetConfiguration* audio_set_conf, - types::LeAudioContextType context_type, int ccid) { + types::LeAudioContextType context_type, AudioContexts metadata_context_type, + const std::vector<uint8_t>& ccid_list) { if (!set_configurations::check_if_may_cover_scenario( audio_set_conf, NumOfConnected(context_type))) return false; @@ -1523,7 +1536,8 @@ bool LeAudioDeviceGroup::ConfigureAses( if (!device->ConfigureAses(ent, context_type, &active_ase_num, group_snk_audio_locations, - group_src_audio_locations, reuse_cis_id, ccid)) + group_src_audio_locations, reuse_cis_id, + metadata_context_type, ccid_list)) continue; required_device_cnt--; @@ -1611,10 +1625,10 @@ bool LeAudioDeviceGroup::IsContextSupported( } bool LeAudioDeviceGroup::IsMetadataChanged( - types::LeAudioContextType context_type, int ccid) { + types::AudioContexts context_type, const std::vector<uint8_t>& ccid_list) { for (auto* leAudioDevice = GetFirstActiveDevice(); leAudioDevice; leAudioDevice = GetNextActiveDevice(leAudioDevice)) { - if (leAudioDevice->IsMetadataChanged(context_type, ccid)) return true; + if (leAudioDevice->IsMetadataChanged(context_type, ccid_list)) return true; } return false; @@ -1786,7 +1800,9 @@ LeAudioDeviceGroup::FindFirstSupportedConfiguration( /* This method should choose aproperiate ASEs to be active and set a cached * configuration for codec and qos. */ -bool LeAudioDeviceGroup::Configure(LeAudioContextType context_type, int ccid) { +bool LeAudioDeviceGroup::Configure(LeAudioContextType context_type, + AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list) { const set_configurations::AudioSetConfiguration* conf = active_context_to_configuration_map[context_type]; @@ -1801,7 +1817,7 @@ bool LeAudioDeviceGroup::Configure(LeAudioContextType context_type, int ccid) { DLOG(INFO) << __func__ << " setting context type: " << int(context_type); - if (!ConfigureAses(conf, context_type, ccid)) { + if (!ConfigureAses(conf, context_type, metadata_context_type, ccid_list)) { LOG(ERROR) << __func__ << ", requested pick ASE config context type: " << loghex(static_cast<uint16_t>(context_type)) << ", is in mismatch with cached active contexts"; @@ -1928,6 +1944,12 @@ struct ase* LeAudioDevice::GetAseByValHandle(uint16_t val_hdl) { return (iter == ases_.end()) ? nullptr : &(*iter); } +int LeAudioDevice::GetAseCount(uint8_t direction) { + return std::count_if(ases_.begin(), ases_.end(), [direction](const auto& a) { + return a.direction == direction; + }); +} + struct ase* LeAudioDevice::GetFirstInactiveAseWithState(uint8_t direction, AseState state) { auto iter = std::find_if( @@ -2390,21 +2412,22 @@ void LeAudioDevice::DeactivateAllAses(void) { } } -std::vector<uint8_t> LeAudioDevice::GetMetadata(LeAudioContextType context_type, - int ccid) { +std::vector<uint8_t> LeAudioDevice::GetMetadata( + AudioContexts context_type, const std::vector<uint8_t>& ccid_list) { std::vector<uint8_t> metadata; AppendMetadataLtvEntryForStreamingContext(metadata, context_type); - AppendMetadataLtvEntryForCcidList(metadata, ccid); + AppendMetadataLtvEntryForCcidList(metadata, ccid_list); return std::move(metadata); } -bool LeAudioDevice::IsMetadataChanged(types::LeAudioContextType context_type, - int ccid) { +bool LeAudioDevice::IsMetadataChanged(AudioContexts context_type, + const std::vector<uint8_t>& ccid_list) { for (auto* ase = this->GetFirstActiveAse(); ase; ase = this->GetNextActiveAse(ase)) { - if (this->GetMetadata(context_type, ccid) != ase->metadata) return true; + if (this->GetMetadata(context_type, ccid_list) != ase->metadata) + return true; } return false; diff --git a/system/bta/le_audio/devices.h b/system/bta/le_audio/devices.h index 79980094ca..885f013026 100644 --- a/system/bta/le_audio/devices.h +++ b/system/bta/le_audio/devices.h @@ -104,6 +104,7 @@ class LeAudioDevice { void RegisterPACs(std::vector<struct types::acs_ac_record>* apr_db, std::vector<struct types::acs_ac_record>* apr); struct types::ase* GetAseByValHandle(uint16_t val_hdl); + int GetAseCount(uint8_t direction); struct types::ase* GetFirstActiveAse(void); struct types::ase* GetFirstActiveAseByDirection(uint8_t direction); struct types::ase* GetNextActiveAseWithSameDirection( @@ -139,7 +140,8 @@ class LeAudioDevice { uint8_t* number_of_already_active_group_ase, types::AudioLocations& group_snk_audio_locations, types::AudioLocations& group_src_audio_locations, - bool reconnect = false, int ccid = -1); + bool reconnect, types::AudioContexts metadata_context_type, + const std::vector<uint8_t>& ccid_list); void SetSupportedContexts(types::AudioContexts snk_contexts, types::AudioContexts src_contexts); types::AudioContexts GetAvailableContexts(void); @@ -149,9 +151,10 @@ class LeAudioDevice { bool ActivateConfiguredAses(types::LeAudioContextType context_type); void Dump(int fd); void DisconnectAcl(void); - std::vector<uint8_t> GetMetadata(types::LeAudioContextType context_type, - int ccid); - bool IsMetadataChanged(types::LeAudioContextType context_type, int ccid); + std::vector<uint8_t> GetMetadata(types::AudioContexts context_type, + const std::vector<uint8_t>& ccid_list); + bool IsMetadataChanged(types::AudioContexts context_type, + const std::vector<uint8_t>& ccid_list); private: types::AudioContexts avail_snk_contexts_; @@ -233,6 +236,7 @@ class LeAudioDeviceGroup { LeAudioDevice* GetFirstDeviceWithActiveContext( types::LeAudioContextType context_type); le_audio::types::LeAudioConfigurationStrategy GetGroupStrategy(void); + int GetAseCount(uint8_t direction); LeAudioDevice* GetNextDevice(LeAudioDevice* leAudioDevice); LeAudioDevice* GetNextDeviceWithActiveContext( LeAudioDevice* leAudioDevice, types::LeAudioContextType context_type); @@ -255,7 +259,9 @@ class LeAudioDeviceGroup { void CigAssignCisConnHandlesToAses(LeAudioDevice* leAudioDevice); void CigAssignCisConnHandlesToAses(void); void CigUnassignCis(LeAudioDevice* leAudioDevice); - bool Configure(types::LeAudioContextType context_type, int ccid = 1); + bool Configure(types::LeAudioContextType context_type, + types::AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list = {}); bool SetContextType(types::LeAudioContextType context_type); types::LeAudioContextType GetContextType(void); uint32_t GetSduInterval(uint8_t direction); @@ -287,8 +293,8 @@ class LeAudioDeviceGroup { std::optional<LeAudioCodecConfiguration> GetCodecConfigurationByDirection( types::LeAudioContextType group_context_type, uint8_t direction); bool IsContextSupported(types::LeAudioContextType group_context_type); - bool IsMetadataChanged(types::LeAudioContextType group_context_type, - int ccid); + bool IsMetadataChanged(types::AudioContexts group_context_type, + const std::vector<uint8_t>& ccid_list); void CreateStreamVectorForOffloader(uint8_t direction); void StreamOffloaderUpdated(uint8_t direction); @@ -327,7 +333,9 @@ class LeAudioDeviceGroup { FindFirstSupportedConfiguration(types::LeAudioContextType context_type); bool ConfigureAses( const set_configurations::AudioSetConfiguration* audio_set_conf, - types::LeAudioContextType context_type, int ccid = 1); + types::LeAudioContextType context_type, + types::AudioContexts metadata_context_type, + const std::vector<uint8_t>& ccid_list); bool IsConfigurationSupported( const set_configurations::AudioSetConfiguration* audio_set_configuration, types::LeAudioContextType context_type); diff --git a/system/bta/le_audio/devices_test.cc b/system/bta/le_audio/devices_test.cc index 6b48fdca61..ac6fa2f884 100644 --- a/system/bta/le_audio/devices_test.cc +++ b/system/bta/le_audio/devices_test.cc @@ -388,8 +388,8 @@ struct TestGroupAseConfigurationData { uint8_t audio_channel_counts_snk; uint8_t audio_channel_counts_src; - uint8_t active_channel_num_snk; - uint8_t active_channel_num_src; + uint8_t expected_active_channel_num_snk; + uint8_t expected_active_channel_num_src; }; class LeAudioAseConfigurationTest : public Test { @@ -458,9 +458,9 @@ class LeAudioAseConfigurationTest : public Test { uint8_t active_channel_num_snk = 0; uint8_t active_channel_num_src = 0; - bool have_active_ase = - data.active_channel_num_snk + data.active_channel_num_src; - ASSERT_EQ(have_active_ase, data.device->HaveActiveAse()); + bool expected_active_ase = data.expected_active_channel_num_snk + + data.expected_active_channel_num_src; + ASSERT_EQ(expected_active_ase, data.device->HaveActiveAse()); for (ase* ase = data.device->GetFirstActiveAse(); ase; ase = data.device->GetNextActiveAse(ase)) { @@ -472,8 +472,8 @@ class LeAudioAseConfigurationTest : public Test { GetAudioChannelCounts(*ase->codec_config.audio_channel_allocation); } - ASSERT_EQ(data.active_channel_num_snk, active_channel_num_snk); - ASSERT_EQ(data.active_channel_num_src, active_channel_num_src); + ASSERT_EQ(data.expected_active_channel_num_snk, active_channel_num_snk); + ASSERT_EQ(data.expected_active_channel_num_src, active_channel_num_src); } void SetCisInformationToActiveAse(void) { @@ -497,8 +497,8 @@ class LeAudioAseConfigurationTest : public Test { // the configuration should fail if there are no active ases expected bool success_expected = data_size > 0; for (int i = 0; i < data_size; i++) { - success_expected &= - (data[i].active_channel_num_snk + data[i].active_channel_num_src) > 0; + success_expected &= (data[i].expected_active_channel_num_snk + + data[i].expected_active_channel_num_src) > 0; /* Prepare PAC's */ PublishedAudioCapabilitiesBuilder snk_pac_builder, src_pac_builder; @@ -516,78 +516,101 @@ class LeAudioAseConfigurationTest : public Test { /* Stimulate update of active context map */ group_->UpdateActiveContextsMap(static_cast<uint16_t>(context_type)); - ASSERT_EQ(success_expected, group_->Configure(context_type)); + ASSERT_EQ( + success_expected, + group_->Configure(context_type, + AudioContexts(static_cast<uint16_t>(context_type)))); for (int i = 0; i < data_size; i++) { TestGroupAseConfigurationVerdict(data[i]); } } + int getNumOfAses(LeAudioDevice* device, uint8_t direction) { + return std::count_if( + device->ases_.begin(), device->ases_.end(), + [direction](auto& a) { return a.direction == direction; }); + } + void TestGroupAseConfiguration(LeAudioContextType context_type, TestGroupAseConfigurationData* data, uint8_t data_size) { const auto* configurations = ::le_audio::AudioSetConfigurationProvider::Get()->GetConfigurations( context_type); + + int num_of_matching_configurations = 0; + bool success_expected = data_size > 0; for (const auto& audio_set_conf : *configurations) { + bool interesting_configuration = true; // the configuration should fail if there are no active ases expected - bool success_expected = data_size > 0; - bool not_matching_scenario = false; - uint8_t snk_ases_cnt = 0; - uint8_t src_ases_cnt = 0; PublishedAudioCapabilitiesBuilder snk_pac_builder, src_pac_builder; snk_pac_builder.Reset(); src_pac_builder.Reset(); + /* Let's go thru devices in the group and configure them*/ for (int i = 0; i < data_size; i++) { - success_expected &= (data[i].active_channel_num_snk + - data[i].active_channel_num_src) > 0; - - /* Prepare PAC's */ - /* Note this test requires that reach TwoStereoChan configuration - * version has similar version for OneStereoChan (both SingleDev, - * DualDev). This is just how the test is created and this limitation - * should be removed b/230107540 - */ + success_expected &= (data[i].expected_active_channel_num_snk + + data[i].expected_active_channel_num_src) > 0; + int num_of_ase_snk_per_dev = 0; + int num_of_ase_src_per_dev = 0; + + /* Prepare PAC's for each device. Also make sure configuration is in our + * interest to test */ for (const auto& entry : (*audio_set_conf).confs) { - /* Configuration requires more devices than are supplied */ - if (entry.device_cnt > data_size) { - not_matching_scenario = true; - break; + /* We are interested in the configurations which contains exact number + * of devices and number of ases is same the number of expected ases + * to active + */ + if (entry.device_cnt != data_size) { + interesting_configuration = false; } + if (entry.direction == kLeAudioDirectionSink) { - snk_ases_cnt += entry.ase_cnt; + num_of_ase_snk_per_dev = entry.ase_cnt / data_size; snk_pac_builder.Add(entry.codec, data[i].audio_channel_counts_snk); } else { + num_of_ase_src_per_dev = entry.ase_cnt / data_size; src_pac_builder.Add(entry.codec, data[i].audio_channel_counts_src); } - } - /* Scenario requires more ASEs than defined requirement */ - if (snk_ases_cnt < data[i].audio_channel_counts_snk || - src_ases_cnt < data[i].audio_channel_counts_src) { - not_matching_scenario = true; + data[i].device->snk_pacs_ = snk_pac_builder.Get(); + data[i].device->src_pacs_ = src_pac_builder.Get(); } - if (not_matching_scenario) break; + /* Make sure configuration can satisfy number of expected active ASEs*/ + if (num_of_ase_snk_per_dev != data[i].expected_active_channel_num_snk) { + interesting_configuration = false; + } - data[i].device->snk_pacs_ = snk_pac_builder.Get(); - data[i].device->src_pacs_ = src_pac_builder.Get(); + if (num_of_ase_src_per_dev != data[i].expected_active_channel_num_src) { + interesting_configuration = false; + } } - - if (not_matching_scenario) continue; - /* Stimulate update of active context map */ group_->UpdateActiveContextsMap(static_cast<uint16_t>(context_type)); - ASSERT_EQ(success_expected, group_->Configure(context_type)); - - for (int i = 0; i < data_size; i++) { - TestGroupAseConfigurationVerdict(data[i]); + auto configuration_result = group_->Configure( + context_type, AudioContexts(static_cast<uint16_t>(context_type))); + + /* In case of configuration #ase is same as the one we expected to be + * activated verify, ASEs are actually active */ + if (interesting_configuration) { + ASSERT_TRUE(configuration_result); + num_of_matching_configurations++; + /* Check if each of the devices has activated ASEs as expected */ + for (int i = 0; i < data_size; i++) { + TestGroupAseConfigurationVerdict(data[i]); + } } - group_->Deactivate(); TestAsesInactive(); } + + if (success_expected) { + ASSERT_TRUE((num_of_matching_configurations > 0)); + } else { + ASSERT_TRUE(num_of_matching_configurations == 0); + } } void TestAsesActive(LeAudioCodecId codec_id, uint8_t sampling_frequency, @@ -678,7 +701,10 @@ class LeAudioAseConfigurationTest : public Test { /* Stimulate update of active context map */ group_->UpdateActiveContextsMap( static_cast<uint16_t>(context_type)); - ASSERT_EQ(success_expected, group_->Configure(context_type)); + ASSERT_EQ(success_expected, + group_->Configure( + context_type, + AudioContexts(static_cast<uint16_t>(context_type)))); if (success_expected) { TestAsesActive(LeAudioCodecIdLc3, sampling_frequency, frame_duration, octets_per_frame); @@ -703,12 +729,12 @@ TEST_F(LeAudioAseConfigurationTest, test_mono_speaker_ringtone) { LeAudioDevice* mono_speaker = AddTestDevice(1, 1); TestGroupAseConfigurationData data( {mono_speaker, kLeAudioCodecLC3ChannelCountSingleChannel, - kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0}); + kLeAudioCodecLC3ChannelCountSingleChannel, 1, 1}); TestGroupAseConfiguration(LeAudioContextType::RINGTONE, &data, 1); } -TEST_F(LeAudioAseConfigurationTest, test_mono_speaker_conversional) { +TEST_F(LeAudioAseConfigurationTest, test_mono_speaker_conversational) { LeAudioDevice* mono_speaker = AddTestDevice(1, 0); TestGroupAseConfigurationData data({mono_speaker, kLeAudioCodecLC3ChannelCountSingleChannel, @@ -730,7 +756,7 @@ TEST_F(LeAudioAseConfigurationTest, test_bounded_headphones_ringtone) { LeAudioDevice* bounded_headphones = AddTestDevice(2, 1); TestGroupAseConfigurationData data( {bounded_headphones, kLeAudioCodecLC3ChannelCountTwoChannel, - kLeAudioCodecLC3ChannelCountSingleChannel, 2, 0}); + kLeAudioCodecLC3ChannelCountSingleChannel, 2, 1}); TestGroupAseConfiguration(LeAudioContextType::RINGTONE, &data, 1); } @@ -757,7 +783,7 @@ TEST_F(LeAudioAseConfigurationTest, test_bounded_headset_ringtone) { LeAudioDevice* bounded_headset = AddTestDevice(2, 1); TestGroupAseConfigurationData data( {bounded_headset, kLeAudioCodecLC3ChannelCountTwoChannel, - kLeAudioCodecLC3ChannelCountSingleChannel, 2, 0}); + kLeAudioCodecLC3ChannelCountSingleChannel, 2, 1}); TestGroupAseConfiguration(LeAudioContextType::RINGTONE, &data, 1); } @@ -785,9 +811,9 @@ TEST_F(LeAudioAseConfigurationTest, test_earbuds_ringtone) { LeAudioDevice* right = AddTestDevice(1, 1); TestGroupAseConfigurationData data[] = { {left, kLeAudioCodecLC3ChannelCountSingleChannel, - kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0}, + kLeAudioCodecLC3ChannelCountSingleChannel, 1, 1}, {right, kLeAudioCodecLC3ChannelCountSingleChannel, - kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0}}; + kLeAudioCodecLC3ChannelCountSingleChannel, 1, 1}}; TestGroupAseConfiguration(LeAudioContextType::RINGTONE, data, 2); } @@ -820,7 +846,7 @@ TEST_F(LeAudioAseConfigurationTest, test_handsfree_ringtone) { LeAudioDevice* handsfree = AddTestDevice(1, 1); TestGroupAseConfigurationData data( {handsfree, kLeAudioCodecLC3ChannelCountSingleChannel, - kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0}); + kLeAudioCodecLC3ChannelCountSingleChannel, 1, 1}); TestGroupAseConfiguration(LeAudioContextType::RINGTONE, &data, 1); } @@ -898,7 +924,9 @@ TEST_F(LeAudioAseConfigurationTest, test_unsupported_codec) { device->snk_pacs_ = pac_builder.Get(); device->src_pacs_ = pac_builder.Get(); - ASSERT_FALSE(group_->Configure(LeAudioContextType::RINGTONE)); + ASSERT_FALSE(group_->Configure( + LeAudioContextType::RINGTONE, + AudioContexts(static_cast<uint16_t>(LeAudioContextType::RINGTONE)))); TestAsesInactive(); } @@ -936,11 +964,12 @@ TEST_F(LeAudioAseConfigurationTest, test_reconnection_media) { *ase->codec_config.audio_channel_allocation; /* Get entry for the sink direction and use it to set configuration */ + std::vector<uint8_t> ccid_list; for (auto& ent : configuration->confs) { if (ent.direction == ::le_audio::types::kLeAudioDirectionSink) { left->ConfigureAses(ent, group_->GetCurrentContextType(), &number_of_active_ases, group_snk_audio_location, - group_src_audio_location); + group_src_audio_location, false, 0, ccid_list); } } diff --git a/system/bta/le_audio/le_audio_client_test.cc b/system/bta/le_audio/le_audio_client_test.cc index f77f7bf952..8a7bd0139d 100644 --- a/system/bta/le_audio/le_audio_client_test.cc +++ b/system/bta/le_audio/le_audio_client_test.cc @@ -550,10 +550,11 @@ class UnicastTestNoInit : public Test { ON_CALL(mock_state_machine_, Initialize(_)) .WillByDefault(SaveArg<0>(&state_machine_callbacks_)); - ON_CALL(mock_state_machine_, ConfigureStream(_, _, _)) + ON_CALL(mock_state_machine_, ConfigureStream(_, _, _, _)) .WillByDefault([this](LeAudioDeviceGroup* group, types::LeAudioContextType context_type, - int ccid) { + types::AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list) { bool isReconfiguration = group->IsPendingConfiguration(); /* This shall be called only for user reconfiguration */ @@ -570,7 +571,8 @@ class UnicastTestNoInit : public Test { group->CigClearCis(); /* end */ - if (!group->Configure(context_type, ccid)) { + if (!group->Configure(context_type, metadata_context_type, + ccid_list)) { LOG_ERROR("Could not configure ASEs for group %d content type %d", group->group_id_, int(context_type)); @@ -615,7 +617,8 @@ class UnicastTestNoInit : public Test { return false; } - group->Configure(group->GetContextType()); + group->Configure(group->GetContextType(), + static_cast<uint16_t>(group->GetContextType()), {}); if (!group->CigAssignCisIds(leAudioDevice)) return false; group->CigAssignCisConnHandlesToAses(leAudioDevice); @@ -693,10 +696,11 @@ class UnicastTestNoInit : public Test { return true; }); - ON_CALL(mock_state_machine_, StartStream(_, _, _)) + ON_CALL(mock_state_machine_, StartStream(_, _, _, _)) .WillByDefault([this](LeAudioDeviceGroup* group, types::LeAudioContextType context_type, - int ccid) { + types::AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list) { if (group->GetState() == types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) { if (group->GetContextType() != context_type) { @@ -717,7 +721,8 @@ class UnicastTestNoInit : public Test { group->CigClearCis(); /* end */ - if (!group->Configure(context_type, ccid)) { + if (!group->Configure(context_type, metadata_context_type, + ccid_list)) { LOG(ERROR) << __func__ << ", failed to set ASE configuration"; return false; } @@ -2870,11 +2875,11 @@ TEST_F(UnicastTest, RemoveWhileStreaming) { ASSERT_NE(group_id, bluetooth::groups::kGroupUnknown); // Start streaming - uint8_t cis_count_out = 1; - uint8_t cis_count_in = 0; + constexpr uint8_t cis_count_out = 1; + constexpr uint8_t cis_count_in = 0; - int gmcs_ccid = 1; - int gtbs_ccid = 2; + constexpr int gmcs_ccid = 1; + constexpr int gtbs_ccid = 2; // Audio sessions are started only when device gets active EXPECT_CALL(*mock_unicast_audio_source_, Start(_, _)).Times(1); @@ -2883,7 +2888,8 @@ TEST_F(UnicastTest, RemoveWhileStreaming) { LeAudioClient::Get()->SetCcidInformation(gtbs_ccid, 2 /* Phone */); LeAudioClient::Get()->GroupSetActive(group_id); - EXPECT_CALL(mock_state_machine_, StartStream(_, _, gmcs_ccid)).Times(1); + EXPECT_CALL(mock_state_machine_, StartStream(_, _, _, {{gmcs_ccid}})) + .Times(1); StartStreaming(AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, group_id); @@ -3139,7 +3145,7 @@ TEST_F(UnicastTest, TwoEarbudsStreamingContextSwitchSimple) { // Start streaming with reconfiguration from default media stream setup EXPECT_CALL( mock_state_machine_, - StartStream(_, le_audio::types::LeAudioContextType::NOTIFICATIONS, _)) + StartStream(_, le_audio::types::LeAudioContextType::NOTIFICATIONS, _, _)) .Times(1); StartStreaming(AUDIO_USAGE_NOTIFICATION, AUDIO_CONTENT_TYPE_UNKNOWN, @@ -3154,7 +3160,7 @@ TEST_F(UnicastTest, TwoEarbudsStreamingContextSwitchSimple) { EXPECT_CALL(*mock_unicast_audio_source_, Stop).Times(0); EXPECT_CALL(*mock_unicast_audio_source_, Start).Times(0); EXPECT_CALL(mock_state_machine_, - StartStream(_, le_audio::types::LeAudioContextType::ALERTS, _)) + StartStream(_, le_audio::types::LeAudioContextType::ALERTS, _, _)) .Times(1); UpdateMetadata(AUDIO_USAGE_ALARM, AUDIO_CONTENT_TYPE_UNKNOWN); Mock::VerifyAndClearExpectations(&mock_client_callbacks_); @@ -3167,7 +3173,7 @@ TEST_F(UnicastTest, TwoEarbudsStreamingContextSwitchSimple) { EXPECT_CALL( mock_state_machine_, - StartStream(_, le_audio::types::LeAudioContextType::EMERGENCYALARM, _)) + StartStream(_, le_audio::types::LeAudioContextType::EMERGENCYALARM, _, _)) .Times(1); UpdateMetadata(AUDIO_USAGE_EMERGENCY, AUDIO_CONTENT_TYPE_UNKNOWN); Mock::VerifyAndClearExpectations(mock_unicast_audio_source_); @@ -3199,8 +3205,8 @@ TEST_F(UnicastTest, TwoEarbudsStreamingContextSwitchReconfigure) { codec_spec_conf::kLeAudioLocationFrontRight, group_size, group_id, 2 /* rank*/, true /*connect_through_csis*/); - int gmcs_ccid = 1; - int gtbs_ccid = 2; + constexpr int gmcs_ccid = 1; + constexpr int gtbs_ccid = 2; // Start streaming MEDIA EXPECT_CALL(*mock_unicast_audio_source_, Start(_, _)).Times(1); @@ -3209,7 +3215,8 @@ TEST_F(UnicastTest, TwoEarbudsStreamingContextSwitchReconfigure) { LeAudioClient::Get()->SetCcidInformation(gtbs_ccid, 2 /* Phone */); LeAudioClient::Get()->GroupSetActive(group_id); - EXPECT_CALL(mock_state_machine_, StartStream(_, _, gmcs_ccid)).Times(1); + EXPECT_CALL(mock_state_machine_, StartStream(_, _, _, {{gmcs_ccid}})) + .Times(1); StartStreaming(AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, group_id); Mock::VerifyAndClearExpectations(&mock_client_callbacks_); @@ -3227,7 +3234,8 @@ TEST_F(UnicastTest, TwoEarbudsStreamingContextSwitchReconfigure) { fake_osi_alarm_set_on_mloop_.cb(fake_osi_alarm_set_on_mloop_.data); Mock::VerifyAndClearExpectations(&mock_client_callbacks_); - EXPECT_CALL(mock_state_machine_, StartStream(_, _, gtbs_ccid)).Times(1); + EXPECT_CALL(mock_state_machine_, StartStream(_, _, _, {{gtbs_ccid}})) + .Times(1); StartStreaming(AUDIO_USAGE_VOICE_COMMUNICATION, AUDIO_CONTENT_TYPE_SPEECH, group_id); @@ -3470,7 +3478,8 @@ TEST_F(UnicastTest, MicrophoneAttachToCurrentMediaScenario) { EXPECT_CALL( mock_state_machine_, - StartStream(_, le_audio::types::LeAudioContextType::VOICEASSISTANTS, _)) + StartStream(_, le_audio::types::LeAudioContextType::VOICEASSISTANTS, _, + _)) .Times(1); Mock::VerifyAndClearExpectations(&mock_client_callbacks_); Mock::VerifyAndClearExpectations(mock_unicast_audio_source_); diff --git a/system/bta/le_audio/le_audio_types.cc b/system/bta/le_audio/le_audio_types.cc index 5979d73afc..8920052c28 100644 --- a/system/bta/le_audio/le_audio_types.cc +++ b/system/bta/le_audio/le_audio_types.cc @@ -71,8 +71,12 @@ static uint8_t min_req_devices_cnt( void get_cis_count(const AudioSetConfigurations* audio_set_confs, types::LeAudioConfigurationStrategy strategy, + int group_ase_snk_cnt, int group_ase_src_count, uint8_t* cis_count_bidir, uint8_t* cis_count_unidir_sink, uint8_t* cis_count_unidir_source) { + LOG_INFO(" strategy %d, sink ases: %d, source ases %d", + static_cast<int>(strategy), group_ase_snk_cnt, group_ase_src_count); + for (auto audio_set_conf : *audio_set_confs) { std::pair<uint8_t /* sink */, uint8_t /* source */> snk_src_pair(0, 0); uint8_t bidir_count = 0; @@ -83,8 +87,8 @@ void get_cis_count(const AudioSetConfigurations* audio_set_confs, bool stategy_mismatch = false; for (auto ent : (*audio_set_conf).confs) { if (ent.strategy != strategy) { - LOG_INFO("Strategy does not match (%d != %d)- skip this configuration", - static_cast<int>(ent.strategy), static_cast<int>(strategy)); + LOG_DEBUG("Strategy does not match (%d != %d)- skip this configuration", + static_cast<int>(ent.strategy), static_cast<int>(strategy)); stategy_mismatch = true; break; } @@ -100,6 +104,42 @@ void get_cis_count(const AudioSetConfigurations* audio_set_confs, continue; } + /* Before we start adding another CISes, ignore scenarios which cannot + * satisfied because of the number of ases + */ + + if (group_ase_snk_cnt == 0 && snk_src_pair.first > 0) { + LOG_DEBUG("Group does not have sink ASEs"); + continue; + } + + if (group_ase_src_count == 0 && snk_src_pair.second > 0) { + LOG_DEBUG("Group does not have source ASEs"); + continue; + } + + /* Configuration list is set in the prioritized order. + * it might happen that more prio configuration can be supported + * and is already taken into account. + * Now let's try to ignore ortogonal configuration which would just + * increase our demant on number of CISes but will never happen + */ + + if (snk_src_pair.first == 0 && + (*cis_count_unidir_sink > 0 || *cis_count_bidir > 0)) { + LOG_DEBUG( + "More prio configuration using sink ASEs has been taken into " + "account"); + continue; + } + if (snk_src_pair.second == 0 && + (*cis_count_unidir_source > 0 || *cis_count_bidir > 0)) { + LOG_DEBUG( + "More prio configuration using source ASEs has been taken into " + "account"); + continue; + } + bidir_count = std::min(snk_src_pair.first, snk_src_pair.second); unidir_sink_count = ((snk_src_pair.first - bidir_count) > 0) ? (snk_src_pair.first - bidir_count) @@ -479,24 +519,21 @@ std::string LeAudioLtvMap::ToString() const { } // namespace types void AppendMetadataLtvEntryForCcidList(std::vector<uint8_t>& metadata, - int ccid) { - if (ccid < 0) return; - - std::vector<uint8_t> ccid_ltv_entry; - std::vector<uint8_t> ccid_value = {static_cast<uint8_t>(ccid)}; + const std::vector<uint8_t>& ccid_list) { + if (ccid_list.size() == 0) { + LOG_WARN("Empty CCID list."); + return; + } - ccid_ltv_entry.push_back( - static_cast<uint8_t>(types::kLeAudioMetadataTypeLen + ccid_value.size())); - ccid_ltv_entry.push_back( - static_cast<uint8_t>(types::kLeAudioMetadataTypeCcidList)); - ccid_ltv_entry.insert(ccid_ltv_entry.end(), ccid_value.begin(), - ccid_value.end()); + metadata.push_back( + static_cast<uint8_t>(types::kLeAudioMetadataTypeLen + ccid_list.size())); + metadata.push_back(static_cast<uint8_t>(types::kLeAudioMetadataTypeCcidList)); - metadata.insert(metadata.end(), ccid_ltv_entry.begin(), ccid_ltv_entry.end()); + metadata.insert(metadata.end(), ccid_list.begin(), ccid_list.end()); } void AppendMetadataLtvEntryForStreamingContext( - std::vector<uint8_t>& metadata, LeAudioContextType context_type) { + std::vector<uint8_t>& metadata, types::AudioContexts context_type) { std::vector<uint8_t> streaming_context_ltv_entry; streaming_context_ltv_entry.resize( @@ -510,7 +547,7 @@ void AppendMetadataLtvEntryForStreamingContext( UINT8_TO_STREAM(streaming_context_ltv_entry_buf, types::kLeAudioMetadataTypeStreamingAudioContext); UINT16_TO_STREAM(streaming_context_ltv_entry_buf, - static_cast<uint16_t>(context_type)); + static_cast<uint16_t>(context_type.to_ulong())); metadata.insert(metadata.end(), streaming_context_ltv_entry.begin(), streaming_context_ltv_entry.end()); diff --git a/system/bta/le_audio/le_audio_types.h b/system/bta/le_audio/le_audio_types.h index 30e52a00d8..fd198e19b3 100644 --- a/system/bta/le_audio/le_audio_types.h +++ b/system/bta/le_audio/le_audio_types.h @@ -414,6 +414,7 @@ class LeAudioLtvMap { void Add(uint8_t type, std::vector<uint8_t> value) { values.insert_or_assign(type, std::move(value)); } + void Remove(uint8_t type) { values.erase(type); } bool IsEmpty() const { return values.empty(); } void Clear() { values.clear(); } size_t Size() const { return values.size(); } @@ -668,6 +669,7 @@ static constexpr uint32_t kChannelAllocationStereo = /* Declarations */ void get_cis_count(const AudioSetConfigurations* audio_set_configurations, types::LeAudioConfigurationStrategy strategy, + int group_ase_snk_cnt, int group_ase_src_count, uint8_t* cis_count_bidir, uint8_t* cis_count_unidir_sink, uint8_t* cis_count_unidir_source); bool check_if_may_cover_scenario( @@ -721,9 +723,9 @@ struct stream_configuration { }; void AppendMetadataLtvEntryForCcidList(std::vector<uint8_t>& metadata, - int ccid); + const std::vector<uint8_t>& ccid_list); void AppendMetadataLtvEntryForStreamingContext( - std::vector<uint8_t>& metadata, types::LeAudioContextType context_type); + std::vector<uint8_t>& metadata, types::AudioContexts context_type); uint8_t GetMaxCodecFramesPerSduFromPac(const types::acs_ac_record* pac_record); uint32_t AdjustAllocationForOffloader(uint32_t allocation); } // namespace le_audio
\ No newline at end of file diff --git a/system/bta/le_audio/le_audio_utils.cc b/system/bta/le_audio/le_audio_utils.cc new file mode 100644 index 0000000000..ff83cc8784 --- /dev/null +++ b/system/bta/le_audio/le_audio_utils.cc @@ -0,0 +1,169 @@ +/* + * 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. + */ + +#include "le_audio_utils.h" + +#include "bta/le_audio/content_control_id_keeper.h" +#include "gd/common/strings.h" +#include "le_audio_types.h" +#include "osi/include/log.h" + +using le_audio::types::AudioContexts; +using le_audio::types::LeAudioContextType; + +namespace le_audio { +namespace utils { +LeAudioContextType AudioContentToLeAudioContext( + audio_content_type_t content_type, audio_usage_t usage) { + /* Check audio attribute usage of stream */ + switch (usage) { + case AUDIO_USAGE_MEDIA: + return LeAudioContextType::MEDIA; + case AUDIO_USAGE_VOICE_COMMUNICATION: + case AUDIO_USAGE_CALL_ASSISTANT: + return LeAudioContextType::CONVERSATIONAL; + case AUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING: + if (content_type == AUDIO_CONTENT_TYPE_SPEECH) + return LeAudioContextType::CONVERSATIONAL; + else + return LeAudioContextType::MEDIA; + case AUDIO_USAGE_GAME: + return LeAudioContextType::GAME; + case AUDIO_USAGE_NOTIFICATION: + return LeAudioContextType::NOTIFICATIONS; + case AUDIO_USAGE_NOTIFICATION_TELEPHONY_RINGTONE: + return LeAudioContextType::RINGTONE; + case AUDIO_USAGE_ALARM: + return LeAudioContextType::ALERTS; + case AUDIO_USAGE_EMERGENCY: + return LeAudioContextType::EMERGENCYALARM; + case AUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + return LeAudioContextType::INSTRUCTIONAL; + default: + break; + } + + return LeAudioContextType::MEDIA; +} + +static std::string usageToString(audio_usage_t usage) { + switch (usage) { + case AUDIO_USAGE_UNKNOWN: + return "USAGE_UNKNOWN"; + case AUDIO_USAGE_MEDIA: + return "USAGE_MEDIA"; + case AUDIO_USAGE_VOICE_COMMUNICATION: + return "USAGE_VOICE_COMMUNICATION"; + case AUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING: + return "USAGE_VOICE_COMMUNICATION_SIGNALLING"; + case AUDIO_USAGE_ALARM: + return "USAGE_ALARM"; + case AUDIO_USAGE_NOTIFICATION: + return "USAGE_NOTIFICATION"; + case AUDIO_USAGE_NOTIFICATION_TELEPHONY_RINGTONE: + return "USAGE_NOTIFICATION_TELEPHONY_RINGTONE"; + case AUDIO_USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + return "USAGE_NOTIFICATION_COMMUNICATION_REQUEST"; + case AUDIO_USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + return "USAGE_NOTIFICATION_COMMUNICATION_INSTANT"; + case AUDIO_USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + return "USAGE_NOTIFICATION_COMMUNICATION_DELAYED"; + case AUDIO_USAGE_NOTIFICATION_EVENT: + return "USAGE_NOTIFICATION_EVENT"; + case AUDIO_USAGE_ASSISTANCE_ACCESSIBILITY: + return "USAGE_ASSISTANCE_ACCESSIBILITY"; + case AUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + return "USAGE_ASSISTANCE_NAVIGATION_GUIDANCE"; + case AUDIO_USAGE_ASSISTANCE_SONIFICATION: + return "USAGE_ASSISTANCE_SONIFICATION"; + case AUDIO_USAGE_GAME: + return "USAGE_GAME"; + case AUDIO_USAGE_ASSISTANT: + return "USAGE_ASSISTANT"; + case AUDIO_USAGE_CALL_ASSISTANT: + return "USAGE_CALL_ASSISTANT"; + case AUDIO_USAGE_EMERGENCY: + return "USAGE_EMERGENCY"; + case AUDIO_USAGE_SAFETY: + return "USAGE_SAFETY"; + case AUDIO_USAGE_VEHICLE_STATUS: + return "USAGE_VEHICLE_STATUS"; + case AUDIO_USAGE_ANNOUNCEMENT: + return "USAGE_ANNOUNCEMENT"; + default: + return "unknown usage "; + } +} + +static std::string contentTypeToString(audio_content_type_t content_type) { + switch (content_type) { + case AUDIO_CONTENT_TYPE_UNKNOWN: + return "CONTENT_TYPE_UNKNOWN"; + case AUDIO_CONTENT_TYPE_SPEECH: + return "CONTENT_TYPE_SPEECH"; + case AUDIO_CONTENT_TYPE_MUSIC: + return "CONTENT_TYPE_MUSIC"; + case AUDIO_CONTENT_TYPE_MOVIE: + return "CONTENT_TYPE_MOVIE"; + case AUDIO_CONTENT_TYPE_SONIFICATION: + return "CONTENT_TYPE_SONIFICATION"; + default: + return "unknown content type "; + } +} + +AudioContexts GetAllowedAudioContextsFromSourceMetadata( + const source_metadata_t& source_metadata, AudioContexts allowed_contexts) { + AudioContexts track_contexts(0); + for (auto idx = 0u; idx < source_metadata.track_count; ++idx) { + auto& track = source_metadata.tracks[idx]; + if (track.content_type == 0 && track.usage == 0) continue; + + LOG_INFO("%s: usage=%s(%d), content_type=%s(%d), gain=%f", __func__, + usageToString(track.usage).c_str(), track.usage, + contentTypeToString(track.content_type).c_str(), + track.content_type, track.gain); + + track_contexts |= AudioContexts( + static_cast<std::underlying_type<LeAudioContextType>::type>( + AudioContentToLeAudioContext(track.content_type, track.usage))); + } + track_contexts &= allowed_contexts; + LOG_INFO("%s: allowed context=%lu", __func__, track_contexts.to_ulong()); + + return track_contexts; +} + +std::vector<uint8_t> GetAllCcids(std::bitset<16> context_types) { + auto ccid_keeper = ContentControlIdKeeper::GetInstance(); + std::vector<uint8_t> ccid_vec; + + std::size_t bit_pos = 0; + while (bit_pos < context_types.size()) { + if (context_types.test(bit_pos)) { + auto ccid = ccid_keeper->GetCcid(static_cast<uint16_t>(0b1 << bit_pos)); + if (ccid != -1) { + ccid_vec.push_back(static_cast<uint8_t>(ccid)); + } + } + ++bit_pos; + } + + return ccid_vec; +} + +} // namespace utils +} // namespace le_audio diff --git a/system/bta/le_audio/le_audio_utils.h b/system/bta/le_audio/le_audio_utils.h new file mode 100644 index 0000000000..6596c97323 --- /dev/null +++ b/system/bta/le_audio/le_audio_utils.h @@ -0,0 +1,36 @@ +/* + * 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. + */ + +#pragma once + +#include <hardware/audio.h> + +#include <bitset> +#include <vector> + +#include "le_audio_types.h" + +namespace le_audio { +namespace utils { +types::LeAudioContextType AudioContentToLeAudioContext( + audio_content_type_t content_type, audio_usage_t usage); +types::AudioContexts GetAllowedAudioContextsFromSourceMetadata( + const source_metadata_t& source_metadata, + types::AudioContexts allowed_contexts); +std::vector<uint8_t> GetAllCcids(std::bitset<16> context_types); + +} // namespace utils +} // namespace le_audio diff --git a/system/bta/le_audio/metrics_collector.cc b/system/bta/le_audio/metrics_collector.cc index 4a00d2da84..aed66c006e 100644 --- a/system/bta/le_audio/metrics_collector.cc +++ b/system/bta/le_audio/metrics_collector.cc @@ -16,7 +16,6 @@ #include "metrics_collector.h" -#include <chrono> #include <memory> #include <vector> @@ -24,17 +23,15 @@ namespace le_audio { -using ClockTimePoint = - std::chrono::time_point<std::chrono::high_resolution_clock>; using bluetooth::le_audio::ConnectionState; using le_audio::types::LeAudioContextType; -const static ClockTimePoint kInvalidTimePoint{}; +const static metrics::ClockTimePoint kInvalidTimePoint{}; MetricsCollector* MetricsCollector::instance = nullptr; -inline int64_t get_timedelta_nanos(const ClockTimePoint& t1, - const ClockTimePoint& t2) { +inline int64_t get_timedelta_nanos(const metrics::ClockTimePoint& t1, + const metrics::ClockTimePoint& t2) { if (t1 == kInvalidTimePoint || t2 == kInvalidTimePoint) { return -1; } @@ -79,9 +76,9 @@ inline int32_t to_atom_context_type(const LeAudioContextType stack_type) { class DeviceMetrics { public: RawAddress address_; - ClockTimePoint connecting_timepoint_ = kInvalidTimePoint; - ClockTimePoint connected_timepoint_ = kInvalidTimePoint; - ClockTimePoint disconnected_timepoint_ = kInvalidTimePoint; + metrics::ClockTimePoint connecting_timepoint_ = kInvalidTimePoint; + metrics::ClockTimePoint connected_timepoint_ = kInvalidTimePoint; + metrics::ClockTimePoint disconnected_timepoint_ = kInvalidTimePoint; int32_t connection_status_ = 0; int32_t disconnection_status_ = 0; @@ -114,7 +111,7 @@ class GroupMetricsImpl : public GroupMetrics { int32_t group_size_; std::vector<std::unique_ptr<DeviceMetrics>> device_metrics_; std::unordered_map<RawAddress, DeviceMetrics*> opened_devices_; - ClockTimePoint beginning_timepoint_; + metrics::ClockTimePoint beginning_timepoint_; std::vector<int64_t> streaming_offset_nanos_; std::vector<int64_t> streaming_duration_nanos_; std::vector<int32_t> streaming_context_type_; @@ -283,6 +280,18 @@ void MetricsCollector::OnStreamEnded(int32_t group_id) { } } +void MetricsCollector::OnBroadcastStateChanged(bool started) { + if (started) { + broadcast_beginning_timepoint_ = std::chrono::high_resolution_clock::now(); + } else { + auto broadcast_ending_timepoint_ = + std::chrono::high_resolution_clock::now(); + bluetooth::common::LogLeAudioBroadcastSessionReported(get_timedelta_nanos( + broadcast_beginning_timepoint_, broadcast_ending_timepoint_)); + broadcast_beginning_timepoint_ = kInvalidTimePoint; + } +} + void MetricsCollector::Flush() { LOG(INFO) << __func__; for (auto& p : opened_groups_) { diff --git a/system/bta/le_audio/metrics_collector.h b/system/bta/le_audio/metrics_collector.h index f988364734..1e235dfdc4 100644 --- a/system/bta/le_audio/metrics_collector.h +++ b/system/bta/le_audio/metrics_collector.h @@ -18,6 +18,7 @@ #include <hardware/bt_le_audio.h> +#include <chrono> #include <cstdint> #include <memory> #include <unordered_map> @@ -27,6 +28,11 @@ namespace le_audio { +namespace metrics { +using ClockTimePoint = + std::chrono::time_point<std::chrono::high_resolution_clock>; +} + enum ConnectionStatus : int32_t { UNKNOWN = 0, SUCCESS = 1, @@ -117,6 +123,13 @@ class MetricsCollector { void OnStreamEnded(int32_t group_id); /** + * When there is a change in Bluetooth LE Audio broadcast state + * + * @param started if broadcast streaming is started. + */ + void OnBroadcastStateChanged(bool started); + + /** * Flush all log to statsd * * @param group_id Group ID of the associated stream. @@ -131,6 +144,8 @@ class MetricsCollector { std::unordered_map<int32_t, std::unique_ptr<GroupMetrics>> opened_groups_; std::unordered_map<int32_t, int32_t> group_size_table_; + + metrics::ClockTimePoint broadcast_beginning_timepoint_; }; } // namespace le_audio diff --git a/system/bta/le_audio/metrics_collector_linux.cc b/system/bta/le_audio/metrics_collector_linux.cc index 2ac63a567b..cb36fbec2e 100644 --- a/system/bta/le_audio/metrics_collector_linux.cc +++ b/system/bta/le_audio/metrics_collector_linux.cc @@ -40,6 +40,8 @@ void MetricsCollector::OnStreamStarted( void MetricsCollector::OnStreamEnded(int32_t group_id) {} +void MetricsCollector::OnBroadcastStateChanged(bool started) {} + void MetricsCollector::Flush() {} } // namespace le_audio diff --git a/system/bta/le_audio/metrics_collector_test.cc b/system/bta/le_audio/metrics_collector_test.cc index ee295d56d7..49c13bf05d 100644 --- a/system/bta/le_audio/metrics_collector_test.cc +++ b/system/bta/le_audio/metrics_collector_test.cc @@ -44,6 +44,7 @@ int log_count = 0; int32_t last_group_size; int32_t last_group_metric_id; int64_t last_connection_duration_nanos; +int64_t last_broadcast_duration_nanos; std::vector<int64_t> last_device_connecting_offset_nanos; std::vector<int64_t> last_device_connected_offset_nanos; std::vector<int64_t> last_device_connection_duration_nanos; @@ -84,6 +85,10 @@ void LogLeAudioConnectionSessionReported( last_streaming_context_type = streaming_context_type; } +void LogLeAudioBroadcastSessionReported(int64_t duration_nanos) { + last_broadcast_duration_nanos = duration_nanos; +} + } // namespace common } // namespace bluetooth @@ -111,6 +116,7 @@ class MetricsCollectorTest : public Test { log_count = 0; last_group_size = 0; last_group_metric_id = 0; + last_broadcast_duration_nanos = 0; last_connection_duration_nanos = 0; last_device_connecting_offset_nanos = {}; last_device_connected_offset_nanos = {}; @@ -367,4 +373,15 @@ TEST_F(MetricsCollectorTest, StreamingSessions) { static_cast<int32_t>(LeAudioMetricsContextType::COMMUNICATION)); } +TEST_F(MetricsCollectorTest, BroadastSessions) { + last_broadcast_duration_nanos = 0; + collector->OnBroadcastStateChanged(true); + collector->OnBroadcastStateChanged(false); + ASSERT_GT(last_broadcast_duration_nanos, 0); + last_broadcast_duration_nanos = 0; + collector->OnBroadcastStateChanged(true); + collector->OnBroadcastStateChanged(false); + ASSERT_GT(last_broadcast_duration_nanos, 0); +} + } // namespace le_audio
\ No newline at end of file diff --git a/system/bta/le_audio/mock_state_machine.h b/system/bta/le_audio/mock_state_machine.h index 63e850b8e1..ac98e45161 100644 --- a/system/bta/le_audio/mock_state_machine.h +++ b/system/bta/le_audio/mock_state_machine.h @@ -25,7 +25,9 @@ class MockLeAudioGroupStateMachine : public le_audio::LeAudioGroupStateMachine { public: MOCK_METHOD((bool), StartStream, (le_audio::LeAudioDeviceGroup * group, - le_audio::types::LeAudioContextType context_type, int ccid), + le_audio::types::LeAudioContextType context_type, + le_audio::types::AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list), (override)); MOCK_METHOD((bool), AttachToStream, (le_audio::LeAudioDeviceGroup * group, @@ -35,7 +37,9 @@ class MockLeAudioGroupStateMachine : public le_audio::LeAudioGroupStateMachine { (override)); MOCK_METHOD((bool), ConfigureStream, (le_audio::LeAudioDeviceGroup * group, - le_audio::types::LeAudioContextType context_type, int ccid), + le_audio::types::LeAudioContextType context_type, + le_audio::types::AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list), (override)); MOCK_METHOD((void), StopStream, (le_audio::LeAudioDeviceGroup * group), (override)); diff --git a/system/bta/le_audio/state_machine.cc b/system/bta/le_audio/state_machine.cc index 713be3ac68..b9e9528767 100644 --- a/system/bta/le_audio/state_machine.cc +++ b/system/bta/le_audio/state_machine.cc @@ -99,6 +99,7 @@ using le_audio::LeAudioGroupStateMachine; using le_audio::types::ase; using le_audio::types::AseState; +using le_audio::types::AudioContexts; using le_audio::types::AudioStreamDataPathState; using le_audio::types::CodecLocation; @@ -156,7 +157,8 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { bool StartStream(LeAudioDeviceGroup* group, le_audio::types::LeAudioContextType context_type, - int ccid) override { + AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list) override { LOG_INFO(" current state: %s", ToString(group->GetState()).c_str()); switch (group->GetState()) { @@ -177,7 +179,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { /* If configuration is needed */ FALLTHROUGH; case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE: - if (!group->Configure(context_type, ccid)) { + if (!group->Configure(context_type, metadata_context_type, ccid_list)) { LOG(ERROR) << __func__ << ", failed to set ASE configuration"; return false; } @@ -206,7 +208,8 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { /* This case just updates the metadata for the stream, in case * stream configuration is satisfied */ - if (!group->IsMetadataChanged(context_type, ccid)) return true; + if (!group->IsMetadataChanged(metadata_context_type, ccid_list)) + return true; LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice(); if (!leAudioDevice) { @@ -214,7 +217,8 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { return false; } - PrepareAndSendUpdateMetadata(group, leAudioDevice, context_type, ccid); + PrepareAndSendUpdateMetadata(group, leAudioDevice, + metadata_context_type, ccid_list); break; } @@ -229,7 +233,8 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { bool ConfigureStream(LeAudioDeviceGroup* group, le_audio::types::LeAudioContextType context_type, - int ccid) override { + AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list) override { if (group->GetState() > AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED) { LOG_ERROR( "Stream should be stopped or in configured stream. Current state: %s", @@ -239,7 +244,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { ReleaseCisIds(group); - if (!group->Configure(context_type, ccid)) { + if (!group->Configure(context_type, metadata_context_type, ccid_list)) { LOG_ERROR("Could not configure ASEs for group %d content type %d", group->group_id_, int(context_type)); @@ -727,6 +732,12 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { AudioStreamDataPathState::DATA_PATH_ESTABLISHED) { value |= bluetooth::hci::iso_manager::kRemoveIsoDataPathDirectionOutput; } + + if (value == 0) { + LOG_INFO("Data path was not set. Nothing to do here."); + return; + } + IsoManager::GetInstance()->RemoveIsoDataPath(cis_conn_hdl, value); } @@ -737,6 +748,11 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { FreeLinkQualityReports(leAudioDevice); + /* If this is peer disconnecting CIS, make sure to clear data path */ + if (event->reason != HCI_ERR_CONN_CAUSE_LOCAL_HOST) { + RemoveDataPathByCisHandle(leAudioDevice, event->cis_conn_hdl); + } + auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(event->cis_conn_hdl); if (ases_pair.sink) { ases_pair.sink->data_path_state = AudioStreamDataPathState::CIS_ASSIGNED; @@ -922,6 +938,17 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { *ase->codec_config.codec_frames_blocks_per_sdu); } + if (stream_conf->sink_frame_duration_us == 0) { + stream_conf->sink_frame_duration_us = + ase->codec_config.GetFrameDurationUs(); + } else { + ASSERT_LOG(stream_conf->sink_frame_duration_us == + ase->codec_config.GetFrameDurationUs(), + "frame_duration_us: %d!=%d", + stream_conf->sink_frame_duration_us, + ase->codec_config.GetFrameDurationUs()); + } + LOG_INFO( " Added Sink Stream Configuration. CIS Connection Handle: %d" ", Audio Channel Allocation: %d" @@ -981,6 +1008,17 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { *ase->codec_config.codec_frames_blocks_per_sdu); } + if (stream_conf->source_frame_duration_us == 0) { + stream_conf->source_frame_duration_us = + ase->codec_config.GetFrameDurationUs(); + } else { + ASSERT_LOG(stream_conf->source_frame_duration_us == + ase->codec_config.GetFrameDurationUs(), + "frame_duration_us: %d!=%d", + stream_conf->source_frame_duration_us, + ase->codec_config.GetFrameDurationUs()); + } + LOG_INFO( " Added Source Stream Configuration. CIS Connection Handle: %d" ", Audio Channel Allocation: %d" @@ -1888,17 +1926,18 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { GATT_WRITE_NO_RSP, NULL, NULL); } - void PrepareAndSendUpdateMetadata( - LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice, - le_audio::types::LeAudioContextType context_type, int ccid) { + void PrepareAndSendUpdateMetadata(LeAudioDeviceGroup* group, + LeAudioDevice* leAudioDevice, + le_audio::types::AudioContexts context_type, + const std::vector<uint8_t>& ccid_list) { std::vector<struct le_audio::client_parser::ascs::ctp_update_metadata> confs; for (; leAudioDevice; leAudioDevice = group->GetNextActiveDevice(leAudioDevice)) { - if (!leAudioDevice->IsMetadataChanged(context_type, ccid)) continue; + if (!leAudioDevice->IsMetadataChanged(context_type, ccid_list)) continue; - auto new_metadata = leAudioDevice->GetMetadata(context_type, ccid); + auto new_metadata = leAudioDevice->GetMetadata(context_type, ccid_list); /* Request server to update ASEs with new metadata */ for (struct ase* ase = leAudioDevice->GetFirstActiveAse(); ase != nullptr; diff --git a/system/bta/le_audio/state_machine.h b/system/bta/le_audio/state_machine.h index 06b63fb0bf..d4e97c1d07 100644 --- a/system/bta/le_audio/state_machine.h +++ b/system/bta/le_audio/state_machine.h @@ -49,11 +49,13 @@ class LeAudioGroupStateMachine { LeAudioDevice* leAudioDevice) = 0; virtual bool StartStream(LeAudioDeviceGroup* group, types::LeAudioContextType context_type, - int ccid = -1) = 0; + types::AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list = {}) = 0; virtual void SuspendStream(LeAudioDeviceGroup* group) = 0; virtual bool ConfigureStream(LeAudioDeviceGroup* group, - le_audio::types::LeAudioContextType context_type, - int ccid = -1) = 0; + types::LeAudioContextType context_type, + types::AudioContexts metadata_context_type, + std::vector<uint8_t> ccid_list = {}) = 0; virtual void StopStream(LeAudioDeviceGroup* group) = 0; virtual void ProcessGattNotifEvent(uint8_t* value, uint16_t len, struct types::ase* ase, diff --git a/system/bta/le_audio/state_machine_test.cc b/system/bta/le_audio/state_machine_test.cc index 2fd0e7865a..da363cf357 100644 --- a/system/bta/le_audio/state_machine_test.cc +++ b/system/bta/le_audio/state_machine_test.cc @@ -1185,7 +1185,8 @@ TEST_F(StateMachineTest, testConfigureCodecSingle) { InjectInitialIdleNotification(group); ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1226,7 +1227,8 @@ TEST_F(StateMachineTest, testConfigureCodecMulti) { // Start the configuration and stream the content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1263,7 +1265,8 @@ TEST_F(StateMachineTest, testConfigureQosSingle) { InjectInitialIdleNotification(group); ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1307,7 +1310,8 @@ TEST_F(StateMachineTest, testConfigureQosMultiple) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1351,7 +1355,8 @@ TEST_F(StateMachineTest, testStreamSingle) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1395,7 +1400,8 @@ TEST_F(StateMachineTest, testStreamSkipEnablingSink) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1440,7 +1446,8 @@ TEST_F(StateMachineTest, testStreamSkipEnablingSinkSource) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1492,7 +1499,8 @@ TEST_F(StateMachineTest, testStreamMultipleConversational) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1543,7 +1551,8 @@ TEST_F(StateMachineTest, testStreamMultiple) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1586,7 +1595,8 @@ TEST_F(StateMachineTest, testDisableSingle) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1653,7 +1663,8 @@ TEST_F(StateMachineTest, testDisableMultiple) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1715,7 +1726,8 @@ TEST_F(StateMachineTest, testDisableBidirectional) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1762,7 +1774,8 @@ TEST_F(StateMachineTest, testReleaseSingle) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1833,7 +1846,8 @@ TEST_F(StateMachineTest, testReleaseCachingSingle) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1906,7 +1920,8 @@ TEST_F(StateMachineTest, testStreamCachingSingle) { // Start the configuration and stream Ringtone content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1921,7 +1936,8 @@ TEST_F(StateMachineTest, testStreamCachingSingle) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -1991,7 +2007,8 @@ TEST_F(StateMachineTest, testActivateStreamCachingSingle) { // Start the configuration and stream Conversational content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -2007,7 +2024,8 @@ TEST_F(StateMachineTest, testActivateStreamCachingSingle) { // Start the configuration and stream Media content context_type = kContextTypeMedia; LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -2054,7 +2072,8 @@ TEST_F(StateMachineTest, testReleaseMultiple) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + types::AudioContexts(context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -2110,7 +2129,8 @@ TEST_F(StateMachineTest, testReleaseBidirectional) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + types::AudioContexts(context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -2156,7 +2176,8 @@ TEST_F(StateMachineTest, testDisableAndReleaseBidirectional) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Suspend the stream LeAudioGroupStateMachine::Get()->SuspendStream(group); @@ -2260,7 +2281,8 @@ TEST_F(StateMachineTest, testAseAutonomousRelease) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Validate new GroupStreamStatus EXPECT_CALL(mock_callbacks_, @@ -2326,7 +2348,8 @@ TEST_F(StateMachineTest, testAseAutonomousRelease2Devices) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check streaming will continue EXPECT_CALL(mock_callbacks_, @@ -2367,7 +2390,8 @@ TEST_F(StateMachineTest, testStateTransitionTimeoutOnIdleState) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Disconnect device LeAudioGroupStateMachine::Get()->ProcessHciNotifAclDisconnected( @@ -2398,7 +2422,8 @@ TEST_F(StateMachineTest, testStateTransitionTimeout) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if timeout is fired EXPECT_CALL(mock_callbacks_, OnStateTransitionTimeout(leaudio_group_id)); @@ -2445,7 +2470,8 @@ TEST_F(StateMachineTest, testConfigureDataPathForHost) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); } TEST_F(StateMachineTest, testConfigureDataPathForAdsp) { const auto context_type = kContextTypeRingtone; @@ -2483,7 +2509,8 @@ TEST_F(StateMachineTest, testConfigureDataPathForAdsp) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); } TEST_F(StateMachineTest, testStreamConfigurationAdsp) { @@ -2516,7 +2543,8 @@ TEST_F(StateMachineTest, testStreamConfigurationAdsp) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -2569,7 +2597,8 @@ TEST_F(StateMachineTest, testStreamConfigurationAdspNoDownMix) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); ASSERT_EQ(static_cast<int>(group->stream_conf.sink_offloader_streams.size()), 2); @@ -2630,7 +2659,8 @@ TEST_F(StateMachineTest, testStreamConfigurationAdspDownMix) { // Start the configuration and stream Media content ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type))); + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); ASSERT_EQ(static_cast<int>(group->stream_conf.sink_offloader_streams.size()), 2); @@ -2715,7 +2745,8 @@ TEST_F(StateMachineTest, testAttachDeviceToTheStream) { // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( - group, static_cast<types::LeAudioContextType>(context_type)); + group, static_cast<types::LeAudioContextType>(context_type), + context_type); // Check if group has transitioned to a proper state ASSERT_EQ(group->GetState(), @@ -2752,12 +2783,14 @@ TEST_F(StateMachineTest, testAttachDeviceToTheStream) { source_num_of_active_ases++; } + std::vector<uint8_t> ccid_list; for (auto& ent : stream_conf->conf->confs) { if (ent.direction == le_audio::types::kLeAudioDirectionSink) { /* Sink*/ if (!lastDevice->ConfigureAses( ent, group->GetCurrentContextType(), &sink_num_of_active_ases, - sink_group_audio_locations, source_group_audio_locations, true)) { + sink_group_audio_locations, source_group_audio_locations, true, 0, + ccid_list)) { FAIL() << __func__ << " Could not set sink configuration of " << stream_conf->conf->name; } @@ -2765,7 +2798,8 @@ TEST_F(StateMachineTest, testAttachDeviceToTheStream) { /* Source*/ if (!lastDevice->ConfigureAses( ent, group->GetCurrentContextType(), &source_num_of_active_ases, - sink_group_audio_locations, source_group_audio_locations, true)) { + sink_group_audio_locations, source_group_audio_locations, true, 0, + ccid_list)) { FAIL() << __func__ << " Could not set source configuration of " << stream_conf->conf->name; } diff --git a/system/bta/vc/vc.cc b/system/bta/vc/vc.cc index 7475e63e32..70f82e16f5 100644 --- a/system/bta/vc/vc.cc +++ b/system/bta/vc/vc.cc @@ -1051,7 +1051,7 @@ class VolumeControlImpl : public VolumeControl { case BTA_GATTC_ENC_CMPL_CB_EVT: { uint8_t encryption_status; - if (!BTM_IsEncrypted(p_data->enc_cmpl.remote_bda, BT_TRANSPORT_LE)) { + if (BTM_IsEncrypted(p_data->enc_cmpl.remote_bda, BT_TRANSPORT_LE)) { encryption_status = BTM_SUCCESS; } else { encryption_status = BTM_FAILED_ON_SECURITY; diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc index 0d5a11b952..6718de7220 100644 --- a/system/btif/src/btif_dm.cc +++ b/system/btif/src/btif_dm.cc @@ -1314,6 +1314,13 @@ static void btif_dm_search_devices_evt(tBTA_DM_SEARCH_EVT event, } #if TARGET_FLOSS + std::vector<uint8_t> property_value; + for (auto uuid : uuid_iter->second) { + auto uuid_128bit = uuid.To128BitBE(); + property_value.insert(property_value.end(), uuid_128bit.begin(), + uuid_128bit.end()); + } + // Floss expects that EIR uuids are immediately reported when the // device is found and doesn't wait for the pairing intent. // @@ -1321,8 +1328,8 @@ static void btif_dm_search_devices_evt(tBTA_DM_SEARCH_EVT event, // existing UUIDs. BTIF_STORAGE_FILL_PROPERTY( &properties[num_properties], BT_PROPERTY_UUIDS, - pairing_cb.num_eir_uuids * Uuid::kNumBytes128, - pairing_cb.eir_uuids); + uuid_iter->second.size() * Uuid::kNumBytes128, + (void*)property_value.data()); num_properties++; #endif } diff --git a/system/common/metrics.cc b/system/common/metrics.cc index 0664186cac..baf838b0c4 100644 --- a/system/common/metrics.cc +++ b/system/common/metrics.cc @@ -962,6 +962,13 @@ void LogLeAudioConnectionSessionReported( } } +void LogLeAudioBroadcastSessionReported(int64_t duration_nanos) { + int ret = stats_write(LE_AUDIO_BROADCAST_SESSION_REPORTED, duration_nanos); + if (ret < 0) { + LOG(WARNING) << __func__ << ": failed for duration=" << duration_nanos; + } +} + } // namespace common } // namespace bluetooth diff --git a/system/common/metrics.h b/system/common/metrics.h index 9d33e1b81f..a0a34b5945 100644 --- a/system/common/metrics.h +++ b/system/common/metrics.h @@ -518,6 +518,8 @@ void LogLeAudioConnectionSessionReported( std::vector<int64_t>& streaming_duration_nanos, std::vector<int32_t>& streaming_context_type); +void LogLeAudioBroadcastSessionReported(int64_t duration_nanos); + } // namespace common } // namespace bluetooth diff --git a/system/common/metrics_linux.cc b/system/common/metrics_linux.cc index fa8c6a0497..883a645e30 100644 --- a/system/common/metrics_linux.cc +++ b/system/common/metrics_linux.cc @@ -167,6 +167,8 @@ void LogLeAudioConnectionSessionReported( std::vector<int64_t>& streaming_duration_nanos, std::vector<int32_t>& streaming_context_type) {} +void LogLeAudioBroadcastSessionReported(int64_t duration_nanos) {} + } // namespace common } // namespace bluetooth diff --git a/system/embdrv/aptx/README.md b/system/embdrv/aptx/README.md new file mode 100644 index 0000000000..4b56c29dce --- /dev/null +++ b/system/embdrv/aptx/README.md @@ -0,0 +1 @@ +# AptX Codec diff --git a/system/embdrv/aptx/aptxhd/README.md b/system/embdrv/aptx/aptxhd/README.md new file mode 100644 index 0000000000..703b1da518 --- /dev/null +++ b/system/embdrv/aptx/aptxhd/README.md @@ -0,0 +1 @@ +# AptX High Definition Codec diff --git a/system/gd/Android.bp b/system/gd/Android.bp index 1c6573aba9..722902e47e 100644 --- a/system/gd/Android.bp +++ b/system/gd/Android.bp @@ -59,7 +59,7 @@ cc_defaults { name: "gd_clang_file_coverage", target: { glibc: { - clang_cflags: [ + cflags: [ "-fprofile-instr-generate", "-fcoverage-mapping", ], diff --git a/system/gd/rust/linux/mgmt/Cargo.toml b/system/gd/rust/linux/mgmt/Cargo.toml index b4a8aa4b12..f16841d153 100644 --- a/system/gd/rust/linux/mgmt/Cargo.toml +++ b/system/gd/rust/linux/mgmt/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" [dependencies] bt_common = { path = "../../common" } bt_topshim = { path = "../../topshim" } -bt_socket = { path = "../socket" } +bt_utils = { path = "../utils" } btstack = { path = "../stack" } # external deps diff --git a/system/gd/rust/linux/mgmt/src/state_machine.rs b/system/gd/rust/linux/mgmt/src/state_machine.rs index 4459f86c2b..d6228d103f 100644 --- a/system/gd/rust/linux/mgmt/src/state_machine.rs +++ b/system/gd/rust/linux/mgmt/src/state_machine.rs @@ -1,7 +1,9 @@ use crate::bluetooth_manager::BluetoothManager; use crate::config_util; use bt_common::time::Alarm; -use bt_socket::{BtSocket, HciChannels, MgmtCommand, MgmtCommandResponse, MgmtEvent, HCI_DEV_NONE}; +use bt_utils::socket::{ + BtSocket, HciChannels, MgmtCommand, MgmtCommandResponse, MgmtEvent, HCI_DEV_NONE, +}; use log::{debug, error, info, warn}; use nix::sys::signal::{self, Signal}; diff --git a/system/gd/rust/linux/socket/Cargo.toml b/system/gd/rust/linux/utils/Cargo.toml index 8ab039eefc..adbf949505 100644 --- a/system/gd/rust/linux/socket/Cargo.toml +++ b/system/gd/rust/linux/utils/Cargo.toml @@ -13,13 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. [package] -name = "bt_socket" +name = "bt_utils" version = "0.0.1" edition = "2021" [dependencies] libc = "0.2" log = "0.4.14" +nix = "0.19" num-derive = "0.3" num-traits = "0.2" diff --git a/system/gd/rust/linux/utils/src/lib.rs b/system/gd/rust/linux/utils/src/lib.rs new file mode 100644 index 0000000000..734756cf46 --- /dev/null +++ b/system/gd/rust/linux/utils/src/lib.rs @@ -0,0 +1,7 @@ +//! Utilities + +#[macro_use] +extern crate num_derive; + +pub mod socket; +pub mod uinput; diff --git a/system/gd/rust/linux/socket/src/lib.rs b/system/gd/rust/linux/utils/src/socket.rs index 8c0c80e4b4..719bbcc36f 100644 --- a/system/gd/rust/linux/socket/src/lib.rs +++ b/system/gd/rust/linux/utils/src/socket.rs @@ -4,9 +4,6 @@ use std::mem; use std::os::unix::io::{AsRawFd, RawFd}; -#[macro_use] -extern crate num_derive; - use libc; use log::debug; use num_traits::cast::{FromPrimitive, ToPrimitive}; diff --git a/system/gd/rust/linux/utils/src/uinput.rs b/system/gd/rust/linux/utils/src/uinput.rs new file mode 100644 index 0000000000..f274aadf73 --- /dev/null +++ b/system/gd/rust/linux/utils/src/uinput.rs @@ -0,0 +1,199 @@ +//! This library provides access to Linux uinput. + +use libc; +use log::error; +use nix; +use std::ffi::CString; +use std::mem; +use std::slice; + +// Supported AVRCP Keys +const AVC_PLAY: u8 = 0x44; +const AVC_STOP: u8 = 0x45; +const AVC_PAUSE: u8 = 0x46; +const AVC_REWIND: u8 = 0x48; +const AVC_FAST_FORWAED: u8 = 0x49; +const AVC_FORWARD: u8 = 0x4B; +const AVC_BACKWARD: u8 = 0x4C; + +// Supported uinput keys +const KEY_PLAYPAUSE: libc::c_uint = 164; +const KEY_STOPCD: libc::c_uint = 166; +const KEY_REWIND: libc::c_uint = 168; +const KEY_FASTFORWAED: libc::c_uint = 208; +const KEY_NEXTSONG: libc::c_uint = 163; +const KEY_PREVIOUSSONG: libc::c_uint = 165; + +// uinput setup constants +const UINPUT_MAX_NAME_SIZE: usize = 80; +const ABS_MAX: usize = 0x3F; +const BUS_BLUETOOTH: u16 = 0x05; +const UINPUT_IOCTL_BASE: char = 'U'; + +const EV_SYN: libc::c_int = 0x00; +const EV_KEY: libc::c_int = 0x01; +const EV_REL: libc::c_int = 0x02; +const EV_REP: libc::c_int = 0x14; + +const UI_DEV_CREATE: u64 = nix::request_code_none!(UINPUT_IOCTL_BASE, 1); +const UI_DEV_DESTROY: u64 = nix::request_code_none!(UINPUT_IOCTL_BASE, 2); +const UI_SET_EVBIT: u64 = + nix::request_code_write!(UINPUT_IOCTL_BASE, 100, mem::size_of::<libc::c_int>()); +const UI_SET_PHYS: u64 = + nix::request_code_write!(UINPUT_IOCTL_BASE, 108, mem::size_of::<libc::c_char>()); +const UI_SET_KEYBIT: u64 = + nix::request_code_write!(UINPUT_IOCTL_BASE, 101, mem::size_of::<libc::c_int>()); + +// Conversion key map from AVRCP keys to uinput keys. +#[allow(dead_code)] +struct KeyMap { + avc: u8, + uinput: libc::c_uint, +} + +const KEY_MAP: [KeyMap; 7] = [ + KeyMap { avc: AVC_PLAY, uinput: KEY_PLAYPAUSE }, + KeyMap { avc: AVC_STOP, uinput: KEY_STOPCD }, + KeyMap { avc: AVC_PAUSE, uinput: KEY_PLAYPAUSE }, + KeyMap { avc: AVC_REWIND, uinput: KEY_REWIND }, + KeyMap { avc: AVC_FAST_FORWAED, uinput: KEY_FASTFORWAED }, + KeyMap { avc: AVC_FORWARD, uinput: KEY_NEXTSONG }, + KeyMap { avc: AVC_BACKWARD, uinput: KEY_PREVIOUSSONG }, +]; + +#[repr(C, packed)] +struct UInputId { + bustype: libc::c_ushort, + vendor: libc::c_ushort, + product: libc::c_ushort, + version: libc::c_ushort, +} + +#[repr(C, packed)] +struct UInputDev { + name: [libc::c_char; UINPUT_MAX_NAME_SIZE], + id: UInputId, + ff_effects_max: libc::c_int, + absmax: [libc::c_int; ABS_MAX + 1], + absmin: [libc::c_int; ABS_MAX + 1], + absfuzz: [libc::c_int; ABS_MAX + 1], + absflat: [libc::c_int; ABS_MAX + 1], +} + +#[allow(dead_code)] +impl UInputDev { + pub fn serialize(&mut self) -> &[u8] { + unsafe { + slice::from_raw_parts( + (self as *const UInputDev) as *const u8, + mem::size_of::<UInputDev>(), + ) + } + } +} + +/// A struct that holds the uinput object. It consists of a file descriptor fetched from the kernel +/// and a device struct which contains the information required to construct an uinput device. +pub struct UInput { + fd: i32, + device: UInputDev, +} + +impl Drop for UInput { + fn drop(&mut self) { + self.close(); + } +} + +impl UInput { + /// Create a new UInput object. + pub fn new() -> Self { + UInput { + fd: -1, + device: UInputDev { + name: [0; UINPUT_MAX_NAME_SIZE], + id: UInputId { bustype: BUS_BLUETOOTH, vendor: 0, product: 0, version: 0 }, + ff_effects_max: 0, + absmax: [0; ABS_MAX + 1], + absmin: [0; ABS_MAX + 1], + absfuzz: [0; ABS_MAX + 1], + absflat: [0; ABS_MAX + 1], + }, + } + } + + /// Return true if uinput is open and a valid fd is retrieved. + pub fn is_initialized(&self) -> bool { + self.fd >= 0 + } + + /// Initialize a uinput device with kernel. + #[allow(temporary_cstring_as_ptr)] + pub fn init(&mut self, mut name: String, addr: String) { + if self.is_initialized() { + return; + } + + // Truncate the device name if over the max size allowed. + name.truncate(UINPUT_MAX_NAME_SIZE); + for (i, ch) in name.chars().enumerate() { + self.device.name[i] = ch as libc::c_char; + } + + let mut fd = -1; + + unsafe { + for path in ["/dev/uinput", "/dev/input/uinput", "/dev/misc/uinput"] { + fd = libc::open(CString::new(path).unwrap().as_ptr().cast(), libc::O_RDWR); + if fd >= 0 { + break; + } + } + + if fd < -1 { + error!("Failed to open uinput: {}", std::io::Error::last_os_error()); + return; + } + + if libc::write( + fd, + self.device.serialize().as_ptr() as *const libc::c_void, + mem::size_of::<UInputDev>(), + ) < 0 + { + error!("Can't write device information: {}", std::io::Error::last_os_error()); + libc::close(fd); + return; + } + + libc::ioctl(fd, UI_SET_EVBIT, EV_KEY); + libc::ioctl(fd, UI_SET_EVBIT, EV_REL); + libc::ioctl(fd, UI_SET_EVBIT, EV_REP); + libc::ioctl(fd, UI_SET_EVBIT, EV_SYN); + libc::ioctl(fd, UI_SET_PHYS, addr); + + for key_map in KEY_MAP { + libc::ioctl(fd, UI_SET_KEYBIT, key_map.uinput); + } + + if libc::ioctl(fd, UI_DEV_CREATE, 0) < 0 { + error!("Can't create uinput device: {}", std::io::Error::last_os_error()); + libc::close(fd); + return; + } + } + + self.fd = fd; + } + + /// Close the uinput device with kernel if there is one. + pub fn close(&mut self) { + if self.is_initialized() { + unsafe { + libc::ioctl(self.fd, UI_DEV_DESTROY, 0); + libc::close(self.fd); + } + self.fd = -1; + } + } +} diff --git a/system/stack/btm/btm_sco_hci.cc b/system/stack/btm/btm_sco_hci.cc index 7c0e16e7c7..8cebe18d66 100644 --- a/system/stack/btm/btm_sco_hci.cc +++ b/system/stack/btm/btm_sco_hci.cc @@ -134,7 +134,7 @@ static const uint8_t btm_msbc_zero_packet[] = { static const uint8_t btm_msbc_zero_frames[BTM_MSBC_CODE_SIZE] = {0}; /* Define the structure that contains mSBC data */ -typedef struct { +struct tBTM_MSBC_INFO { size_t packet_size; /* SCO mSBC packet size supported by lower layer */ bool check_alignment; /* True to wait for mSBC packet to align */ size_t buf_size; /* The size of the buffer, determined by the packet_size. */ @@ -286,8 +286,7 @@ typedef struct { return &msbc_encode_buf[encode_buf_ro]; } - -} tBTM_MSBC_INFO; +}; static tBTM_MSBC_INFO* msbc_info = nullptr; diff --git a/system/test/mock/mock_common_metrics.cc b/system/test/mock/mock_common_metrics.cc index 766fec2747..cf71750734 100644 --- a/system/test/mock/mock_common_metrics.cc +++ b/system/test/mock/mock_common_metrics.cc @@ -219,5 +219,9 @@ void LogLeAudioConnectionSessionReported( mock_function_count_map[__func__]++; } +void LogLeAudioBroadcastSessionReported(int64_t duration_nanos) { + mock_function_count_map[__func__]++; +} + } // namespace common } // namespace bluetooth diff --git a/tools/pdl/Android.bp b/tools/pdl/Android.bp index 4991c01946..80473ac6a2 100644 --- a/tools/pdl/Android.bp +++ b/tools/pdl/Android.bp @@ -82,18 +82,35 @@ rust_test_host { ], } +// Python generator. +python_binary_host { + name: "pdl_python_generator", + main: "scripts/generate_python_backend.py", + srcs: [ + "scripts/generate_python_backend.py", + "scripts/pdl/ast.py", + "scripts/pdl/core.py", + ] +} + +// Defaults for PDL python backend generation. +genrule_defaults { + name: "pdl_python_generator_defaults", + tools: [ + ":pdl", + ":pdl_python_generator", + ], +} + // Generate the python parser+serializer backend for the // little endian test file located at tests/canonical/le_test_file.pdl. genrule { name: "pdl_python_generator_le_test_gen", + defaults: [ "pdl_python_generator_defaults" ], cmd: "$(location :pdl) $(in) |" + - " $(location scripts/generate_python_backend.py)" + + " $(location :pdl_python_generator)" + " --output $(out) --custom-type-location tests.custom_types", - tools: [ ":pdl" ], tool_files: [ - "scripts/generate_python_backend.py", - "scripts/pdl/core.py", - "scripts/pdl/ast.py", "tests/custom_types.py", ], srcs: [ @@ -108,14 +125,11 @@ genrule { // big endian test file located at tests/canonical/be_test_file.pdl. genrule { name: "pdl_python_generator_be_test_gen", + defaults: [ "pdl_python_generator_defaults" ], cmd: "$(location :pdl) $(in) |" + - " $(location scripts/generate_python_backend.py)" + + " $(location :pdl_python_generator)" + " --output $(out) --custom-type-location tests.custom_types", - tools: [ ":pdl" ], tool_files: [ - "scripts/generate_python_backend.py", - "scripts/pdl/core.py", - "scripts/pdl/ast.py", "tests/custom_types.py", ], srcs: [ @@ -141,6 +155,9 @@ python_test_host { "tests/canonical/le_test_vectors.json", "tests/canonical/be_test_vectors.json", ], + libs: [ + "typing_extensions", + ], test_options: { unit_test: true, }, diff --git a/tools/pdl/scripts/generate_python_backend.py b/tools/pdl/scripts/generate_python_backend.py index a9c6935f9e..34497a844a 100755 --- a/tools/pdl/scripts/generate_python_backend.py +++ b/tools/pdl/scripts/generate_python_backend.py @@ -39,7 +39,7 @@ def generate_prelude() -> str: @dataclass class Packet: - payload: Optional[bytes] = field(repr=False) + payload: Optional[bytes] = field(repr=False, default_factory=bytes) @classmethod def parse_all(cls, span: bytes) -> 'Packet': @@ -48,6 +48,10 @@ def generate_prelude() -> str: raise Exception('Unexpected parsing remainder') return packet + @property + def size(self) -> int: + pass + def show(self, prefix: str = ''): print(f'{self.__class__.__name__}') @@ -141,11 +145,11 @@ class FieldParser: self.check_size_(str(self.offset)) self.code.extend(unchecked_code) - def consume_span_(self) -> str: + def consume_span_(self, keep: int = 0) -> str: """Skip consumed span bytes.""" if self.offset > 0: self.check_code_() - self.append_(f'span = span[{self.offset}:]') + self.append_(f'span = span[{self.offset - keep}:]') self.offset = 0 def parse_array_element_dynamic_(self, field: ast.ArrayField, span: str): @@ -196,9 +200,7 @@ class FieldParser: # Apply the size modifier. if field.size_modifier and size: - self.append_(f"{size} = {size} {field.size_modifier}") - if field.size_modifier and count: - self.append_(f"{count} = {count} {field.size_modifier}") + self.append_(f"{size} = {size} - {field.size_modifier}") # The element width is not known, but the array full octet size # is known by size field. Parse elements item by item as a vector. @@ -288,7 +290,7 @@ class FieldParser: value = "value_" for shift, width, field in self.chunk: - v = (value if len(self.chunk) == 1 else f"({value} >> {shift}) & {mask(width)}") + v = (value if len(self.chunk) == 1 and shift == 0 else f"({value} >> {shift}) & {mask(width)}") if isinstance(field, ast.ScalarField): self.unchecked_append_(f"fields['{field.id}'] = {v}") @@ -349,14 +351,29 @@ class FieldParser: def parse_payload_field_(self, field: Union[ast.BodyField, ast.PayloadField]): """Parse body and payload fields.""" - size = core.get_payload_field_size(field) + payload_size = core.get_payload_field_size(field) offset_from_end = core.get_field_offset_from_end(field) - self.consume_span_() + + # If the payload is not byte aligned, do parse the bit fields + # that can be extracted, but do not consume the input bytes as + # they will also be included in the payload span. + if self.shift != 0: + if payload_size: + raise Exception("Unexpected payload size for non byte aligned payload") + + rounded_size = int((self.shift + 7) / 8) + padding_bits = 8 * rounded_size - self.shift + self.parse_bit_field_(core.make_reserved_field(padding_bits)) + self.consume_span_(rounded_size) + else: + self.consume_span_() # The payload or body has a known size. # Consume the payload and update the span in case # fields are placed after the payload. - if size: + if payload_size: + if getattr(field, 'size_modifier', None): + self.append_(f"{field.id}_size -= {field.size_modifier}") self.check_size_(f'{field.id}_size') self.append_(f"payload = span[:{field.id}_size]") self.append_(f"span = span[{field.id}_size:]") @@ -429,7 +446,7 @@ class FieldParser: checksum_span = f'span[:-{offset_from_end}]' if value_size > 1: start = offset_from_end - end = offset_from_start - value_size + end = offset_from_end - value_size value = f"int.from_bytes(span[-{start}:-{end}], byteorder='{self.byteorder}')" else: value = f'span[-{offset_from_end}]' @@ -479,23 +496,262 @@ class FieldParser: self.consume_span_() +@dataclass +class FieldSerializer: + byteorder: str + shift: int = 0 + value: List[str] = field(default_factory=lambda: []) + code: List[str] = field(default_factory=lambda: []) + indent: int = 0 + + def indent_(self): + self.indent += 1 + + def unindent_(self): + self.indent -= 1 + + def append_(self, line: str): + """Append field serializing code.""" + lines = line.split('\n') + self.code.extend([' ' * self.indent + line for line in lines]) + + def extend_(self, value: str, length: int): + """Append data to the span being constructed.""" + if length == 1: + self.append_(f"_span.append({value})") + else: + self.append_(f"_span.extend(int.to_bytes({value}, length={length}, byteorder='{self.byteorder}'))") + + def serialize_array_element_(self, field: ast.ArrayField): + """Serialize a single array field element.""" + if field.width is not None: + length = int(field.width / 8) + self.extend_('_elt', length) + elif isinstance(field.type, ast.EnumDeclaration): + length = int(field.type.width / 8) + self.extend_('_elt', length) + else: + self.append_("_span.extend(_elt.serialize())") + + def serialize_array_field_(self, field: ast.ArrayField): + """Serialize the selected array field.""" + if field.width == 8: + self.append_(f"_span.extend(self.{field.id})") + else: + self.append_(f"for _elt in self.{field.id}:") + self.indent_() + self.serialize_array_element_(field) + self.unindent_() + + def serialize_bit_field_(self, field: ast.Field): + """Serialize the selected field as a bit field. + The field is added to the current chunk. When a byte boundary + is reached all saved fields are serialized together.""" + + # Add to current chunk. + width = core.get_field_size(field) + shift = self.shift + + if isinstance(field, str): + self.value.append(f"({field} << {shift})") + elif isinstance(field, ast.ScalarField): + max_value = (1 << field.width) - 1 + self.append_(f"if self.{field.id} > {max_value}:") + self.append_(f" print(f\"Invalid value for field {field.parent.id}::{field.id}:" + + f" {{self.{field.id}}} > {max_value}; the value will be truncated\")") + self.append_(f" self.{field.id} &= {max_value}") + self.value.append(f"(self.{field.id} << {shift})") + elif isinstance(field, ast.FixedField) and field.enum_id: + self.value.append(f"({field.enum_id}.{field.tag_id} << {shift})") + elif isinstance(field, ast.FixedField): + self.value.append(f"({field.value} << {shift})") + elif isinstance(field, ast.TypedefField): + self.value.append(f"(self.{field.id} << {shift})") + + elif isinstance(field, ast.SizeField): + max_size = (1 << field.width) - 1 + value_field = core.get_packet_field(field.parent, field.field_id) + size_modifier = '' + + if getattr(value_field, 'size_modifier', None): + size_modifier = f' + {value_field.size_modifier}' + + if isinstance(value_field, (ast.PayloadField, ast.BodyField)): + self.append_(f"_size = len(payload or self.payload or []){size_modifier}") + self.append_(f"if _size > {max_size}:") + self.append_(f" print(f\"Invalid length for payload field:" + + f" {{_size}} > {max_size}; the packet cannot be generated\")") + self.append_(f" raise Exception(\"Invalid payload length\")") + array_size = "_size" + elif isinstance(value_field, ast.ArrayField) and value_field.width: + array_size = f"(len(self.{value_field.id}) * {int(value_field.width / 8)})" + elif isinstance(value_field, ast.ArrayField): + self.append_(f"_size = sum([elt.size for elt in self.{value_field.id}]){size_modifier}") + array_size = "_size" + else: + raise Exception("Unsupported field type") + self.value.append(f"({array_size} << {shift})") + + elif isinstance(field, ast.CountField): + max_count = (1 << field.width) - 1 + self.append_(f"if len(self.{field.field_id}) > {max_count}:") + self.append_(f" print(f\"Invalid length for field {field.parent.id}::{field.field_id}:" + + f" {{len(self.{field.field_id})}} > {max_count}; the array will be truncated\")") + self.append_(f" del self.{field.field_id}[{max_count}:]") + self.value.append(f"(len(self.{field.field_id}) << {shift})") + elif isinstance(field, ast.ReservedField): + pass + else: + raise Exception(f'Unsupported bit field type {field.kind}') + + # Check if a byte boundary is reached. + self.shift += width + if (self.shift % 8) == 0: + self.pack_bit_fields_() + + def pack_bit_fields_(self): + """Pack serialized bit fields.""" + + # Should have an integral number of bytes now. + assert (self.shift % 8) == 0 + + # Generate the backing integer, and serialize it + # using the configured endiannes, + size = int(self.shift / 8) + + if len(self.value) == 0: + self.append_(f"_span.extend([0] * {size})") + elif len(self.value) == 1: + self.extend_(self.value[0], size) + else: + self.append_(f"_value = (") + self.append_(" " + " |\n ".join(self.value)) + self.append_(")") + self.extend_('_value', size) + + # Reset state. + self.shift = 0 + self.value = [] + + def serialize_typedef_field_(self, field: ast.TypedefField): + """Serialize a typedef field, to the exclusion of Enum fields.""" + + if self.shift != 0: + raise Exception('Typedef field does not start on an octet boundary') + if (isinstance(field.type, ast.StructDeclaration) and field.type.parent_id is not None): + raise Exception('Derived struct used in typedef field') + + if isinstance(field.type, ast.ChecksumDeclaration): + size = int(field.type.width / 8) + self.append_(f"_checksum = {field.type.function}(_span[_checksum_start:])") + self.extend_('_checksum', size) + else: + self.append_(f"_span.extend(self.{field.id}.serialize())") + + def serialize_padding_field_(self, field: ast.PaddingField): + """Serialize a padding field. The value is zero.""" + + if self.shift != 0: + raise Exception('Padding field does not start on an octet boundary') + self.append_(f"_span.extend([0] * {field.width})") + + def serialize_payload_field_(self, field: Union[ast.BodyField, ast.PayloadField]): + """Serialize body and payload fields.""" + + if self.shift != 0 and self.byteorder == 'big': + raise Exception('Payload field does not start on an octet boundary') + + if self.shift == 0: + self.append_(f"_span.extend(payload or self.payload or [])") + else: + # Supported case of packet inheritance; + # the incomplete fields are serialized into + # the payload, rather than separately. + # First extract the padding bits from the payload, + # then recombine them with the bit fields to be serialized. + rounded_size = int((self.shift + 7) / 8) + padding_bits = 8 * rounded_size - self.shift + self.append_(f"_payload = payload or self.payload or bytes()") + self.append_(f"if len(_payload) < {rounded_size}:") + self.append_(f" raise Exception(f\"Invalid length for payload field:" + + f" {{len(_payload)}} < {rounded_size}\")") + self.append_( + f"_padding = int.from_bytes(_payload[:{rounded_size}], byteorder='{self.byteorder}') >> {self.shift}") + self.value.append(f"(_padding << {self.shift})") + self.shift += padding_bits + self.pack_bit_fields_() + self.append_(f"_span.extend(_payload[{rounded_size}:])") + + def serialize_checksum_field_(self, field: ast.ChecksumField): + """Generate a checksum check.""" + + self.append_("_checksum_start = len(_span)") + + def serialize(self, field: ast.Field): + # Field has bit granularity. + # Append the field to the current chunk, + # check if a byte boundary was reached. + if core.is_bit_field(field): + self.serialize_bit_field_(field) + + # Padding fields. + elif isinstance(field, ast.PaddingField): + self.serialize_padding_field_(field) + + # Array fields. + elif isinstance(field, ast.ArrayField): + self.serialize_array_field_(field) + + # Other typedef fields. + elif isinstance(field, ast.TypedefField): + self.serialize_typedef_field_(field) + + # Payload and body fields. + elif isinstance(field, (ast.PayloadField, ast.BodyField)): + self.serialize_payload_field_(field) + + # Checksum fields. + elif isinstance(field, ast.ChecksumField): + self.serialize_checksum_field_(field) + + else: + raise Exception(f'Unimplemented field type {field.kind}') + + def generate_toplevel_packet_serializer(packet: ast.Declaration) -> List[str]: """Generate the serialize() function for a toplevel Packet or Struct declaration.""" - return ["pass"] + + serializer = FieldSerializer(byteorder=packet.file.byteorder) + for f in packet.fields: + serializer.serialize(f) + return ['_span = bytearray()'] + serializer.code + ['return bytes(_span)'] def generate_derived_packet_serializer(packet: ast.Declaration) -> List[str]: """Generate the serialize() function for a derived Packet or Struct declaration.""" - return ["pass"] + + packet_shift = core.get_packet_shift(packet) + if packet_shift and packet.file.byteorder == 'big': + raise Exception(f"Big-endian packet {packet.id} has an unsupported body shift") + + serializer = FieldSerializer(byteorder=packet.file.byteorder, shift=packet_shift) + for f in packet.fields: + serializer.serialize(f) + return ['_span = bytearray()' + ] + serializer.code + [f'return {packet.parent.id}.serialize(self, payload = bytes(_span))'] def generate_packet_parser(packet: ast.Declaration) -> List[str]: """Generate the parse() function for a toplevel Packet or Struct declaration.""" - parser = FieldParser(byteorder=packet.file.byteorder) + packet_shift = core.get_packet_shift(packet) + if packet_shift and packet.file.byteorder == 'big': + raise Exception(f"Big-endian packet {packet.id} has an unsupported body shift") + + parser = FieldParser(byteorder=packet.file.byteorder, shift=packet_shift) for f in packet.fields: parser.parse(f) parser.done() @@ -527,15 +783,39 @@ def generate_packet_parser(packet: ast.Declaration) -> List[str]: return decl + parser.code + [f"return {packet.id}(**fields), span"] -def generate_derived_packet_parser(packet: ast.Declaration) -> List[str]: - """Generate the parse() function for a derived Packet or Struct - declaration.""" - print(f"Parsing packet {packet.id}", file=sys.stderr) - parser = FieldParser(byteorder=packet.file.byteorder) +def generate_packet_size_getter(packet: ast.Declaration) -> List[str]: + constant_width = 0 + variable_width = [] for f in packet.fields: - parser.parse(f) - parser.done() - return parser.code + [f"return {packet.id}(**fields)"] + field_size = core.get_field_size(f) + if field_size is not None: + constant_width += field_size + elif isinstance(f, (ast.PayloadField, ast.BodyField)): + variable_width.append("len(self.payload)") + elif isinstance(f, ast.TypedefField): + variable_width.append(f"self.{f.id}.size") + elif isinstance(f, ast.ArrayField) and isinstance(f.type, (ast.StructDeclaration, ast.CustomFieldDeclaration)): + variable_width.append(f"sum([elt.size for elt in self.{f.id}])") + elif isinstance(f, ast.ArrayField) and isinstance(f.type, ast.EnumDeclaration): + variable_width.append(f"len(self.{f.id}) * {f.type.width}") + elif isinstance(f, ast.ArrayField): + variable_width.append(f"len(self.{f.id}) * {int(f.width / 8)}") + else: + raise Exception("Unsupported field type") + + constant_width = int(constant_width / 8) + if len(variable_width) == 0: + return [f"return {constant_width}"] + elif len(variable_width) == 1 and constant_width: + return [f"return {variable_width[0]} + {constant_width}"] + elif len(variable_width) == 1: + return [f"return {variable_width[0]}"] + elif len(variable_width) > 1 and constant_width: + return ([f"return {constant_width} + ("] + " +\n ".join(variable_width).split("\n") + [")"]) + elif len(variable_width) > 1: + return (["return ("] + " +\n ".join(variable_width).split("\n") + [")"]) + else: + assert False def generate_enum_declaration(decl: ast.EnumDeclaration) -> str: @@ -562,15 +842,22 @@ def generate_packet_declaration(packet: ast.Declaration) -> str: field_decls = [] for f in packet.fields: if isinstance(f, ast.ScalarField): - field_decls.append(f"{f.id}: int") + field_decls.append(f"{f.id}: int = 0") elif isinstance(f, ast.TypedefField): - field_decls.append(f"{f.id}: {f.type_id}") + if isinstance(f.type, ast.EnumDeclaration): + field_decls.append(f"{f.id}: {f.type_id} = {f.type_id}.{f.type.tags[0].id}") + elif isinstance(f.type, ast.ChecksumDeclaration): + field_decls.append(f"{f.id}: int = 0") + elif isinstance(f.type, (ast.StructDeclaration, ast.CustomFieldDeclaration)): + field_decls.append(f"{f.id}: {f.type_id} = field(default_factory={f.type_id})") + else: + raise Exception("Unsupported typedef field type") elif isinstance(f, ast.ArrayField) and f.width == 8: - field_decls.append(f"{f.id}: bytes") + field_decls.append(f"{f.id}: bytearray = field(default_factory=bytearray)") elif isinstance(f, ast.ArrayField) and f.width: - field_decls.append(f"{f.id}: List[int]") + field_decls.append(f"{f.id}: List[int] = field(default_factory=list)") elif isinstance(f, ast.ArrayField) and f.type_id: - field_decls.append(f"{f.id}: List[{f.type_id}]") + field_decls.append(f"{f.id}: List[{f.type_id}] = field(default_factory=list)") if packet.parent_id: parent_name = packet.parent_id @@ -582,6 +869,7 @@ def generate_packet_declaration(packet: ast.Declaration) -> str: serializer = generate_toplevel_packet_serializer(packet) parser = generate_packet_parser(packet) + size = generate_packet_size_getter(packet) return dedent("""\ @@ -593,15 +881,20 @@ def generate_packet_declaration(packet: ast.Declaration) -> str: def parse({parent_fields}span: bytes) -> Tuple['{packet_name}', bytes]: {parser} - def serialize(self) -> bytes: + def serialize(self, payload: bytes = None) -> bytes: {serializer} + + @property + def size(self) -> int: + {size} """).format( packet_name=packet_name, parent_name=parent_name, parent_fields=parent_fields, field_decls=indent(field_decls, 1), parser=indent(parser, 2), - serializer=indent(serializer, 2)) + serializer=indent(serializer, 2), + size=indent(size, 2)) def generate_custom_field_declaration_check(decl: ast.CustomFieldDeclaration) -> str: diff --git a/tools/pdl/scripts/pdl/core.py b/tools/pdl/scripts/pdl/core.py index f7db265e8f..f6ad1f730c 100644 --- a/tools/pdl/scripts/pdl/core.py +++ b/tools/pdl/scripts/pdl/core.py @@ -54,6 +54,11 @@ def desugar(file: File): file.group_scope = {} +def make_reserved_field(width: int) -> ReservedField: + """Create a reserved field of specified width.""" + return ReservedField(kind='reserved_field', loc=None, width=width) + + def get_packet_field(packet: Union[PacketDeclaration, StructDeclaration], id: str) -> Optional[Field]: """Return the field with selected identifier declared in the provided packet or its ancestors.""" @@ -70,6 +75,53 @@ def get_packet_field(packet: Union[PacketDeclaration, StructDeclaration], id: st return None +def get_packet_shift(packet: Union[PacketDeclaration, StructDeclaration]) -> int: + """Return the bit shift of the payload or body field in the parent packet. + + When using packet derivation on bit fields, the body may be shifted. + The shift is handled statically in the implementation of child packets, + and the incomplete field is included in the body. + ``` + packet Basic { + type: 1, + _body_ + } + ``` + """ + + # Traverse empty parents. + parent = packet.parent + while parent and len(parent.fields) == 1: + parent = parent.parent + + if not parent: + return 0 + + shift = 0 + for f in packet.parent.fields: + if isinstance(f, (BodyField, PayloadField)): + return 0 if (shift % 8) == 0 else shift + else: + # Fields that do not have a constant size are assumed to start + # on a byte boundary, and measure an integral number of bytes. + # Start the count over. + size = get_field_size(f) + shift = 0 if size is None else shift + size + + # No payload or body in parent packet. + # Not raising an error, the generation will fail somewhere else. + return 0 + + +def get_packet_ancestor( + decl: Union[PacketDeclaration, StructDeclaration]) -> Union[PacketDeclaration, StructDeclaration]: + """Return the root ancestor of the selected packet or struct.""" + if decl.parent_id is None: + return decl + else: + return get_packet_ancestor(decl.grammar.packet_scope[decl.parent_id]) + + def get_derived_packets(decl: Union[PacketDeclaration, StructDeclaration] ) -> List[Tuple[List[Constraint], Union[PacketDeclaration, StructDeclaration]]]: """Return the list of packets or structs that immediately derive from the diff --git a/tools/pdl/tests/canonical/be_test_vectors.json b/tools/pdl/tests/canonical/be_test_vectors.json index 24832a9a64..de0b2b6782 100644 --- a/tools/pdl/tests/canonical/be_test_vectors.json +++ b/tools/pdl/tests/canonical/be_test_vectors.json @@ -144,6 +144,29 @@ ] }, { + "packet": "Packet_Payload_Field_SizeModifier", + "tests": [ + { + "packed": "02", + "unpacked": { + "payload": [] + } + }, + { + "packed": "070001020304", + "unpacked": { + "payload": [ + 0, + 1, + 2, + 3, + 4 + ] + } + } + ] + }, + { "packet": "Packet_Payload_Field_UnknownSize", "tests": [ { @@ -593,6 +616,7 @@ "packet": "ScalarParent", "tests": [ { + "packet": "ScalarChild_A", "packed": "0001da", "unpacked": { "a": 0, @@ -600,6 +624,7 @@ } }, { + "packet": "ScalarChild_B", "packed": "0102dedc", "unpacked": { "a": 1, @@ -607,6 +632,7 @@ } }, { + "packet": "AliasedChild_A", "packed": "0201d8", "unpacked": { "a": 2, @@ -614,6 +640,7 @@ } }, { + "packet": "AliasedChild_B", "packed": "0302e70a", "unpacked": { "a": 3, @@ -626,6 +653,7 @@ "packet": "EnumParent", "tests": [ { + "packet": "EnumChild_A", "packed": "aabb01dd", "unpacked": { "a": 43707, @@ -633,6 +661,7 @@ } }, { + "packet": "EnumChild_B", "packed": "ccdd02def7", "unpacked": { "a": 52445, diff --git a/tools/pdl/tests/canonical/le_test_file.pdl b/tools/pdl/tests/canonical/le_test_file.pdl index c697b1b6dc..0c6b181c6b 100644 --- a/tools/pdl/tests/canonical/le_test_file.pdl +++ b/tools/pdl/tests/canonical/le_test_file.pdl @@ -50,6 +50,16 @@ packet EmptyParent : ScalarParent { _payload_ } +packet PartialParent5 { + a: 5, + _payload_ +} + +packet PartialParent12 { + a: 12, + _payload_ +} + // Packet bit fields // The parser must be able to handle bit fields with scalar values @@ -344,6 +354,34 @@ packet AliasedChild_B : EmptyParent (a = 3) { c: 16, } +// The parser must handle inheritance of packets with payloads starting +// on a shifted byte boundary, as long as the first fields of the child +// complete the bit fields. +packet PartialChild5_A : PartialParent5 (a = 0) { + b: 11, +} + +// The parser must handle inheritance of packets with payloads starting +// on a shifted byte boundary, as long as the first fields of the child +// complete the bit fields. +packet PartialChild5_B : PartialParent5 (a = 1) { + c: 27, +} + +// The parser must handle inheritance of packets with payloads starting +// on a shifted byte boundary, as long as the first fields of the child +// complete the bit fields. +packet PartialChild12_A : PartialParent12 (a = 2) { + d: 4, +} + +// The parser must handle inheritance of packets with payloads starting +// on a shifted byte boundary, as long as the first fields of the child +// complete the bit fields. +packet PartialChild12_B : PartialParent12 (a = 3) { + e: 20, +} + // Struct bit fields // The parser must be able to handle bit fields with scalar values diff --git a/tools/pdl/tests/canonical/le_test_vectors.json b/tools/pdl/tests/canonical/le_test_vectors.json index 83aadf85b6..8eff0837ac 100644 --- a/tools/pdl/tests/canonical/le_test_vectors.json +++ b/tools/pdl/tests/canonical/le_test_vectors.json @@ -144,6 +144,29 @@ ] }, { + "packet": "Packet_Payload_Field_SizeModifier", + "tests": [ + { + "packed": "02", + "unpacked": { + "payload": [] + } + }, + { + "packed": "070001020304", + "unpacked": { + "payload": [ + 0, + 1, + 2, + 3, + 4 + ] + } + } + ] + }, + { "packet": "Packet_Payload_Field_UnknownSize", "tests": [ { @@ -593,6 +616,7 @@ "packet": "ScalarParent", "tests": [ { + "packet": "ScalarChild_A", "packed": "0001da", "unpacked": { "a": 0, @@ -600,6 +624,7 @@ } }, { + "packet": "ScalarChild_B", "packed": "0102dcde", "unpacked": { "a": 1, @@ -607,6 +632,7 @@ } }, { + "packet": "AliasedChild_A", "packed": "0201d8", "unpacked": { "a": 2, @@ -614,6 +640,7 @@ } }, { + "packet": "AliasedChild_B", "packed": "03020ae7", "unpacked": { "a": 3, @@ -623,9 +650,52 @@ ] }, { + "packet": "PartialParent5", + "tests": [ + { + "packet": "PartialChild5_A", + "packed": "409d", + "unpacked": { + "a": 0, + "b": 1258 + } + }, + { + "packet": "PartialChild5_B", + "packed": "a1434b84", + "unpacked": { + "a": 1, + "c": 69360157 + } + } + ] + }, + { + "packet": "PartialParent12", + "tests": [ + { + "packet": "PartialChild12_A", + "packed": "02c0", + "unpacked": { + "a": 2, + "d": 12 + } + }, + { + "packet": "PartialChild12_B", + "packed": "030035e9", + "unpacked": { + "a": 3, + "e": 955216 + } + } + ] + }, + { "packet": "EnumParent", "tests": [ { + "packet": "EnumChild_A", "packed": "bbaa01dd", "unpacked": { "a": 43707, @@ -633,6 +703,7 @@ } }, { + "packet": "EnumChild_B", "packed": "ddcc02f7de", "unpacked": { "a": 52445, diff --git a/tools/pdl/tests/custom_types.py b/tools/pdl/tests/custom_types.py index 115314f757..72484c62d2 100644 --- a/tools/pdl/tests/custom_types.py +++ b/tools/pdl/tests/custom_types.py @@ -5,7 +5,7 @@ from typing import Tuple @dataclass class SizedCustomField: - def __init__(self, value: int): + def __init__(self, value: int = 0): self.value = value def parse(span: bytes) -> Tuple['SizedCustomField', bytes]: @@ -23,7 +23,7 @@ class SizedCustomField: @dataclass class UnsizedCustomField: - def __init__(self, value: int): + def __init__(self, value: int = 0): self.value = value def parse(span: bytes) -> Tuple['UnsizedCustomField', bytes]: diff --git a/tools/pdl/tests/python_generator_test.py b/tools/pdl/tests/python_generator_test.py index 6014e4baa0..720d887311 100644 --- a/tools/pdl/tests/python_generator_test.py +++ b/tools/pdl/tests/python_generator_test.py @@ -5,7 +5,11 @@ # Tests the generated python backend against standard PDL # constructs, with matching input vectors. +import dataclasses +import enum import json +import typing +import typing_extensions import unittest # (le|be)_pdl_test are the names of the modules generated from the canonical @@ -18,7 +22,7 @@ import be_pdl_test def match_object(self, left, right): """Recursively match a python class object against a reference - json object.""" + json object.""" if isinstance(right, int): self.assertEqual(left, right) elif isinstance(right, list): @@ -31,6 +35,34 @@ def match_object(self, left, right): match_object(self, getattr(left, k), v) +def create_object(typ, value): + """Build an object of the selected type using the input value.""" + if dataclasses.is_dataclass(typ): + field_types = dict([(f.name, f.type) for f in dataclasses.fields(typ)]) + values = dict() + for (f, v) in value.items(): + field_type = field_types[f] + values[f] = create_object(field_type, v) + return typ(**values) + elif typing_extensions.get_origin(typ) is list: + typ = typing_extensions.get_args(typ)[0] + return [create_object(typ, v) for v in value] + elif typing_extensions.get_origin(typ) is typing.Union: + # typing.Optional[int] expands to typing.Union[int, None] + typ = typing_extensions.get_args(typ)[0] + return create_object(typ, value) if value else None + elif typ is bytes: + return bytes(value) + elif typ is bytearray: + return bytearray(value) + elif issubclass(typ, enum.Enum): + return typ(value) + elif typ is int: + return value + else: + raise Exception(f"unsupported type annotation {typ}") + + class PacketParserTest(unittest.TestCase): """Validate the generated parser against pre-generated test vectors in canonical/(le|be)_test_vectors.json""" @@ -66,7 +98,7 @@ class PacketParserTest(unittest.TestCase): tests = item['tests'] with self.subTest(packet=packet): # Retrieve the class object from the generated - # module, in order to invoke the proper parse + # module, in order to invoke the proper constructor # method for this test. cls = getattr(be_pdl_test, packet) for test in tests: @@ -74,6 +106,51 @@ class PacketParserTest(unittest.TestCase): match_object(self, result, test['unpacked']) +class PacketSerializerTest(unittest.TestCase): + """Validate the generated serializer against pre-generated test + vectors in canonical/(le|be)_test_vectors.json""" + + def testLittleEndian(self): + with open('tests/canonical/le_test_vectors.json') as f: + reference = json.load(f) + + for item in reference: + # 'packet' is the name of the packet being tested, + # 'tests' lists input vectors that must match the + # selected packet. + packet = item['packet'] + tests = item['tests'] + with self.subTest(packet=packet): + # Retrieve the class object from the generated + # module, in order to invoke the proper constructor + # method for this test. + for test in tests: + cls = getattr(le_pdl_test, test.get('packet', packet)) + obj = create_object(cls, test['unpacked']) + result = obj.serialize() + self.assertEqual(result, bytes.fromhex(test['packed'])) + + def testBigEndian(self): + with open('tests/canonical/be_test_vectors.json') as f: + reference = json.load(f) + + for item in reference: + # 'packet' is the name of the packet being tested, + # 'tests' lists input vectors that must match the + # selected packet. + packet = item['packet'] + tests = item['tests'] + with self.subTest(packet=packet): + # Retrieve the class object from the generated + # module, in order to invoke the proper parse + # method for this test. + for test in tests: + cls = getattr(be_pdl_test, test.get('packet', packet)) + obj = create_object(cls, test['unpacked']) + result = obj.serialize() + self.assertEqual(result, bytes.fromhex(test['packed'])) + + class CustomPacketParserTest(unittest.TestCase): """Manual testing for custom fields.""" @@ -104,4 +181,4 @@ class CustomPacketParserTest(unittest.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=3) |