diff options
231 files changed, 10916 insertions, 2717 deletions
diff --git a/android/app/Android.bp b/android/app/Android.bp index 5af8cfd487..2b1d388c8c 100644 --- a/android/app/Android.bp +++ b/android/app/Android.bp @@ -69,12 +69,6 @@ cc_library_shared { "libbluetooth", "libc++fs", ], - cflags: [ - "-Wall", - "-Werror", - "-Wextra", - "-Wno-unused-parameter", - ], sanitize: { scs: true, }, diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapMethodProxy.java b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java index 8846e6020c..57104510f5 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapMethodProxy.java +++ b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.bluetooth.pbap; +package com.android.bluetooth; import android.content.ContentResolver; import android.content.Context; @@ -22,7 +22,6 @@ import android.database.Cursor; import android.net.Uri; import android.util.Log; -import com.android.bluetooth.Utils; import com.android.internal.annotations.VisibleForTesting; import com.android.obex.HeaderSet; @@ -31,22 +30,22 @@ import java.io.IOException; /** * Proxy class for method calls to help with unit testing */ -public class BluetoothPbapMethodProxy { - private static final String TAG = BluetoothPbapMethodProxy.class.getSimpleName(); - private static BluetoothPbapMethodProxy sInstance; +public class BluetoothMethodProxy { + private static final String TAG = BluetoothMethodProxy.class.getSimpleName(); + private static BluetoothMethodProxy sInstance; private static final Object INSTANCE_LOCK = new Object(); - private BluetoothPbapMethodProxy() {} + private BluetoothMethodProxy() {} /** * Get the singleton instance of proxy * * @return the singleton instance, guaranteed not null */ - public static BluetoothPbapMethodProxy getInstance() { + public static BluetoothMethodProxy getInstance() { synchronized (INSTANCE_LOCK) { if (sInstance == null) { - sInstance = new BluetoothPbapMethodProxy(); + sInstance = new BluetoothMethodProxy(); } } return sInstance; @@ -58,7 +57,7 @@ public class BluetoothPbapMethodProxy { * @param proxy a test instance of the BluetoothPbapMethodCallProxy */ @VisibleForTesting - public static void setInstanceForTesting(BluetoothPbapMethodProxy proxy) { + public static void setInstanceForTesting(BluetoothMethodProxy proxy) { Utils.enforceInstrumentationTestMode(); synchronized (INSTANCE_LOCK) { Log.d(TAG, "setInstanceForTesting(), set to " + proxy); diff --git a/android/app/src/com/android/bluetooth/bass_client/BaseData.java b/android/app/src/com/android/bluetooth/bass_client/BaseData.java index c76987ede1..c76987ede1 100755..100644 --- a/android/app/src/com/android/bluetooth/bass_client/BaseData.java +++ b/android/app/src/com/android/bluetooth/bass_client/BaseData.java diff --git a/android/app/src/com/android/bluetooth/bass_client/BassClientService.java b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java index 9ba993aff5..c38ef94d8e 100755..100644 --- a/android/app/src/com/android/bluetooth/bass_client/BassClientService.java +++ b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java @@ -92,7 +92,6 @@ public class BassClientService extends ProfileService { private AdapterService mAdapterService; private DatabaseManager mDatabaseManager; private BluetoothAdapter mBluetoothAdapter = null; - private BassUtils mBassUtils = null; private Map<BluetoothDevice, BluetoothDevice> mActiveSourceMap; /* Caching the PeriodicAdvertisementResult from Broadcast source */ /* This is stored at service so that each device state machine can access @@ -279,7 +278,6 @@ public class BassClientService extends ProfileService { registerReceiver(mIntentReceiver, filter, Context.RECEIVER_NOT_EXPORTED); setBassClientService(this); - mBassUtils = new BassUtils(this); // Saving PSync stuff for future addition mDeviceToSyncHandleMap = new HashMap<BluetoothDevice, Integer>(); mPeriodicAdvertisementResultMap = new HashMap<BluetoothDevice, @@ -324,10 +322,6 @@ public class BassClientService extends ProfileService { mActiveSourceMap.clear(); mActiveSourceMap = null; } - if (mBassUtils != null) { - mBassUtils.cleanUp(); - mBassUtils = null; - } if (mPendingGroupOp != null) { mPendingGroupOp.clear(); } @@ -340,13 +334,6 @@ public class BassClientService extends ProfileService { return super.onUnbind(intent); } - /** - * getBassUtils - */ - public BassUtils getBassUtils() { - return mBassUtils; - } - BluetoothDevice getDeviceForSyncHandle(int syncHandle) { if (mDeviceToSyncHandleMap == null) { return null; 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 bc14f6d3c7..cadf0e5a78 100755..100644 --- a/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java +++ b/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java @@ -178,7 +178,6 @@ public class BassClientStateMachine extends StateMachine { private final Map<Integer, Boolean> mPendingRemove = new HashMap(); // Psync and PAST interfaces private PeriodicAdvertisingManager mPeriodicAdvManager; - private boolean mAutoAssist = false; private boolean mAutoTriggered = false; private boolean mNoStopScanOffload = false; private boolean mDefNoPAS = false; @@ -606,10 +605,6 @@ public class BassClientStateMachine extends StateMachine { advHandle, mPeriodicAdvCallback); } else { Log.e(TAG, "There is no valid sync handle for this Source"); - if (mAutoAssist) { - // Initiate Auto Assist procedure for this device - mService.getBassUtils().triggerAutoAssist(recvState); - } } } } else if (state == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED diff --git a/android/app/src/com/android/bluetooth/bass_client/BassConstants.java b/android/app/src/com/android/bluetooth/bass_client/BassConstants.java index dcd6b5b5a4..6e0dd184b7 100755..100644 --- a/android/app/src/com/android/bluetooth/bass_client/BassConstants.java +++ b/android/app/src/com/android/bluetooth/bass_client/BassConstants.java @@ -37,12 +37,7 @@ public class BassConstants { UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); public static final ParcelUuid BASIC_AUDIO_UUID = ParcelUuid.fromString("00001851-0000-1000-8000-00805F9B34FB"); - public static final int AA_START_SCAN = 1; - public static final int AA_SCAN_SUCCESS = 2; - public static final int AA_SCAN_FAILURE = 3; - public static final int AA_SCAN_TIMEOUT = 4; - // timeout for internal scan - public static final int AA_SCAN_TIMEOUT_MS = 1000; + public static final int INVALID_SYNC_HANDLE = -1; public static final int INVALID_ADV_SID = -1; public static final int INVALID_ADV_ADDRESS_TYPE = -1; diff --git a/android/app/src/com/android/bluetooth/bass_client/BassUtils.java b/android/app/src/com/android/bluetooth/bass_client/BassUtils.java index 42f88cad0c..6a31485cc5 100755..100644 --- a/android/app/src/com/android/bluetooth/bass_client/BassUtils.java +++ b/android/app/src/com/android/bluetooth/bass_client/BassUtils.java @@ -16,27 +16,12 @@ package com.android.bluetooth.bass_client; -import android.annotation.NonNull; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothLeBroadcastReceiveState; -import android.bluetooth.IBluetoothLeBroadcastAssistantCallback; -import android.bluetooth.le.BluetoothLeScanner; -import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanResult; -import android.os.Handler; -import android.os.Message; import android.os.ParcelUuid; import android.util.Log; -import com.android.bluetooth.btservice.ServiceFactory; - -import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** * Bass Utility functions @@ -44,73 +29,6 @@ import java.util.Map; class BassUtils { private static final String TAG = "BassUtils"; - // Using ArrayList as KEY to hashmap. May be not risk - // in this case as It is used to track the callback to cancel Scanning later - private final Map<ArrayList<IBluetoothLeBroadcastAssistantCallback>, ScanCallback> - mLeAudioSourceScanCallbacks = - new HashMap<ArrayList<IBluetoothLeBroadcastAssistantCallback>, ScanCallback>(); - private final Map<BluetoothDevice, ScanCallback> mBassAutoAssist = - new HashMap<BluetoothDevice, ScanCallback>(); - - /*LE Scan related members*/ - private boolean mBroadcastersAround = false; - private BluetoothAdapter mBluetoothAdapter = null; - private BluetoothLeScanner mLeScanner = null; - private BassClientService mService = null; - private ServiceFactory mFactory = new ServiceFactory(); - - BassUtils(BassClientService service) { - mService = service; - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - mLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); - } - - void cleanUp() { - if (mLeAudioSourceScanCallbacks != null) { - mLeAudioSourceScanCallbacks.clear(); - } - if (mBassAutoAssist != null) { - mBassAutoAssist.clear(); - } - } - - private final Handler mAutoAssistScanHandler = - new Handler() { - public void handleMessage(Message msg) { - super.handleMessage(msg); - switch (msg.what) { - case BassConstants.AA_START_SCAN: - Message m = obtainMessage(BassConstants.AA_SCAN_TIMEOUT); - sendMessageDelayed(m, BassConstants.AA_SCAN_TIMEOUT_MS); - mService.startSearchingForSources(null); - break; - case BassConstants.AA_SCAN_SUCCESS: - // Able to find to desired desired Source Device - ScanResult scanRes = (ScanResult) msg.obj; - BluetoothDevice dev = scanRes.getDevice(); - mService.stopSearchingForSources(); - mService.selectSource(dev, scanRes, true); - break; - case BassConstants.AA_SCAN_FAILURE: - // Not able to find the given source - break; - case BassConstants.AA_SCAN_TIMEOUT: - mService.stopSearchingForSources(); - break; - } - } - }; - - @NonNull Handler getAutoAssistScanHandler() { - return mAutoAssistScanHandler; - } - - void triggerAutoAssist(BluetoothLeBroadcastReceiveState recvState) { - Message msg = mAutoAssistScanHandler.obtainMessage(BassConstants.AA_START_SCAN); - msg.obj = recvState.getSourceDevice(); - mAutoAssistScanHandler.sendMessage(msg); - } - static boolean containUuid(List<ScanFilter> filters, ParcelUuid uuid) { for (ScanFilter filter: filters) { if (filter.getServiceUuid().equals(uuid)) { diff --git a/android/app/src/com/android/bluetooth/bass_client/PeriodicAdvertisementResult.java b/android/app/src/com/android/bluetooth/bass_client/PeriodicAdvertisementResult.java index 9d1aab0e5a..9d1aab0e5a 100755..100644 --- a/android/app/src/com/android/bluetooth/bass_client/PeriodicAdvertisementResult.java +++ b/android/app/src/com/android/bluetooth/bass_client/PeriodicAdvertisementResult.java diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java index c3f04a1106..f3ad2ea140 100644 --- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java +++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java @@ -5247,6 +5247,7 @@ public class AdapterService extends Service { } // Boolean flags + private static final String SDP_SERIALIZATION_FLAG = "INIT_sdp_serialization"; private static final String GD_CORE_FLAG = "INIT_gd_core"; private static final String GD_ADVERTISING_FLAG = "INIT_gd_advertising"; private static final String GD_SCANNING_FLAG = "INIT_gd_scanning"; @@ -5256,7 +5257,8 @@ public class AdapterService extends Service { private static final String GD_L2CAP_FLAG = "INIT_gd_l2cap"; private static final String GD_RUST_FLAG = "INIT_gd_rust"; private static final String GD_LINK_POLICY_FLAG = "INIT_gd_link_policy"; - private static final String GATT_ROBUST_CACHING_FLAG = "INIT_gatt_robust_caching"; + private static final String GATT_ROBUST_CACHING_CLIENT_FLAG = "INIT_gatt_robust_caching_client"; + private static final String GATT_ROBUST_CACHING_SERVER_FLAG = "INIT_gatt_robust_caching_server"; /** * Logging flags logic (only applies to DEBUG and VERBOSE levels): @@ -5285,6 +5287,10 @@ public class AdapterService extends Service { if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_CORE_FLAG, false)) { initFlags.add(String.format("%s=%s", GD_CORE_FLAG, "true")); } + if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, + SDP_SERIALIZATION_FLAG, true)) { + initFlags.add(String.format("%s=%s", SDP_SERIALIZATION_FLAG, "true")); + } if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_ADVERTISING_FLAG, false)) { initFlags.add(String.format("%s=%s", GD_ADVERTISING_FLAG, "true")); } @@ -5311,8 +5317,12 @@ public class AdapterService extends Service { initFlags.add(String.format("%s=%s", GD_LINK_POLICY_FLAG, "true")); } if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, - GATT_ROBUST_CACHING_FLAG, false)) { - initFlags.add(String.format("%s=%s", GATT_ROBUST_CACHING_FLAG, "true")); + GATT_ROBUST_CACHING_CLIENT_FLAG, true)) { + initFlags.add(String.format("%s=%s", GATT_ROBUST_CACHING_CLIENT_FLAG, "true")); + } + if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, + GATT_ROBUST_CACHING_SERVER_FLAG, false)) { + initFlags.add(String.format("%s=%s", GATT_ROBUST_CACHING_SERVER_FLAG, "true")); } if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, LOGGING_DEBUG_ENABLED_FOR_ALL_FLAG, false)) { diff --git a/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java index f85dbbc4bf..172383e2f2 100644 --- a/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java +++ b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java @@ -598,6 +598,24 @@ public class CsipSetCoordinatorService extends ProfileService { } /** + * Get group ID for a given device and UUID + * @param device potential group member + * @param uuid profile context UUID + * @return group ID + */ + public Integer getGroupId(BluetoothDevice device, ParcelUuid uuid) { + Map<Integer, Integer> device_groups = + mDeviceGroupIdRankMap.getOrDefault(device, new HashMap<>()); + return mGroupIdToUuidMap.entrySet() + .stream() + .filter(e -> (device_groups.containsKey(e.getKey()) + && e.getValue().equals(uuid))) + .map(Map.Entry::getKey) + .findFirst() + .orElse(IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID); + } + + /** * Get device's groups/ * @param device group member device * @return map of group id and related uuids. diff --git a/android/app/src/com/android/bluetooth/gatt/GattService.java b/android/app/src/com/android/bluetooth/gatt/GattService.java index 0d4cfa95b9..b21a349867 100644 --- a/android/app/src/com/android/bluetooth/gatt/GattService.java +++ b/android/app/src/com/android/bluetooth/gatt/GattService.java @@ -258,9 +258,9 @@ public class GattService extends ProfileService { /** * HashMap used to synchronize writeCharacteristic calls mapping remote device address to - * available permit (either 1 or 0). + * available permit (connectId or -1). */ - private final HashMap<String, AtomicBoolean> mPermits = new HashMap<>(); + private final HashMap<String, Integer> mPermits = new HashMap<>(); private AdapterService mAdapterService; private BluetoothAdapterProxy mBluetoothAdapterProxy; @@ -2020,7 +2020,7 @@ public class GattService extends ProfileService { synchronized (mPermits) { Log.d(TAG, "onConnected() - adding permit for address=" + address); - mPermits.putIfAbsent(address, new AtomicBoolean(true)); + mPermits.putIfAbsent(address, -1); } connectionState = BluetoothProtoEnums.CONNECTION_STATE_CONNECTED; @@ -2052,6 +2052,13 @@ public class GattService extends ProfileService { + address); mPermits.remove(address); } + } else { + synchronized (mPermits) { + if (mPermits.get(address) == connId) { + Log.d(TAG, "onDisconnected() - set permit -1 for address=" + address); + mPermits.put(address, -1); + } + } } if (app != null) { @@ -2357,7 +2364,7 @@ public class GattService extends ProfileService { synchronized (mPermits) { Log.d(TAG, "onWriteCharacteristic() - increasing permit for address=" + address); - mPermits.get(address).set(true); + mPermits.put(address, -1); } if (VDBG) { @@ -3653,18 +3660,18 @@ public class GattService extends ProfileService { Log.d(TAG, "writeCharacteristic() - trying to acquire permit."); // Lock the thread until onCharacteristicWrite callback comes back. synchronized (mPermits) { - AtomicBoolean atomicBoolean = mPermits.get(address); - if (atomicBoolean == null) { + Integer permit = mPermits.get(address); + if (permit == null) { Log.d(TAG, "writeCharacteristic() - atomicBoolean uninitialized!"); return BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED; } - boolean success = atomicBoolean.get(); + boolean success = (permit == -1); if (!success) { Log.d(TAG, "writeCharacteristic() - no permit available."); return BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY; } - atomicBoolean.set(false); + mPermits.put(address, connId); } mNativeInterface.gattClientWriteCharacteristic(connId, handle, writeType, authReq, value); diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java index 0c40235e4b..98abf5b5a9 100644 --- a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java +++ b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java @@ -1399,8 +1399,9 @@ public class HeadsetService extends ProfileService { LeAudioService leAudioService = mFactory.getLeAudioService(); if (leAudioService != null) { Log.i(TAG, "Make sure there is no le audio device active."); - leAudioService.setActiveDevice(null); + leAudioService.setInactiveForHfpHandover(mActiveDevice); } + broadcastActiveDevice(mActiveDevice); int connectStatus = connectAudio(mActiveDevice); if (connectStatus != BluetoothStatusCodes.SUCCESS) { @@ -1432,7 +1433,7 @@ public class HeadsetService extends ProfileService { } } - int connectAudio() { + public int connectAudio() { synchronized (mStateMachines) { BluetoothDevice device = mActiveDevice; if (device == null) { 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 6889d04226..729aaf1132 100644 --- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java @@ -60,6 +60,7 @@ import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; +import com.android.bluetooth.hfp.HeadsetService; import com.android.bluetooth.mcp.McpService; import com.android.bluetooth.tbs.TbsGatt; import com.android.bluetooth.vc.VolumeControlService; @@ -92,6 +93,11 @@ public class LeAudioService extends ProfileService { private static LeAudioService sLeAudioService; /** + * Indicates group audio support for none direction + */ + private static final int AUDIO_DIRECTION_NONE = 0x00; + + /** * Indicates group audio support for input direction */ private static final int AUDIO_DIRECTION_INPUT_BIT = 0x01; @@ -101,11 +107,6 @@ public class LeAudioService extends ProfileService { */ private static final int AUDIO_DIRECTION_OUTPUT_BIT = 0x02; - /* - * Indicates no active contexts - */ - private static final int ACTIVE_CONTEXTS_NONE = 0; - private AdapterService mAdapterService; private DatabaseManager mDatabaseManager; private HandlerThread mStateMachinesThread; @@ -117,6 +118,7 @@ public class LeAudioService extends ProfileService { LeAudioNativeInterface mLeAudioNativeInterface; boolean mLeAudioNativeIsInitialized = false; + BluetoothDevice mHfpHandoverDevice = null; LeAudioBroadcasterNativeInterface mLeAudioBroadcasterNativeInterface = null; @VisibleForTesting AudioManager mAudioManager; @@ -135,14 +137,14 @@ public class LeAudioService extends ProfileService { LeAudioGroupDescriptor() { mIsConnected = false; mIsActive = false; - mActiveContexts = ACTIVE_CONTEXTS_NONE; + mDirection = AUDIO_DIRECTION_NONE; mCodecStatus = null; mLostLeadDeviceWhileStreaming = null; } public Boolean mIsConnected; public Boolean mIsActive; - public Integer mActiveContexts; + public Integer mDirection; public BluetoothLeAudioCodecStatus mCodecStatus; /* This can be non empty only for the streaming time */ BluetoothDevice mLostLeadDeviceWhileStreaming; @@ -159,21 +161,6 @@ public class LeAudioService extends ProfileService { private final Map<BluetoothDevice, Integer> mDeviceGroupIdMap = new ConcurrentHashMap<>(); private final Map<BluetoothDevice, Integer> mDeviceAudioLocationMap = new ConcurrentHashMap<>(); - private final int mContextSupportingInputAudio = BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL - | BluetoothLeAudio.CONTEXT_TYPE_VOICE_ASSISTANTS; - - private final int mContextSupportingOutputAudio = BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL - | BluetoothLeAudio.CONTEXT_TYPE_MEDIA - | BluetoothLeAudio.CONTEXT_TYPE_GAME - | BluetoothLeAudio.CONTEXT_TYPE_INSTRUCTIONAL - | BluetoothLeAudio.CONTEXT_TYPE_VOICE_ASSISTANTS - | BluetoothLeAudio.CONTEXT_TYPE_LIVE - | BluetoothLeAudio.CONTEXT_TYPE_SOUND_EFFECTS - | BluetoothLeAudio.CONTEXT_TYPE_NOTIFICATIONS - | BluetoothLeAudio.CONTEXT_TYPE_RINGTONE - | BluetoothLeAudio.CONTEXT_TYPE_ALERTS - | BluetoothLeAudio.CONTEXT_TYPE_EMERGENCY_ALARM; - private BroadcastReceiver mBondStateChangedReceiver; private BroadcastReceiver mConnectionStateChangedReceiver; private BroadcastReceiver mMuteStateChangedReceiver; @@ -318,8 +305,8 @@ public class LeAudioService extends ProfileService { Integer group_id = entry.getKey(); if (descriptor.mIsActive) { descriptor.mIsActive = false; - updateActiveDevices(group_id, descriptor.mActiveContexts, - ACTIVE_CONTEXTS_NONE, descriptor.mIsActive); + updateActiveDevices(group_id, descriptor.mDirection, AUDIO_DIRECTION_NONE, + descriptor.mIsActive); break; } } @@ -331,6 +318,7 @@ public class LeAudioService extends ProfileService { mLeAudioNativeInterface.cleanup(); mLeAudioNativeInterface = null; mLeAudioNativeIsInitialized = false; + mHfpHandoverDevice = null; // Set the service and BLE devices as inactive setLeAudioService(null); @@ -412,7 +400,7 @@ public class LeAudioService extends ProfileService { sLeAudioService = instance; } - private int getGroupVolume(int groupId) { + private int getAudioDeviceGroupVolume(int groupId) { if (mVolumeControlService == null) { mVolumeControlService = mServiceFactory.getVolumeControlService(); if (mVolumeControlService == null) { @@ -421,7 +409,7 @@ public class LeAudioService extends ProfileService { } } - return mVolumeControlService.getGroupVolume(groupId); + return mVolumeControlService.getAudioDeviceGroupVolume(groupId); } public boolean connect(BluetoothDevice device) { @@ -621,32 +609,6 @@ public class LeAudioService extends ProfileService { return result; } - /** - * Get supported group audio direction from available context. - * - * @param activeContexts bitset of active context to be matched with possible audio direction - * support. - * @return matched possible audio direction support masked bitset - * {@link #AUDIO_DIRECTION_INPUT_BIT} if input audio is supported - * {@link #AUDIO_DIRECTION_OUTPUT_BIT} if output audio is supported - */ - private Integer getAudioDirectionsFromActiveContextsMap(Integer activeContexts) { - Integer supportedAudioDirections = 0; - - if (((activeContexts & mContextSupportingInputAudio) != 0) - || (Utils.isPtsTestMode() - && (activeContexts - & (BluetoothLeAudio.CONTEXT_TYPE_RINGTONE - | BluetoothLeAudio.CONTEXT_TYPE_MEDIA)) != 0)) { - supportedAudioDirections |= AUDIO_DIRECTION_INPUT_BIT; - } - if ((activeContexts & mContextSupportingOutputAudio) != 0) { - supportedAudioDirections |= AUDIO_DIRECTION_OUTPUT_BIT; - } - - return supportedAudioDirections; - } - private Integer getActiveGroupId() { synchronized (mGroupLock) { for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) { @@ -798,12 +760,7 @@ public class LeAudioService extends ProfileService { } private boolean updateActiveInDevice(BluetoothDevice device, Integer groupId, - Integer oldActiveContexts, Integer newActiveContexts) { - Integer oldSupportedAudioDirections = - getAudioDirectionsFromActiveContextsMap(oldActiveContexts); - Integer newSupportedAudioDirections = - getAudioDirectionsFromActiveContextsMap(newActiveContexts); - + Integer oldSupportedAudioDirections, Integer newSupportedAudioDirections) { boolean oldSupportedByDeviceInput = (oldSupportedAudioDirections & AUDIO_DIRECTION_INPUT_BIT) != 0; boolean newSupportedByDeviceInput = (newSupportedAudioDirections @@ -863,12 +820,7 @@ public class LeAudioService extends ProfileService { } private boolean updateActiveOutDevice(BluetoothDevice device, Integer groupId, - Integer oldActiveContexts, Integer newActiveContexts) { - Integer oldSupportedAudioDirections = - getAudioDirectionsFromActiveContextsMap(oldActiveContexts); - Integer newSupportedAudioDirections = - getAudioDirectionsFromActiveContextsMap(newActiveContexts); - + Integer oldSupportedAudioDirections, Integer newSupportedAudioDirections) { boolean oldSupportedByDeviceOutput = (oldSupportedAudioDirections & AUDIO_DIRECTION_OUTPUT_BIT) != 0; boolean newSupportedByDeviceOutput = (newSupportedAudioDirections @@ -923,7 +875,7 @@ public class LeAudioService extends ProfileService { } int volume = IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME; if (mActiveAudioOutDevice != null) { - volume = getGroupVolume(groupId); + volume = getAudioDeviceGroupVolume(groupId); } mAudioManager.handleBluetoothActiveDeviceChanged(mActiveAudioOutDevice, @@ -938,13 +890,13 @@ public class LeAudioService extends ProfileService { /** * Report the active devices change to the active device manager and the media framework. * @param groupId id of group which devices should be updated - * @param newActiveContexts new active contexts for group of devices - * @param oldActiveContexts old active contexts for group of devices + * @param newSupportedAudioDirections new supported audio directions for group of devices + * @param oldSupportedAudioDirections old supported audio directions for group of devices * @param isActive if there is new active group * @return true if group is active after change false otherwise. */ - private boolean updateActiveDevices(Integer groupId, Integer oldActiveContexts, - Integer newActiveContexts, boolean isActive) { + private boolean updateActiveDevices(Integer groupId, Integer oldSupportedAudioDirections, + Integer newSupportedAudioDirections, boolean isActive) { BluetoothDevice device = null; if (isActive) { @@ -952,9 +904,11 @@ public class LeAudioService extends ProfileService { } boolean outReplaced = - updateActiveOutDevice(device, groupId, oldActiveContexts, newActiveContexts); + updateActiveOutDevice(device, groupId, oldSupportedAudioDirections, + newSupportedAudioDirections); boolean inReplaced = - updateActiveInDevice(device, groupId, oldActiveContexts, newActiveContexts); + updateActiveInDevice(device, groupId, oldSupportedAudioDirections, + newSupportedAudioDirections); if (outReplaced || inReplaced) { Intent intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED); @@ -1073,7 +1027,7 @@ public class LeAudioService extends ProfileService { } if (DBG) { - Log.d(TAG, "connect(): " + device); + Log.d(TAG, "connect(): " + storedDevice); } synchronized (mStateMachines) { @@ -1146,8 +1100,8 @@ public class LeAudioService extends ProfileService { return; } - descriptor.mIsActive = updateActiveDevices(groupId, - ACTIVE_CONTEXTS_NONE, descriptor.mActiveContexts, true); + descriptor.mIsActive = updateActiveDevices(groupId, AUDIO_DIRECTION_NONE, + descriptor.mDirection, true); if (descriptor.mIsActive) { notifyGroupStatusChanged(groupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE); @@ -1164,8 +1118,8 @@ public class LeAudioService extends ProfileService { } descriptor.mIsActive = false; - updateActiveDevices(groupId, descriptor.mActiveContexts, - ACTIVE_CONTEXTS_NONE, descriptor.mIsActive); + updateActiveDevices(groupId, descriptor.mDirection, AUDIO_DIRECTION_NONE, + descriptor.mIsActive); /* Clear lost devices */ if (DBG) Log.d(TAG, "Clear for group: " + groupId); clearLostDevicesWhileStreaming(descriptor); @@ -1173,6 +1127,36 @@ public class LeAudioService extends ProfileService { } } + private void handleGroupIdleDuringCall() { + if (mHfpHandoverDevice == null) { + if (DBG) { + Log.d(TAG, "There is no HFP handover"); + } + return; + } + HeadsetService headsetService = mServiceFactory.getHeadsetService(); + if (headsetService == null) { + if (DBG) { + Log.d(TAG, "There is no HFP service available"); + } + return; + } + + BluetoothDevice activeHfpDevice = headsetService.getActiveDevice(); + if (activeHfpDevice == null) { + if (DBG) { + Log.d(TAG, "Make " + mHfpHandoverDevice + " active again "); + } + headsetService.setActiveDevice(mHfpHandoverDevice); + } else { + if (DBG) { + Log.d(TAG, "Connect audio to " + activeHfpDevice); + } + headsetService.connectAudio(); + } + mHfpHandoverDevice = null; + } + // Suppressed since this is part of a local process @SuppressLint("AndroidFrameworkRequiresPermission") void messageFromNative(LeAudioStackEvent stackEvent) { @@ -1215,8 +1199,13 @@ public class LeAudioService extends ProfileService { break; case LeAudioStackEvent.CONNECTION_STATE_CONNECTED: case LeAudioStackEvent.CONNECTION_STATE_CONNECTING: - if (descriptor != null) { - if (DBG) Log.d(TAG, "Removing from lost devices : " + device); + if (descriptor != null + && Objects.equals( + descriptor.mLostLeadDeviceWhileStreaming, + device)) { + if (DBG) { + Log.d(TAG, "Removing from lost devices : " + device); + } descriptor.mLostLeadDeviceWhileStreaming = null; /* Try to connect other devices from the group */ connectSet(device); @@ -1304,13 +1293,13 @@ public class LeAudioService extends ProfileService { if (descriptor != null) { if (descriptor.mIsActive) { descriptor.mIsActive = - updateActiveDevices(groupId, descriptor.mActiveContexts, - available_contexts, descriptor.mIsActive); + updateActiveDevices(groupId, descriptor.mDirection, direction, + descriptor.mIsActive); if (!descriptor.mIsActive) { notifyGroupStatusChanged(groupId, BluetoothLeAudio.GROUP_STATUS_INACTIVE); } } - descriptor.mActiveContexts = available_contexts; + descriptor.mDirection = direction; } else { Log.e(TAG, "no descriptors for group: " + groupId); } @@ -1338,6 +1327,10 @@ public class LeAudioService extends ProfileService { handleGroupTransitToInactive(groupId); break; } + case LeAudioStackEvent.GROUP_STATUS_TURNED_IDLE_DURING_CALL: { + handleGroupIdleDuringCall(); + break; + } default: break; } @@ -1627,8 +1620,8 @@ public class LeAudioService extends ProfileService { descriptor.mIsActive = false; /* Update audio framework */ updateActiveDevices(myGroupId, - descriptor.mActiveContexts, - descriptor.mActiveContexts, + descriptor.mDirection, + descriptor.mDirection, descriptor.mIsActive); return; } @@ -1636,8 +1629,8 @@ public class LeAudioService extends ProfileService { if (descriptor.mIsActive) { updateActiveDevices(myGroupId, - descriptor.mActiveContexts, - descriptor.mActiveContexts, + descriptor.mDirection, + descriptor.mDirection, descriptor.mIsActive); } } @@ -1747,6 +1740,20 @@ public class LeAudioService extends ProfileService { } /** + * Set Inactive by HFP during handover + */ + public void setInactiveForHfpHandover(BluetoothDevice hfpHandoverDevice) { + if (!mLeAudioNativeIsInitialized) { + Log.e(TAG, "Le Audio not initialized properly."); + return; + } + if (getActiveGroupId() != LE_AUDIO_GROUP_ID_INVALID) { + mHfpHandoverDevice = hfpHandoverDevice; + setActiveDevice(null); + } + } + + /** * Set connection policy of the profile and connects it if connectionPolicy is * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} @@ -1877,6 +1884,13 @@ public class LeAudioService extends ProfileService { } private void notifyGroupNodeAdded(BluetoothDevice device, int groupId) { + if (mVolumeControlService == null) { + mVolumeControlService = mServiceFactory.getVolumeControlService(); + } + if (mVolumeControlService != null) { + mVolumeControlService.handleGroupNodeAdded(groupId, device); + } + if (mLeAudioCallbacks != null) { int n = mLeAudioCallbacks.beginBroadcast(); for (int i = 0; i < n; i++) { @@ -2468,6 +2482,26 @@ public class LeAudioService extends ProfileService { } @Override + public void setInactiveForHfpHandover(BluetoothDevice hfpHandoverDevice, + AttributionSource source, + SynchronousResultReceiver receiver) { + try { + Objects.requireNonNull(source, "source cannot be null"); + Objects.requireNonNull(receiver, "receiver cannot be null"); + + LeAudioService service = getService(source); + if (service == null) { + throw new IllegalStateException("service is null"); + } + enforceBluetoothPrivilegedPermission(service); + service.setInactiveForHfpHandover(hfpHandoverDevice); + receiver.send(null); + } catch (RuntimeException e) { + receiver.propagateException(e); + } + } + + @Override public void groupRemoveNode(int groupId, BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver) { try { @@ -2712,6 +2746,7 @@ public class LeAudioService extends ProfileService { ProfileService.println(sb, " currentlyActiveGroupId: " + getActiveGroupId()); ProfileService.println(sb, " mActiveAudioOutDevice: " + mActiveAudioOutDevice); ProfileService.println(sb, " mActiveAudioInDevice: " + mActiveAudioInDevice); + ProfileService.println(sb, " mHfpHandoverDevice:" + mHfpHandoverDevice); for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) { LeAudioGroupDescriptor descriptor = entry.getValue(); @@ -2719,7 +2754,7 @@ public class LeAudioService extends ProfileService { ProfileService.println(sb, " Group: " + groupId); ProfileService.println(sb, " isActive: " + descriptor.mIsActive); ProfileService.println(sb, " isConnected: " + descriptor.mIsConnected); - ProfileService.println(sb, " mActiveContexts: " + descriptor.mActiveContexts); + ProfileService.println(sb, " mDirection: " + descriptor.mDirection); ProfileService.println(sb, " group lead: " + getConnectedGroupLeadDevice(groupId)); ProfileService.println(sb, " first device: " + getFirstDeviceFromGroup(groupId)); ProfileService.println(sb, " lost lead device: " diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java index 6ec748396c..3824dfe66c 100644 --- a/android/app/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java +++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java @@ -55,6 +55,7 @@ public class LeAudioStackEvent { static final int GROUP_STATUS_INACTIVE = 0; static final int GROUP_STATUS_ACTIVE = 1; + static final int GROUP_STATUS_TURNED_IDLE_DURING_CALL = 2; static final int GROUP_NODE_ADDED = 1; static final int GROUP_NODE_REMOVED = 2; @@ -192,6 +193,8 @@ public class LeAudioStackEvent { return "GROUP_STATUS_ACTIVE"; case GROUP_STATUS_INACTIVE: return "GROUP_STATUS_INACTIVE"; + case GROUP_STATUS_TURNED_IDLE_DURING_CALL: + return "GROUP_STATUS_TURNED_IDLE_DURING_CALL"; default: break; } diff --git a/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java b/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java index d1ab4f7419..a3b11fc38b 100644 --- a/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java +++ b/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java @@ -454,9 +454,7 @@ public class MediaControlGattService implements MediaControlGattServiceInterface } private void onRejectedAuthorizationGattOperation(BluetoothDevice device, GattOpContext op) { - if (VDBG) { - Log.d(TAG, "onRejectedAuthorizationGattOperation device: " + device); - } + Log.w(TAG, "onRejectedAuthorizationGattOperation device: " + device); switch (op.mOperation) { case READ_CHARACTERISTIC: diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java index 3f37aac4a6..123f145f90 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java @@ -24,6 +24,7 @@ import android.provider.CallLog.Calls; import android.text.TextUtils; import android.util.Log; +import com.android.bluetooth.BluetoothMethodProxy; import com.android.bluetooth.R; import com.android.internal.annotations.VisibleForTesting; import com.android.vcard.VCardBuilder; @@ -107,7 +108,7 @@ public class BluetoothPbapCallLogComposer { return false; } - mCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery( + mCursor = BluetoothMethodProxy.getInstance().contentResolverQuery( mContext.getContentResolver(), contentUri, projection, selection, selectionArgs, sortOrder); diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java index 1353152067..e64772c759 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java @@ -43,6 +43,7 @@ import android.provider.CallLog.Calls; import android.text.TextUtils; import android.util.Log; +import com.android.bluetooth.BluetoothMethodProxy; import com.android.internal.annotations.VisibleForTesting; import com.android.obex.ApplicationParameter; import com.android.obex.HeaderSet; @@ -242,7 +243,7 @@ public class BluetoothPbapObexServer extends ServerRequestHandler { private PbapStateMachine mStateMachine; - private BluetoothPbapMethodProxy mPbapMethodProxy; + private BluetoothMethodProxy mPbapMethodProxy; private enum ContactsType { TYPE_PHONEBOOK , TYPE_SIM ; @@ -272,7 +273,7 @@ public class BluetoothPbapObexServer extends ServerRequestHandler { mVcardManager = new BluetoothPbapVcardManager(mContext); mVcardSimManager = new BluetoothPbapSimVcardManager(mContext); mStateMachine = stateMachine; - mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance(); + mPbapMethodProxy = BluetoothMethodProxy.getInstance(); } @Override diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java index 5019169656..22eac64d69 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java @@ -15,41 +15,35 @@ */ package com.android.bluetooth.pbap; -import com.android.bluetooth.R; - import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.Contacts; +import android.text.TextUtils; +import android.util.Log; +import com.android.bluetooth.BluetoothMethodProxy; +import com.android.bluetooth.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.obex.Operation; +import com.android.obex.ResponseCodes; +import com.android.obex.ServerOperation; import com.android.vcard.VCardBuilder; import com.android.vcard.VCardConfig; -import com.android.vcard.VCardConstants; import com.android.vcard.VCardUtils; -import android.content.ContentValues; -import android.provider.CallLog; -import android.provider.CallLog.Calls; -import android.text.TextUtils; -import android.util.Log; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.CommonDataKinds; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; - import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Collections; import java.util.Comparator; - -import com.android.obex.Operation; -import com.android.obex.ResponseCodes; -import com.android.obex.ServerOperation; +import java.util.List; /** * VCard composer especially for Call Log used in Bluetooth. @@ -119,7 +113,7 @@ public class BluetoothPbapSimVcardManager { } //checkpoint Figure out if we can apply selection, projection and sort order. - mCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mContentResolver, + mCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mContentResolver, contentUri, SIM_PROJECTION, null, null, sortOrder); if (mCursor == null) { @@ -273,7 +267,7 @@ public class BluetoothPbapSimVcardManager { int size = 0; Cursor contactCursor = null; try { - contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery( + contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery( mContentResolver, SIM_URI, SIM_PROJECTION, null,null, null); if (contactCursor != null) { size = contactCursor.getCount(); @@ -293,7 +287,7 @@ public class BluetoothPbapSimVcardManager { ArrayList<String> allnames = new ArrayList<String>(); Cursor contactCursor = null; try { - contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery( + contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery( mContentResolver, SIM_URI, SIM_PROJECTION, null,null,null); if (contactCursor != null) { for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor @@ -334,7 +328,7 @@ public class BluetoothPbapSimVcardManager { Cursor contactCursor = null; try { - contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery( + contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery( mContentResolver, SIM_URI, SIM_PROJECTION, null, null, null); if (contactCursor != null) { diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java index 14e26470a9..658b82a4e5 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java @@ -51,6 +51,7 @@ import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.Log; +import com.android.bluetooth.BluetoothMethodProxy; import com.android.bluetooth.R; import com.android.bluetooth.util.DevicePolicyUtils; import com.android.internal.annotations.VisibleForTesting; @@ -182,7 +183,7 @@ public class BluetoothPbapVcardManager { selectionClause = Phone.STARRED + " = 1"; } try { - contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, myUri, new String[]{Phone.CONTACT_ID}, selectionClause, null, Phone.CONTACT_ID); if (contactCursor == null) { @@ -209,7 +210,7 @@ public class BluetoothPbapVcardManager { int size = 0; Cursor callCursor = null; try { - callCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + callCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, myUri, null, selection, null, CallLog.Calls.DEFAULT_SORT_ORDER); if (callCursor != null) { size = callCursor.getCount(); @@ -243,7 +244,7 @@ public class BluetoothPbapVcardManager { Cursor callCursor = null; ArrayList<String> list = new ArrayList<String>(); try { - callCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + callCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, myUri, projection, selection, null, CALLLOG_SORT_ORDER); if (callCursor != null) { for (callCursor.moveToFirst(); !callCursor.isAfterLast(); callCursor.moveToNext()) { @@ -295,7 +296,7 @@ public class BluetoothPbapVcardManager { if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { orderBy = Phone.DISPLAY_NAME; } - contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy); if (contactCursor != null) { appendDistinctNameIdList(nameList, mContext.getString(android.R.string.unknownName), @@ -354,7 +355,7 @@ public class BluetoothPbapVcardManager { final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); Cursor contactCursor = null; try { - contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, myUri, PHONES_CONTACTS_PROJECTION, null, null, Phone.CONTACT_ID); @@ -443,7 +444,7 @@ public class BluetoothPbapVcardManager { } try { - contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, uri, projection, null, null, Phone.CONTACT_ID); if (contactCursor != null) { @@ -478,7 +479,7 @@ public class BluetoothPbapVcardManager { long primaryVcMsb = 0; ArrayList<String> list = new ArrayList<String>(); try { - callCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + callCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, myUri, null, selection, null, null); while (callCursor != null && callCursor.moveToNext()) { count = count + 1; @@ -522,7 +523,7 @@ public class BluetoothPbapVcardManager { long endPointId = 0; try { // Need test to see if order by _ID is ok here, or by date? - callsCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + callsCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, myUri, CALLLOG_PROJECTION, typeSelection, null, CALLLOG_SORT_ORDER); if (callsCursor != null) { @@ -596,7 +597,7 @@ public class BluetoothPbapVcardManager { } try { - contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, myUri, PHONES_CONTACTS_PROJECTION, selectionClause, null, Phone.CONTACT_ID); if (contactCursor != null) { @@ -640,7 +641,7 @@ public class BluetoothPbapVcardManager { if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { orderBy = Phone.DISPLAY_NAME; } - contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver, + contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver, myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy); } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while composing phonebook one vcard"); diff --git a/android/app/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java b/android/app/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java index 70621d9797..ce5ccc9ab1 100644 --- a/android/app/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java +++ b/android/app/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java @@ -17,7 +17,6 @@ package com.android.bluetooth.pbapclient; import android.accounts.Account; import android.accounts.AccountManager; -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothUuid; @@ -32,6 +31,7 @@ import android.util.Log; import com.android.bluetooth.BluetoothObexTransport; import com.android.bluetooth.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.obex.ClientSession; import com.android.obex.HeaderSet; import com.android.obex.ResponseCodes; @@ -102,7 +102,9 @@ class PbapClientConnectionHandler extends Handler { private static final long PBAP_REQUESTED_FIELDS = PBAP_FILTER_VERSION | PBAP_FILTER_FN | PBAP_FILTER_N | PBAP_FILTER_PHOTO | PBAP_FILTER_ADR | PBAP_FILTER_EMAIL | PBAP_FILTER_TEL | PBAP_FILTER_NICKNAME; - private static final int L2CAP_INVALID_PSM = -1; + + @VisibleForTesting + static final int L2CAP_INVALID_PSM = -1; public static final String PB_PATH = "telecom/pb.vcf"; public static final String FAV_PATH = "telecom/fav.vcf"; @@ -126,7 +128,6 @@ class PbapClientConnectionHandler extends Handler { private Account mAccount; private AccountManager mAccountManager; private BluetoothSocket mSocket; - private final BluetoothAdapter mAdapter; private final BluetoothDevice mDevice; // PSE SDP Record for current device. private SdpPseRecord mPseRec = null; @@ -136,19 +137,6 @@ class PbapClientConnectionHandler extends Handler { private final PbapClientStateMachine mPbapClientStateMachine; private boolean mAccountCreated; - PbapClientConnectionHandler(Looper looper, Context context, PbapClientStateMachine stateMachine, - BluetoothDevice device) { - super(looper); - mAdapter = BluetoothAdapter.getDefaultAdapter(); - mDevice = device; - mContext = context; - mPbapClientStateMachine = stateMachine; - mAuth = new BluetoothPbapObexAuthenticator(this); - mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext()); - mAccount = - new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type)); - } - /** * Constructs PCEConnectionHandler object * @@ -156,7 +144,6 @@ class PbapClientConnectionHandler extends Handler { */ PbapClientConnectionHandler(Builder pceHandlerbuild) { super(pceHandlerbuild.mLooper); - mAdapter = BluetoothAdapter.getDefaultAdapter(); mDevice = pceHandlerbuild.mDevice; mContext = pceHandlerbuild.mContext; mPbapClientStateMachine = pceHandlerbuild.mClientStateMachine; @@ -252,8 +239,8 @@ class PbapClientConnectionHandler extends Handler { if (DBG) { Log.d(TAG, "Completing Disconnect"); } - removeAccount(mAccount); - removeCallLog(mAccount); + removeAccount(); + removeCallLog(); mPbapClientStateMachine.sendMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED); break; @@ -286,9 +273,20 @@ class PbapClientConnectionHandler extends Handler { return; } + @VisibleForTesting + synchronized void setPseRecord(SdpPseRecord record) { + mPseRec = record; + } + + @VisibleForTesting + synchronized BluetoothSocket getSocket() { + return mSocket; + } + /* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified * channel, or RFCOMM default channel. */ - private synchronized boolean connectSocket() { + @VisibleForTesting + synchronized boolean connectSocket() { try { /* Use BluetoothSocket to connect */ if (mPseRec == null) { @@ -318,7 +316,8 @@ class PbapClientConnectionHandler extends Handler { /* Connect an OBEX session over the already connected socket. First establish an OBEX Transport * abstraction, then establish a Bluetooth Authenticator, and finally issue the connect call */ - private boolean connectObexSession() { + @VisibleForTesting + boolean connectObexSession() { boolean connectionSuccessful = false; try { @@ -357,13 +356,13 @@ class PbapClientConnectionHandler extends Handler { // Will get NPE if a null mSocket is passed to BluetoothObexTransport. // mSocket can be set to null if an abort() --> closeSocket() was called between // the calls to connectSocket() and connectObexSession(). - Log.w(TAG, "CONNECT Failure " + e.toString()); + Log.w(TAG, "CONNECT Failure ", e); closeSocket(); } return connectionSuccessful; } - public void abort() { + void abort() { // Perform forced cleanup, it is ok if the handler throws an exception this will free the // handler to complete what it is doing and finish with cleanup. closeSocket(); @@ -385,6 +384,7 @@ class PbapClientConnectionHandler extends Handler { } } + @VisibleForTesting void downloadContacts(String path) { try { PhonebookPullRequest processor = @@ -438,6 +438,7 @@ class PbapClientConnectionHandler extends Handler { } } + @VisibleForTesting void downloadCallLog(String path, HashMap<String, Integer> callCounter) { try { BluetoothPbapRequestPullPhoneBook request = @@ -453,7 +454,8 @@ class PbapClientConnectionHandler extends Handler { } } - private boolean addAccount(Account account) { + @VisibleForTesting + boolean addAccount(Account account) { if (mAccountManager.addAccountExplicitly(account, null, null)) { if (DBG) { Log.d(TAG, "Added account " + mAccount); @@ -463,17 +465,19 @@ class PbapClientConnectionHandler extends Handler { return false; } - private void removeAccount(Account account) { - if (mAccountManager.removeAccountExplicitly(account)) { + @VisibleForTesting + void removeAccount() { + if (mAccountManager.removeAccountExplicitly(mAccount)) { if (DBG) { - Log.d(TAG, "Removed account " + account); + Log.d(TAG, "Removed account " + mAccount); } } else { Log.e(TAG, "Failed to remove account " + mAccount); } } - private void removeCallLog(Account account) { + @VisibleForTesting + void removeCallLog() { try { // need to check call table is exist ? if (mContext.getContentResolver() == null) { @@ -489,7 +493,8 @@ class PbapClientConnectionHandler extends Handler { } } - private boolean isRepositorySupported(int mask) { + @VisibleForTesting + boolean isRepositorySupported(int mask) { if (mPseRec == null) { if (VDBG) Log.v(TAG, "No PBAP Server SDP Record"); return false; diff --git a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java index 0b0fa265e3..d1e5fd0fe6 100644 --- a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java +++ b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java @@ -26,6 +26,7 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.BluetoothVolumeControl; +import android.bluetooth.IBluetoothCsipSetCoordinator; import android.bluetooth.IBluetoothLeAudio; import android.bluetooth.IBluetoothVolumeControl; import android.bluetooth.IBluetoothVolumeControlCallback; @@ -47,6 +48,7 @@ import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; +import com.android.bluetooth.csip.CsipSetCoordinatorService; import com.android.bluetooth.le_audio.LeAudioService; import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.SynchronousResultReceiver; @@ -193,7 +195,8 @@ public class VolumeControlService extends ProfileService { private BroadcastReceiver mBondStateChangedReceiver; private BroadcastReceiver mConnectionStateChangedReceiver; - private final ServiceFactory mFactory = new ServiceFactory(); + @VisibleForTesting + ServiceFactory mFactory = new ServiceFactory(); public static boolean isEnabled() { return BluetoothProperties.isProfileVcpControllerEnabled().orElse(false); @@ -586,6 +589,11 @@ public class VolumeControlService extends ProfileService { * {@hide} */ public void setGroupVolume(int groupId, int volume) { + if (volume < 0) { + Log.w(TAG, "Tried to set invalid volume " + volume + ". Ignored."); + return; + } + mGroupVolumeCache.put(groupId, volume); mVolumeControlNativeInterface.setGroupVolume(groupId, volume); } @@ -627,6 +635,29 @@ public class VolumeControlService extends ProfileService { mVolumeControlNativeInterface.unmuteGroup(groupId); } + /** + * {@hide} + */ + public void handleGroupNodeAdded(int groupId, BluetoothDevice device) { + // Ignore disconnected device, its volume will be set once it connects + synchronized (mStateMachines) { + VolumeControlStateMachine sm = mStateMachines.get(device); + if (sm == null) { + return; + } + if (sm.getConnectionState() != BluetoothProfile.STATE_CONNECTED) { + return; + } + } + + // If group volume has already changed, the new group member should set it + Integer groupVolume = mGroupVolumeCache.getOrDefault(groupId, + IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME); + if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) { + mVolumeControlNativeInterface.setVolume(device, groupVolume); + } + } + void handleVolumeControlChanged(BluetoothDevice device, int groupId, int volume, boolean mute, boolean isAutonomous) { @@ -683,6 +714,15 @@ public class VolumeControlService extends ProfileService { } } + /** + * {@hide} + */ + public int getAudioDeviceGroupVolume(int groupId) { + int volume = getGroupVolume(groupId); + if (volume == IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) return -1; + return getDeviceVolume(getBluetoothContextualVolumeStream(), volume); + } + int getDeviceVolume(int streamType, int bleVolume) { int deviceMaxVolume = mAudioManager.getStreamMaxVolume(streamType); @@ -960,6 +1000,17 @@ public class VolumeControlService extends ProfileService { } removeStateMachine(device); } + } else if (toState == BluetoothProfile.STATE_CONNECTED) { + // Restore the group volume if it was changed while the device was not yet connected. + CsipSetCoordinatorService csipClient = mFactory.getCsipSetCoordinatorService(); + Integer groupId = csipClient.getGroupId(device, BluetoothUuid.CAP); + if (groupId != IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID) { + Integer groupVolume = mGroupVolumeCache.getOrDefault(groupId, + IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME); + if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) { + mVolumeControlNativeInterface.setVolume(device, groupVolume); + } + } } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java index b37ae31cbc..a11ee69e59 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java @@ -285,10 +285,9 @@ public class LeAudioServiceTest { assertThat(intent).isNotNull(); assertThat(intent.getAction()) .isEqualTo(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED); - assertThat(device).isEqualTo(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)); - assertThat(newState).isEqualTo(intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); - assertThat(prevState).isEqualTo(intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, - -1)); + assertThat((BluetoothDevice)intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).isEqualTo(device); + assertThat(intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1)).isEqualTo(newState); + assertThat(intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1)).isEqualTo(prevState); } /** @@ -701,31 +700,35 @@ public class LeAudioServiceTest { // LeAudio stack event: CONNECTION_STATE_CONNECTING - state machine should be created generateConnectionMessageFromNative(mLeftDevice, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); - assertThat(BluetoothProfile.STATE_CONNECTING) - .isEqualTo(mService.getConnectionState(mLeftDevice)); + assertThat(mService.getConnectionState(mLeftDevice)) + .isEqualTo(BluetoothProfile.STATE_CONNECTING); assertThat(mService.getDevices().contains(mLeftDevice)).isTrue(); // Device unbond - state machine is not removed mService.bondStateChanged(mLeftDevice, BluetoothDevice.BOND_NONE); assertThat(mService.getDevices().contains(mLeftDevice)).isTrue(); + verifyConnectionStateIntent(TIMEOUT_MS, mLeftDevice, BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.STATE_CONNECTING); // LeAudio stack event: CONNECTION_STATE_CONNECTED - state machine is not removed mService.bondStateChanged(mLeftDevice, BluetoothDevice.BOND_BONDED); generateConnectionMessageFromNative(mLeftDevice, BluetoothProfile.STATE_CONNECTED, - BluetoothProfile.STATE_CONNECTING); - assertThat(BluetoothProfile.STATE_CONNECTED) - .isEqualTo(mService.getConnectionState(mLeftDevice)); + BluetoothProfile.STATE_DISCONNECTED); + assertThat(mService.getConnectionState(mLeftDevice)) + .isEqualTo(BluetoothProfile.STATE_CONNECTED); assertThat(mService.getDevices().contains(mLeftDevice)).isTrue(); // Device unbond - state machine is not removed mService.bondStateChanged(mLeftDevice, BluetoothDevice.BOND_NONE); assertThat(mService.getDevices().contains(mLeftDevice)).isTrue(); + verifyConnectionStateIntent(TIMEOUT_MS, mLeftDevice, BluetoothProfile.STATE_DISCONNECTING, + BluetoothProfile.STATE_CONNECTED); + assertThat(mService.getConnectionState(mLeftDevice)) + .isEqualTo(BluetoothProfile.STATE_DISCONNECTING); + assertThat(mService.getDevices().contains(mLeftDevice)).isTrue(); // LeAudio stack event: CONNECTION_STATE_DISCONNECTING - state machine is not removed mService.bondStateChanged(mLeftDevice, BluetoothDevice.BOND_BONDED); - generateConnectionMessageFromNative(mLeftDevice, BluetoothProfile.STATE_DISCONNECTING, - BluetoothProfile.STATE_CONNECTED); - assertThat(BluetoothProfile.STATE_DISCONNECTING) - .isEqualTo(mService.getConnectionState(mLeftDevice)); - assertThat(mService.getDevices().contains(mLeftDevice)).isTrue(); + assertThat(mService.getConnectionState(mLeftDevice)) + .isEqualTo(BluetoothProfile.STATE_DISCONNECTING); // Device unbond - state machine is not removed mService.bondStateChanged(mLeftDevice, BluetoothDevice.BOND_NONE); assertThat(mService.getDevices().contains(mLeftDevice)).isTrue(); @@ -734,8 +737,8 @@ public class LeAudioServiceTest { mService.bondStateChanged(mLeftDevice, BluetoothDevice.BOND_BONDED); generateConnectionMessageFromNative(mLeftDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING); - assertThat(BluetoothProfile.STATE_DISCONNECTED) - .isEqualTo(mService.getConnectionState(mLeftDevice)); + assertThat(mService.getConnectionState(mLeftDevice)) + .isEqualTo(BluetoothProfile.STATE_DISCONNECTED); assertThat(mService.getDevices().contains(mLeftDevice)).isTrue(); // Device unbond - state machine is removed mService.bondStateChanged(mLeftDevice, BluetoothDevice.BOND_NONE); @@ -1428,7 +1431,7 @@ public class LeAudioServiceTest { //Add location support. injectAudioConfChanged(groupId, availableContexts); - doReturn(-1).when(mVolumeControlService).getGroupVolume(groupId); + doReturn(-1).when(mVolumeControlService).getAudioDeviceGroupVolume(groupId); //Set group and device as active. injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE); @@ -1445,7 +1448,7 @@ public class LeAudioServiceTest { verify(mAudioManager, times(1)).handleBluetoothActiveDeviceChanged(eq(null), any(), any(BluetoothProfileConnectionInfo.class)); - doReturn(100).when(mVolumeControlService).getGroupVolume(groupId); + doReturn(100).when(mVolumeControlService).getAudioDeviceGroupVolume(groupId); //Set back to active and check if last volume is restored. injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE); diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoContactElementTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoContactElementTest.java new file mode 100644 index 0000000000..4e6a144e90 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoContactElementTest.java @@ -0,0 +1,203 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.bluetooth.map; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.bluetooth.SignedLongLong; +import com.android.internal.util.FastXmlSerializer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlSerializer; + +import java.io.StringReader; +import java.io.StringWriter; +import java.text.SimpleDateFormat; + +@RunWith(AndroidJUnit4.class) +public class BluetoothMapConvoContactElementTest { + private static final String TEST_UCI = "test_bt_uci"; + private static final String TEST_NAME = "test_name"; + private static final String TEST_DISPLAY_NAME = "test_display_name"; + private static final String TEST_PRESENCE_STATUS = "test_presence_status"; + private static final int TEST_PRESENCE_AVAILABILITY = 2; + private static final long TEST_LAST_ACTIVITY = 1; + private static final int TEST_CHAT_STATE = 2; + private static final int TEST_PRIORITY = 1; + private static final String TEST_BT_UID = "1111"; + + private final SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); + + @Mock + private MapContact mMapContact; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void constructorWithArguments() { + BluetoothMapConvoContactElement contactElement = + new BluetoothMapConvoContactElement(TEST_UCI, TEST_NAME, TEST_DISPLAY_NAME, + TEST_PRESENCE_STATUS, TEST_PRESENCE_AVAILABILITY, TEST_LAST_ACTIVITY, + TEST_CHAT_STATE, TEST_PRIORITY, TEST_BT_UID); + + assertThat(contactElement.getContactId()).isEqualTo(TEST_UCI); + assertThat(contactElement.getName()).isEqualTo(TEST_NAME); + assertThat(contactElement.getDisplayName()).isEqualTo(TEST_DISPLAY_NAME); + assertThat(contactElement.getPresenceStatus()).isEqualTo(TEST_PRESENCE_STATUS); + assertThat(contactElement.getPresenceAvailability()).isEqualTo(TEST_PRESENCE_AVAILABILITY); + assertThat(contactElement.getLastActivityString()).isEqualTo( + format.format(TEST_LAST_ACTIVITY)); + assertThat(contactElement.getChatState()).isEqualTo(TEST_CHAT_STATE); + assertThat(contactElement.getPriority()).isEqualTo(TEST_PRIORITY); + assertThat(contactElement.getBtUid()).isEqualTo(TEST_BT_UID); + } + + @Test + public void createFromMapContact() { + final long id = 1111; + final SignedLongLong signedLongLong = new SignedLongLong(id, 0); + when(mMapContact.getId()).thenReturn(id); + when(mMapContact.getName()).thenReturn(TEST_DISPLAY_NAME); + BluetoothMapConvoContactElement contactElement = + BluetoothMapConvoContactElement.createFromMapContact(mMapContact, TEST_UCI); + assertThat(contactElement.getContactId()).isEqualTo(TEST_UCI); + assertThat(contactElement.getBtUid()).isEqualTo(signedLongLong.toHexString()); + assertThat(contactElement.getDisplayName()).isEqualTo(TEST_DISPLAY_NAME); + } + + @Test + public void settersAndGetters() throws Exception { + BluetoothMapConvoContactElement contactElement = new BluetoothMapConvoContactElement(); + contactElement.setDisplayName(TEST_DISPLAY_NAME); + contactElement.setPresenceStatus(TEST_PRESENCE_STATUS); + contactElement.setPresenceAvailability(TEST_PRESENCE_AVAILABILITY); + contactElement.setPriority(TEST_PRIORITY); + contactElement.setName(TEST_NAME); + contactElement.setBtUid(SignedLongLong.fromString(TEST_BT_UID)); + contactElement.setChatState(TEST_CHAT_STATE); + contactElement.setLastActivity(TEST_LAST_ACTIVITY); + contactElement.setContactId(TEST_UCI); + + assertThat(contactElement.getContactId()).isEqualTo(TEST_UCI); + assertThat(contactElement.getName()).isEqualTo(TEST_NAME); + assertThat(contactElement.getDisplayName()).isEqualTo(TEST_DISPLAY_NAME); + assertThat(contactElement.getPresenceStatus()).isEqualTo(TEST_PRESENCE_STATUS); + assertThat(contactElement.getPresenceAvailability()).isEqualTo(TEST_PRESENCE_AVAILABILITY); + assertThat(contactElement.getLastActivityString()).isEqualTo( + format.format(TEST_LAST_ACTIVITY)); + assertThat(contactElement.getChatState()).isEqualTo(TEST_CHAT_STATE); + assertThat(contactElement.getPriority()).isEqualTo(TEST_PRIORITY); + assertThat(contactElement.getBtUid()).isEqualTo(TEST_BT_UID); + } + + @Test + public void encodeToXml_thenDecodeToInstance_returnsCorrectly() throws Exception { + BluetoothMapConvoContactElement contactElement = new + BluetoothMapConvoContactElement(TEST_UCI, TEST_NAME, TEST_DISPLAY_NAME, + TEST_PRESENCE_STATUS, TEST_PRESENCE_AVAILABILITY, TEST_LAST_ACTIVITY, + TEST_CHAT_STATE, TEST_PRIORITY, TEST_BT_UID); + + final XmlSerializer serializer = new FastXmlSerializer(); + final StringWriter writer = new StringWriter(); + + serializer.setOutput(writer); + serializer.startDocument("UTF-8", true); + contactElement.encode(serializer); + serializer.endDocument(); + + final XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + final XmlPullParser parser; + parser = parserFactory.newPullParser(); + + parser.setInput(new StringReader(writer.toString())); + parser.next(); + + BluetoothMapConvoContactElement contactElementFromXml = + BluetoothMapConvoContactElement.createFromXml(parser); + + assertThat(contactElementFromXml.getContactId()).isEqualTo(TEST_UCI); + assertThat(contactElementFromXml.getName()).isEqualTo(TEST_NAME); + assertThat(contactElementFromXml.getDisplayName()).isEqualTo(TEST_DISPLAY_NAME); + assertThat(contactElementFromXml.getPresenceStatus()).isEqualTo(TEST_PRESENCE_STATUS); + assertThat(contactElementFromXml.getPresenceAvailability()).isEqualTo( + TEST_PRESENCE_AVAILABILITY); + assertThat(contactElementFromXml.getLastActivityString()).isEqualTo( + format.format(TEST_LAST_ACTIVITY)); + assertThat(contactElementFromXml.getChatState()).isEqualTo(TEST_CHAT_STATE); + assertThat(contactElementFromXml.getPriority()).isEqualTo(TEST_PRIORITY); + assertThat(contactElementFromXml.getBtUid()).isEqualTo(TEST_BT_UID); + } + + @Test + public void equalsWithSameValues_returnsTrue() { + BluetoothMapConvoContactElement contactElement = + new BluetoothMapConvoContactElement(TEST_UCI, TEST_NAME, TEST_DISPLAY_NAME, + TEST_PRESENCE_STATUS, TEST_PRESENCE_AVAILABILITY, TEST_LAST_ACTIVITY, + TEST_CHAT_STATE, TEST_PRIORITY, TEST_BT_UID); + + BluetoothMapConvoContactElement contactElementEqual = + new BluetoothMapConvoContactElement(TEST_UCI, TEST_NAME, TEST_DISPLAY_NAME, + TEST_PRESENCE_STATUS, TEST_PRESENCE_AVAILABILITY, TEST_LAST_ACTIVITY, + TEST_CHAT_STATE, TEST_PRIORITY, TEST_BT_UID); + + assertThat(contactElement).isEqualTo(contactElementEqual); + } + + @Test + public void equalsWithDifferentPriority_returnsFalse() { + BluetoothMapConvoContactElement contactElement = + new BluetoothMapConvoContactElement(TEST_UCI, TEST_NAME, TEST_DISPLAY_NAME, + TEST_PRESENCE_STATUS, TEST_PRESENCE_AVAILABILITY, TEST_LAST_ACTIVITY, + TEST_CHAT_STATE, TEST_PRIORITY, TEST_BT_UID); + + BluetoothMapConvoContactElement contactElementWithDifferentPriority = + new BluetoothMapConvoContactElement(TEST_UCI, TEST_NAME, TEST_DISPLAY_NAME, + TEST_PRESENCE_STATUS, TEST_PRESENCE_AVAILABILITY, TEST_LAST_ACTIVITY, + TEST_CHAT_STATE, /*priority=*/0, TEST_BT_UID); + + assertThat(contactElement).isNotEqualTo(contactElementWithDifferentPriority); + } + + @Test + public void compareTo_withSameValues_returnsZero() { + BluetoothMapConvoContactElement contactElement = + new BluetoothMapConvoContactElement(TEST_UCI, TEST_NAME, TEST_DISPLAY_NAME, + TEST_PRESENCE_STATUS, TEST_PRESENCE_AVAILABILITY, TEST_LAST_ACTIVITY, + TEST_CHAT_STATE, TEST_PRIORITY, TEST_BT_UID); + + BluetoothMapConvoContactElement contactElementSameLastActivity = + new BluetoothMapConvoContactElement(TEST_UCI, TEST_NAME, TEST_DISPLAY_NAME, + TEST_PRESENCE_STATUS, TEST_PRESENCE_AVAILABILITY, TEST_LAST_ACTIVITY, + TEST_CHAT_STATE, TEST_PRIORITY, TEST_BT_UID); + + assertThat(contactElement.compareTo(contactElementSameLastActivity)).isEqualTo(0); + } +}
\ No newline at end of file diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoListingElementTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoListingElementTest.java new file mode 100644 index 0000000000..84b0d7fac2 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoListingElementTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.bluetooth.map; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.bluetooth.SignedLongLong; +import com.android.bluetooth.map.BluetoothMapUtils.TYPE; +import com.android.internal.util.FastXmlSerializer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlSerializer; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class BluetoothMapConvoListingElementTest { + private static final long TEST_ID = 1111; + private static final String TEST_NAME = "test_name"; + private static final long TEST_LAST_ACTIVITY = 0; + private static final boolean TEST_READ = true; + private static final boolean TEST_REPORT_READ = true; + private static final long TEST_VERSION_COUNTER = 0; + private static final int TEST_CURSOR_INDEX = 1; + private static final TYPE TEST_TYPE = TYPE.EMAIL; + private static final String TEST_SUMMARY = "test_summary"; + private static final String TEST_SMS_MMS_CONTACTS = "test_sms_mms_contacts"; + + private final BluetoothMapConvoContactElement TEST_CONTACT_ELEMENT_ONE = + new BluetoothMapConvoContactElement("test_uci_one", "test_name_one", + "test_display_name_one", "test_presence_status_one", 2, TEST_LAST_ACTIVITY, 2, + 1, "1111"); + + private final BluetoothMapConvoContactElement TEST_CONTACT_ELEMENT_TWO = + new BluetoothMapConvoContactElement("test_uci_two", "test_name_two", + "test_display_name_two", "test_presence_status_two", 1, TEST_LAST_ACTIVITY, 1, + 2, "1112"); + + private final List<BluetoothMapConvoContactElement> TEST_CONTACTS = new ArrayList<>( + Arrays.asList(TEST_CONTACT_ELEMENT_ONE, TEST_CONTACT_ELEMENT_TWO)); + + private final SignedLongLong signedLongLong = new SignedLongLong(TEST_ID, 0); + + private BluetoothMapConvoListingElement mListingElement; + + @Before + public void setUp() throws Exception { + mListingElement = new BluetoothMapConvoListingElement(); + + mListingElement.setCursorIndex(TEST_CURSOR_INDEX); + mListingElement.setVersionCounter(TEST_VERSION_COUNTER); + mListingElement.setName(TEST_NAME); + mListingElement.setType(TEST_TYPE); + mListingElement.setContacts(TEST_CONTACTS); + mListingElement.setLastActivity(TEST_LAST_ACTIVITY); + mListingElement.setRead(TEST_READ, TEST_REPORT_READ); + mListingElement.setConvoId(0, TEST_ID); + mListingElement.setSummary(TEST_SUMMARY); + mListingElement.setSmsMmsContacts(TEST_SMS_MMS_CONTACTS); + } + + @Test + public void getters() throws Exception { + assertThat(mListingElement.getCursorIndex()).isEqualTo(TEST_CURSOR_INDEX); + assertThat(mListingElement.getVersionCounter()).isEqualTo(TEST_VERSION_COUNTER); + assertThat(mListingElement.getName()).isEqualTo(TEST_NAME); + assertThat(mListingElement.getType()).isEqualTo(TEST_TYPE); + assertThat(mListingElement.getContacts()).isEqualTo(TEST_CONTACTS); + assertThat(mListingElement.getLastActivity()).isEqualTo(TEST_LAST_ACTIVITY); + assertThat(mListingElement.getRead()).isEqualTo("READ"); + assertThat(mListingElement.getReadBool()).isEqualTo(TEST_READ); + assertThat(mListingElement.getConvoId()).isEqualTo(signedLongLong.toHexString()); + assertThat(mListingElement.getCpConvoId()).isEqualTo( + signedLongLong.getLeastSignificantBits()); + assertThat(mListingElement.getFullSummary()).isEqualTo(TEST_SUMMARY); + assertThat(mListingElement.getSmsMmsContacts()).isEqualTo(TEST_SMS_MMS_CONTACTS); + } + + @Test + public void incrementVersionCounter() { + mListingElement.incrementVersionCounter(); + assertThat(mListingElement.getVersionCounter()).isEqualTo(TEST_VERSION_COUNTER + 1); + } + + @Test + public void removeContactWithObject() { + mListingElement.removeContact(TEST_CONTACT_ELEMENT_TWO); + assertThat(mListingElement.getContacts().size()).isEqualTo(1); + } + + @Test + public void removeContactWithIndex() { + mListingElement.removeContact(1); + assertThat(mListingElement.getContacts().size()).isEqualTo(1); + } + + @Test + public void encodeToXml_thenDecodeToInstance_returnsCorrectly() throws Exception { + final XmlSerializer serializer = new FastXmlSerializer(); + final StringWriter writer = new StringWriter(); + + serializer.setOutput(writer); + serializer.startDocument("UTF-8", true); + mListingElement.encode(serializer); + serializer.endDocument(); + + final XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + final XmlPullParser parser; + parser = parserFactory.newPullParser(); + + parser.setInput(new StringReader(writer.toString())); + parser.next(); + + BluetoothMapConvoListingElement listingElementFromXml = + BluetoothMapConvoListingElement.createFromXml(parser); + + assertThat(listingElementFromXml.getVersionCounter()).isEqualTo(0); + assertThat(listingElementFromXml.getName()).isEqualTo(TEST_NAME); + assertThat(listingElementFromXml.getContacts()).isEqualTo(TEST_CONTACTS); + assertThat(listingElementFromXml.getLastActivity()).isEqualTo(TEST_LAST_ACTIVITY); + assertThat(listingElementFromXml.getRead()).isEqualTo("UNREAD"); + assertThat(listingElementFromXml.getConvoId()).isEqualTo(signedLongLong.toHexString()); + assertThat(listingElementFromXml.getFullSummary().trim()).isEqualTo(TEST_SUMMARY); + } + + @Test + public void equalsWithSameValues_returnsTrue() { + BluetoothMapConvoListingElement listingElement = new BluetoothMapConvoListingElement(); + listingElement.setName(TEST_NAME); + listingElement.setContacts(TEST_CONTACTS); + listingElement.setLastActivity(TEST_LAST_ACTIVITY); + listingElement.setRead(TEST_READ, TEST_REPORT_READ); + + BluetoothMapConvoListingElement listingElementEqual = new BluetoothMapConvoListingElement(); + listingElementEqual.setName(TEST_NAME); + listingElementEqual.setContacts(TEST_CONTACTS); + listingElementEqual.setLastActivity(TEST_LAST_ACTIVITY); + listingElementEqual.setRead(TEST_READ, TEST_REPORT_READ); + + assertThat(listingElement).isEqualTo(listingElementEqual); + } + + @Test + public void equalsWithDifferentRead_returnsFalse() { + BluetoothMapConvoListingElement + listingElement = new BluetoothMapConvoListingElement(); + + BluetoothMapConvoListingElement listingElementWithDifferentRead = + new BluetoothMapConvoListingElement(); + listingElementWithDifferentRead.setRead(TEST_READ, TEST_REPORT_READ); + + assertThat(listingElement).isNotEqualTo(listingElementWithDifferentRead); + } + + @Test + public void compareToWithSameValues_returnsZero() { + BluetoothMapConvoListingElement + listingElement = new BluetoothMapConvoListingElement(); + listingElement.setLastActivity(TEST_LAST_ACTIVITY); + + BluetoothMapConvoListingElement listingElementSameLastActivity = + new BluetoothMapConvoListingElement(); + listingElementSameLastActivity.setLastActivity(TEST_LAST_ACTIVITY); + + assertThat(listingElement.compareTo(listingElementSameLastActivity)).isEqualTo(0); + } +}
\ No newline at end of file diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java index d8e2b62647..33089f4cc0 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java @@ -38,6 +38,8 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.bluetooth.BluetoothMethodProxy; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -61,7 +63,7 @@ public class BluetoothPbapCallLogComposerTest { private BluetoothPbapCallLogComposer mComposer; @Spy - BluetoothPbapMethodProxy mPbapCallProxy = BluetoothPbapMethodProxy.getInstance(); + BluetoothMethodProxy mPbapCallProxy = BluetoothMethodProxy.getInstance(); @Mock Cursor mMockCursor; @@ -69,7 +71,7 @@ public class BluetoothPbapCallLogComposerTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - BluetoothPbapMethodProxy.setInstanceForTesting(mPbapCallProxy); + BluetoothMethodProxy.setInstanceForTesting(mPbapCallProxy); doReturn(mMockCursor).when(mPbapCallProxy) .contentResolverQuery(any(), any(), any(), any(), any(), any()); @@ -82,7 +84,7 @@ public class BluetoothPbapCallLogComposerTest { @After public void tearDown() throws Exception { - BluetoothPbapMethodProxy.setInstanceForTesting(null); + BluetoothMethodProxy.setInstanceForTesting(null); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java index b888930020..1ad09189af 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java @@ -27,15 +27,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.content.Context; import android.os.Handler; import android.os.UserManager; -import android.util.Log; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.bluetooth.BluetoothMethodProxy; import com.android.obex.HeaderSet; import com.android.obex.Operation; import com.android.obex.ResponseCodes; @@ -63,7 +62,7 @@ public class BluetoothPbapObexServerTest { @Mock PbapStateMachine mMockStateMachine; @Spy - BluetoothPbapMethodProxy mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance(); + BluetoothMethodProxy mPbapMethodProxy = BluetoothMethodProxy.getInstance(); BluetoothPbapObexServer mServer; @@ -85,14 +84,14 @@ public class BluetoothPbapObexServerTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - BluetoothPbapMethodProxy.setInstanceForTesting(mPbapMethodProxy); + BluetoothMethodProxy.setInstanceForTesting(mPbapMethodProxy); mServer = new BluetoothPbapObexServer( mMockHandler, InstrumentationRegistry.getTargetContext(), mMockStateMachine); } @After public void tearDown() throws Exception { - BluetoothPbapMethodProxy.setInstanceForTesting(null); + BluetoothMethodProxy.setInstanceForTesting(null); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java index 5492e1cc98..023027542c 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java @@ -35,6 +35,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.bluetooth.BluetoothMethodProxy; import com.android.obex.Operation; import com.android.obex.ResponseCodes; @@ -60,7 +61,7 @@ public class BluetoothPbapSimVcardManagerTest { private static final String TAG = BluetoothPbapSimVcardManagerTest.class.getSimpleName(); @Spy - BluetoothPbapMethodProxy mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance(); + BluetoothMethodProxy mPbapMethodProxy = BluetoothMethodProxy.getInstance(); Context mContext; BluetoothPbapSimVcardManager mManager; @@ -70,14 +71,14 @@ public class BluetoothPbapSimVcardManagerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - BluetoothPbapMethodProxy.setInstanceForTesting(mPbapMethodProxy); + BluetoothMethodProxy.setInstanceForTesting(mPbapMethodProxy); mContext = InstrumentationRegistry.getTargetContext(); mManager = new BluetoothPbapSimVcardManager(mContext); } @After public void tearDown() { - BluetoothPbapMethodProxy.setInstanceForTesting(null); + BluetoothMethodProxy.setInstanceForTesting(null); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java index b6cceb5709..fa8bb5a51e 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java @@ -27,7 +27,6 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.database.Cursor; -import android.net.Uri; import android.provider.CallLog; import android.provider.ContactsContract; @@ -35,6 +34,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.bluetooth.BluetoothMethodProxy; import com.android.bluetooth.R; import org.junit.After; @@ -56,7 +56,7 @@ public class BluetoothPbapVcardManagerTest { private static final String TAG = BluetoothPbapVcardManagerTest.class.getSimpleName(); @Spy - BluetoothPbapMethodProxy mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance(); + BluetoothMethodProxy mPbapMethodProxy = BluetoothMethodProxy.getInstance(); Context mContext; BluetoothPbapVcardManager mManager; @@ -64,14 +64,14 @@ public class BluetoothPbapVcardManagerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - BluetoothPbapMethodProxy.setInstanceForTesting(mPbapMethodProxy); + BluetoothMethodProxy.setInstanceForTesting(mPbapMethodProxy); mContext = InstrumentationRegistry.getTargetContext(); mManager = new BluetoothPbapVcardManager(mContext); } @After public void tearDown() { - BluetoothPbapMethodProxy.setInstanceForTesting(null); + BluetoothMethodProxy.setInstanceForTesting(null); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandlerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandlerTest.java new file mode 100644 index 0000000000..dc83c978a4 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandlerTest.java @@ -0,0 +1,252 @@ +/* + * 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.pbapclient; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.accounts.Account; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.SdpPseRecord; +import android.content.ContentResolver; +import android.content.Context; +import android.content.ContextWrapper; +import android.os.HandlerThread; +import android.os.Looper; +import android.util.Log; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.ServiceTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.android.bluetooth.TestUtils; +import com.android.bluetooth.btservice.AdapterService; +import com.android.bluetooth.btservice.storage.DatabaseManager; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashMap; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PbapClientConnectionHandlerTest { + + private static final String TAG = "ConnHandlerTest"; + private static final String REMOTE_DEVICE_ADDRESS = "00:00:00:00:00:00"; + + private HandlerThread mThread; + private Looper mLooper; + private Context mTargetContext; + private BluetoothDevice mRemoteDevice; + + @Rule + public final ServiceTestRule mServiceRule = new ServiceTestRule(); + + @Mock + private AdapterService mAdapterService; + + @Mock + private DatabaseManager mDatabaseManager; + + private BluetoothAdapter mAdapter; + + private PbapClientService mService; + + private PbapClientStateMachine mStateMachine; + + private PbapClientConnectionHandler mHandler; + + @Before + public void setUp() throws Exception { + mTargetContext = spy(new ContextWrapper( + InstrumentationRegistry.getInstrumentation().getTargetContext())); + Assume.assumeTrue("Ignore test when PbapClientService is not enabled", + PbapClientService.isEnabled()); + MockitoAnnotations.initMocks(this); + TestUtils.setAdapterService(mAdapterService); + doReturn(mDatabaseManager).when(mAdapterService).getDatabase(); + doReturn(true, false).when(mAdapterService) + .isStartedProfile(anyString()); + TestUtils.startService(mServiceRule, PbapClientService.class); + mService = PbapClientService.getPbapClientService(); + assertThat(mService).isNotNull(); + + mAdapter = BluetoothAdapter.getDefaultAdapter(); + + mThread = new HandlerThread("test_handler_thread"); + mThread.start(); + mLooper = mThread.getLooper(); + mRemoteDevice = mAdapter.getRemoteDevice(REMOTE_DEVICE_ADDRESS); + + mStateMachine = new PbapClientStateMachine(mService, mRemoteDevice); + mHandler = new PbapClientConnectionHandler.Builder() + .setLooper(mLooper) + .setClientSM(mStateMachine) + .setContext(mTargetContext) + .setRemoteDevice(mRemoteDevice) + .build(); + } + + @After + public void tearDown() throws Exception { + if (!PbapClientService.isEnabled()) { + return; + } + TestUtils.stopService(mServiceRule, PbapClientService.class); + mService = PbapClientService.getPbapClientService(); + assertThat(mService).isNull(); + TestUtils.clearAdapterService(mAdapterService); + mLooper.quit(); + } + + @Test + public void connectSocket_whenBluetoothIsNotEnabled_returnsFalse() { + assertThat(mHandler.connectSocket()).isFalse(); + } + + @Test + public void connectSocket_whenBluetoothIsNotEnabled_returnsFalse_withInvalidL2capPsm() { + SdpPseRecord record = mock(SdpPseRecord.class); + mHandler.setPseRecord(record); + + when(record.getL2capPsm()).thenReturn(PbapClientConnectionHandler.L2CAP_INVALID_PSM); + assertThat(mHandler.connectSocket()).isFalse(); + } + + @Test + public void connectSocket_whenBluetoothIsNotEnabled_returnsFalse_withValidL2capPsm() { + SdpPseRecord record = mock(SdpPseRecord.class); + mHandler.setPseRecord(record); + + when(record.getL2capPsm()).thenReturn(1); // Valid PSM ranges 1 to 30; + assertThat(mHandler.connectSocket()).isFalse(); + } + + // TODO: Add connectObexSession_returnsTrue + + @Test + public void connectObexSession_returnsFalse_withoutConnectingSocket() { + assertThat(mHandler.connectObexSession()).isFalse(); + } + + @Test + public void abort() { + SdpPseRecord record = mock(SdpPseRecord.class); + when(record.getL2capPsm()).thenReturn(1); // Valid PSM ranges 1 to 30; + mHandler.setPseRecord(record); + mHandler.connectSocket(); // Workaround for setting mSocket as non-null value + assertThat(mHandler.getSocket()).isNotNull(); + + mHandler.abort(); + + assertThat(mThread.isInterrupted()).isTrue(); + assertThat(mHandler.getSocket()).isNull(); + } + + @Test + public void downloadContacts() { + final String path = PbapClientConnectionHandler.PB_PATH; + + try { + mHandler.downloadContacts(path); + } catch (Exception e) { + Log.e(TAG, "Exception happened.", e); + assertWithMessage("Exception should not be thrown!").fail(); + } + } + + @Test + public void downloadCallLog() { + final String path = PbapClientConnectionHandler.ICH_PATH; + final HashMap<String, Integer> callCounter = new HashMap<>(); + + try { + mHandler.downloadCallLog(path, callCounter); + } catch (Exception e) { + Log.e(TAG, "Exception happened.", e); + assertWithMessage("Exception should not be thrown!").fail(); + } + } + + @Test + public void addAccount() { + try { + mHandler.addAccount(mock(Account.class)); + } catch (Exception e) { + Log.e(TAG, "Exception happened.", e); + assertWithMessage("Exception should not be thrown!").fail(); + } + } + + @Test + public void removeAccount() { + try { + mHandler.removeAccount(); + } catch (Exception e) { + Log.e(TAG, "Exception happened.", e); + assertWithMessage("Exception should not be thrown!").fail(); + } + } + + @Test + public void removeCallLog() { + try { + ContentResolver res = mock(ContentResolver.class); + when(mTargetContext.getContentResolver()).thenReturn(res); + mHandler.removeCallLog(); + + when(mTargetContext.getContentResolver()).thenReturn(null); + mHandler.removeCallLog(); + } catch (Exception e) { + Log.e(TAG, "Exception happened.", e); + assertWithMessage("Exception should not be thrown!").fail(); + } + } + + @Test + public void isRepositorySupported_withoutSettingPseRecord_returnsFalse() { + mHandler.setPseRecord(null); + final int mask = 0x11; + + assertThat(mHandler.isRepositorySupported(mask)).isFalse(); + } + + @Test + public void isRepositorySupported_withSettingPseRecord() { + SdpPseRecord record = mock(SdpPseRecord.class); + when(record.getSupportedRepositories()).thenReturn(1); + mHandler.setPseRecord(record); + final int mask = 0x11; + + assertThat(mHandler.isRepositorySupported(mask)).isTrue(); + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java index 7ca7623792..bdd81699bb 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java @@ -41,7 +41,9 @@ import androidx.test.rule.ServiceTestRule; import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.btservice.AdapterService; +import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; +import com.android.bluetooth.csip.CsipSetCoordinatorService; import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import com.android.bluetooth.x.com.android.modules.utils.SynchronousResultReceiver; @@ -73,6 +75,7 @@ public class VolumeControlServiceTest { private VolumeControlService mService; private VolumeControlService.BluetoothVolumeControlBinder mServiceBinder; private BluetoothDevice mDevice; + private BluetoothDevice mDeviceTwo; private HashMap<BluetoothDevice, LinkedBlockingQueue<Intent>> mDeviceQueueMap; private static final int TIMEOUT_MS = 1000; private static final int BT_LE_AUDIO_MAX_VOL = 255; @@ -87,6 +90,8 @@ public class VolumeControlServiceTest { @Mock private DatabaseManager mDatabaseManager; @Mock private VolumeControlNativeInterface mNativeInterface; @Mock private AudioManager mAudioManager; + @Mock private ServiceFactory mServiceFactory; + @Mock private CsipSetCoordinatorService mCsipService; @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule(); @@ -119,9 +124,12 @@ public class VolumeControlServiceTest { startService(); mService.mVolumeControlNativeInterface = mNativeInterface; mService.mAudioManager = mAudioManager; + mService.mFactory = mServiceFactory; mServiceBinder = (VolumeControlService.BluetoothVolumeControlBinder) mService.initBinder(); mServiceBinder.mIsTesting = true; + doReturn(mCsipService).when(mServiceFactory).getCsipSetCoordinatorService(); + // Override the timeout value to speed up the test VolumeControlStateMachine.sConnectTimeoutMs = TIMEOUT_MS; // 1s @@ -134,8 +142,10 @@ public class VolumeControlServiceTest { // Get a device for testing mDevice = TestUtils.getTestDevice(mAdapter, 0); + mDeviceTwo = TestUtils.getTestDevice(mAdapter, 1); mDeviceQueueMap = new HashMap<>(); mDeviceQueueMap.put(mDevice, new LinkedBlockingQueue<>()); + mDeviceQueueMap.put(mDeviceTwo, new LinkedBlockingQueue<>()); doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService) .getBondState(any(BluetoothDevice.class)); doReturn(new ParcelUuid[]{BluetoothUuid.VOLUME_CONTROL}).when(mAdapterService) @@ -631,6 +641,92 @@ public class VolumeControlServiceTest { Assert.assertEquals(volume, mService.getGroupVolume(groupId)); } + /** + * Test setting volume for a group member who connects after the volume level + * for a group was already changed and cached. + */ + @Test + public void testLateConnectingDevice() throws Exception { + int groupId = 1; + int groupVolume = 56; + + // Both devices are in the same group + when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId); + when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId); + + // Update the device policy so okToConnect() returns true + when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); + when(mDatabaseManager + .getProfileConnectionPolicy(any(BluetoothDevice.class), + eq(BluetoothProfile.VOLUME_CONTROL))) + .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); + doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); + doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); + + generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_DISCONNECTED); + Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, + mService.getConnectionState(mDevice)); + Assert.assertTrue(mService.getDevices().contains(mDevice)); + + mService.setGroupVolume(groupId, groupVolume); + verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(groupVolume)); + verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume)); + + // Verify that second device gets the proper group volume level when connected + generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_DISCONNECTED); + Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, + mService.getConnectionState(mDeviceTwo)); + Assert.assertTrue(mService.getDevices().contains(mDeviceTwo)); + verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume)); + } + + /** + * Test setting volume for a new group member who is discovered after the volume level + * for a group was already changed and cached. + */ + @Test + public void testLateDiscoveredGroupMember() throws Exception { + int groupId = 1; + int groupVolume = 56; + + // For now only one device is in the group + when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId); + when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(-1); + + // Update the device policy so okToConnect() returns true + when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); + when(mDatabaseManager + .getProfileConnectionPolicy(any(BluetoothDevice.class), + eq(BluetoothProfile.VOLUME_CONTROL))) + .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); + doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); + doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); + + generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_DISCONNECTED); + Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, + mService.getConnectionState(mDevice)); + Assert.assertTrue(mService.getDevices().contains(mDevice)); + + // Set the group volume + mService.setGroupVolume(groupId, groupVolume); + + // Verify that second device will not get the group volume level if it is not a group member + generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_DISCONNECTED); + Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, + mService.getConnectionState(mDeviceTwo)); + Assert.assertTrue(mService.getDevices().contains(mDeviceTwo)); + verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume)); + + // But gets the volume when it becomes the group member + when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId); + mService.handleGroupNodeAdded(groupId, mDeviceTwo); + verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume)); + } + @Test public void testServiceBinderGetDevicesMatchingConnectionStates() throws Exception { final SynchronousResultReceiver<List<BluetoothDevice>> recv = diff --git a/android/pandora/.gitignore b/android/pandora/.gitignore new file mode 100644 index 0000000000..cdb0870bb5 --- /dev/null +++ b/android/pandora/.gitignore @@ -0,0 +1,3 @@ +trace* +log* +out* diff --git a/android/pandora/gen_cov.py b/android/pandora/gen_cov.py new file mode 100755 index 0000000000..3943341b06 --- /dev/null +++ b/android/pandora/gen_cov.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python + +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import sys +import xml.etree.ElementTree as ET + + +def run_pts_bot(): + run_pts_bot_cmd = [ + # atest command with verbose mode. + 'atest', + '-d', + '-v', + 'pts-bot', + # Coverage tool chains and specify that coverage should be flush to the + # disk between each tests. + '--', + '--coverage', + '--coverage-toolchain JACOCO', + '--coverage-toolchain CLANG', + '--coverage-flush', + ] + subprocess.run(run_pts_bot_cmd).returncode + + +def run_unit_tests(): + + # Output logs directory + logs_out = Path('logs_bt_tests') + logs_out.mkdir(exist_ok=True) + + mts_tests = [] + android_build_top = os.getenv('ANDROID_BUILD_TOP') + mts_xml = ET.parse( + f'{android_build_top}/test/mts/tools/mts-tradefed/res/config/mts-bluetooth-tests-list.xml' + ) + + for child in mts_xml.getroot(): + value = child.attrib['value'] + if 'enable:true' in value: + test = value.replace(':enable:true', '') + mts_tests.append(test) + + for test in mts_tests: + print(f'Test started: {test}') + + # Env variables necessary for native unit tests. + env = os.environ.copy() + env['CLANG_COVERAGE_CONTINUOUS_MODE'] = 'true' + env['CLANG_COVERAGE'] = 'true' + env['NATIVE_COVERAGE_PATHS'] = 'packages/modules/Bluetooth' + run_test_cmd = [ + # atest command with verbose mode. + 'atest', + '-d', + '-v', + test, + # Coverage tool chains and specify that coverage should be flush to the + # disk between each tests. + '--', + '--coverage', + '--coverage-toolchain JACOCO', + '--coverage-toolchain CLANG', + '--coverage-flush', + # Allows tests to use hidden APIs. + '--test-arg ', + 'com.android.compatibility.testtype.LibcoreTest:hidden-api-checks:false', + '--test-arg ', + 'com.android.tradefed.testtype.AndroidJUnitTest:hidden-api-checks:false', + '--test-arg ', + 'com.android.tradefed.testtype.InstrumentationTest:hidden-api-checks:false', + '--skip-system-status-check ', + 'com.android.tradefed.suite.checker.ShellStatusChecker', + ] + with open(f'{logs_out}/{test}.txt', 'w') as f: + returncode = subprocess.run( + run_test_cmd, env=env, stdout=f, stderr=subprocess.STDOUT).returncode + print( + f'Test ended [{"Success" if returncode == 0 else "Failed"}]: {test}') + + +def generate_java_coverage(bt_apex_name, trace_path, coverage_out): + + out = os.getenv('OUT') + android_host_out = os.getenv('ANDROID_HOST_OUT') + + java_coverage_out = Path(f'{coverage_out}/java') + temp_path = Path(f'{coverage_out}/temp') + if temp_path.exists(): + shutil.rmtree(temp_path, ignore_errors=True) + temp_path.mkdir() + + framework_jar_path = Path( + f'{out}/obj/PACKAGING/jacoco_intermediates/JAVA_LIBRARIES/framework-bluetooth.{bt_apex_name}_intermediates' + ) + service_jar_path = Path( + f'{out}/obj/PACKAGING/jacoco_intermediates/JAVA_LIBRARIES/service-bluetooth.{bt_apex_name}_intermediates' + ) + app_jar_path = Path( + f'{out}/obj/PACKAGING/jacoco_intermediates/ETC/Bluetooth{"Google" if "com.google" in bt_apex_name else ""}.{bt_apex_name}_intermediates' + ) + + # From google3/configs/wireless/android/testing/atp/prod/mainline-engprod/templates/modules/bluetooth.gcl. + framework_exclude_classes = [ + '**/com/android/bluetooth/x/**/*.class', + '**/*Test$*.class', + '**/android/bluetooth/I*$Default.class', + '**/android/bluetooth/**/I*$Default.class', + '**/android/bluetooth/I*$Stub.class', + '**/android/bluetooth/**/I*$Stub.class', + '**/android/bluetooth/I*$Stub$Proxy.class', + '**/android/bluetooth/**/I*$Stub$Proxy.class', + '**/com/android/internal/util/**/*.class', + '**/android/net/**/*.class', + ] + service_exclude_classes = [ + '**/com/android/bluetooth/x/**/*.class', + '**/androidx/**/*.class', + '**/android/net/**/*.class', + '**/android/support/**/*.class', + '**/kotlin/**/*.class', + '**/*Test$*.class', + '**/com/android/internal/annotations/**/*.class', + '**/android/annotation/**/*.class', + '**/android/net/**/*.class', + ] + app_exclude_classes = [ + '**/*Test$*.class', + '**/com/android/bluetooth/x/**/*.class', + '**/com/android/internal/annotations/**/*.class', + '**/com/android/internal/util/**/*.class', + '**/android/annotation/**/*.class', + '**/android/net/**/*.class', + '**/android/support/v4/**/*.class', + '**/androidx/**/*.class', + '**/kotlin/**/*.class', + '**/com/google/**/*.class', + '**/javax/**/*.class', + '**/android/hardware/**/*.class', # Added + '**/android/hidl/**/*.class', # Added + '**/com/android/bluetooth/**/BluetoothMetrics*.class', # Added + ] + + # Merged ec files. + merged_ec_path = Path(f'{temp_path}/merged.ec') + subprocess.run(( + f'java -jar {android_host_out}/framework/jacoco-cli.jar merge {trace_path.absolute()}/*.ec ' + f'--destfile {merged_ec_path.absolute()}'), + shell=True) + + # Copy and extract jar files. + framework_temp_path = Path(f'{temp_path}/{framework_jar_path.name}') + service_temp_path = Path(f'{temp_path}/{service_jar_path.name}') + app_temp_path = Path(f'{temp_path}/{app_jar_path.name}') + + shutil.copytree(framework_jar_path, framework_temp_path) + shutil.copytree(service_jar_path, service_temp_path) + shutil.copytree(app_jar_path, app_temp_path) + + current_dir_path = Path.cwd() + for p in [framework_temp_path, service_temp_path, app_temp_path]: + os.chdir(p.absolute()) + os.system('jar xf jacoco-report-classes.jar') + os.chdir(current_dir_path) + + os.remove(f'{framework_temp_path}/jacoco-report-classes.jar') + os.remove(f'{service_temp_path}/jacoco-report-classes.jar') + os.remove(f'{app_temp_path}/jacoco-report-classes.jar') + + # Generate coverage report. + exclude_classes = [] + for glob in framework_exclude_classes: + exclude_classes.extend(list(framework_temp_path.glob(glob))) + for glob in service_exclude_classes: + exclude_classes.extend(list(service_temp_path.glob(glob))) + for glob in app_exclude_classes: + exclude_classes.extend(list(app_temp_path.glob(glob))) + + for c in exclude_classes: + if c.exists(): + os.remove(c.absolute()) + + gen_java_cov_report_cmd = [ + f'java', + f'-jar', + f'{android_host_out}/framework/jacoco-cli.jar', + f'report', + f'{merged_ec_path.absolute()}', + f'--classfiles', + f'{temp_path.absolute()}', + f'--html', + f'{java_coverage_out.absolute()}', + f'--name', + f'{java_coverage_out.absolute()}.html', + ] + subprocess.run(gen_java_cov_report_cmd) + + # Cleanup. + shutil.rmtree(temp_path, ignore_errors=True) + + +def generate_native_coverage(bt_apex_name, trace_path, coverage_out): + + out = os.getenv('OUT') + android_build_top = os.getenv('ANDROID_BUILD_TOP') + + native_coverage_out = Path(f'{coverage_out}/native') + temp_path = Path(f'{coverage_out}/temp') + if temp_path.exists(): + shutil.rmtree(temp_path, ignore_errors=True) + temp_path.mkdir() + + # From google3/configs/wireless/android/testing/atp/prod/mainline-engprod/templates/modules/bluetooth.gcl. + exclude_files = { + 'system/.*_aidl.*', + 'system/.*_test.*', + 'system/.*_mock.*', + 'system/.*_unittest.*', + 'system/binder/', + 'system/blueberry/', + 'system/build/', + 'system/conf/', + 'system/doc/', + 'system/test/', + 'system/gd/l2cap/', + 'system/gd/security/', + 'system/gd/neighbor/', + # 'android/', # Should not be excluded + } + + # Merge profdata files. + profdata_path = Path(f'{temp_path}/coverage.profdata') + subprocess.run( + f'llvm-profdata merge --sparse -o {profdata_path.absolute()} {trace_path.absolute()}/*.profraw', + shell=True) + + gen_native_cov_report_cmd = [ + f'llvm-cov', + f'show', + f'-format=html', + f'-output-dir={native_coverage_out.absolute()}', + f'-instr-profile={profdata_path.absolute()}', + f'{out}/symbols/apex/{bt_apex_name}/lib64/libbluetooth_jni.so', + f'-path-equivalence=/proc/self/cwd,{android_build_top}', + f'/proc/self/cwd/packages/modules/Bluetooth', + ] + for f in exclude_files: + gen_native_cov_report_cmd.append(f'-ignore-filename-regex={f}') + subprocess.run(gen_native_cov_report_cmd, cwd=android_build_top) + + # Cleanup. + shutil.rmtree(temp_path, ignore_errors=True) + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser() + parser.add_argument( + '--apex-name', + default='com.android.btservices', + help='bluetooth apex name. Default: com.android.btservices') + parser.add_argument( + '--java', action='store_true', help='generate Java coverage') + parser.add_argument( + '--native', action='store_true', help='generate native coverage') + parser.add_argument( + '--out', + type=str, + default='out_coverage', + help='out directory for coverage reports. Default: ./out_coverage') + parser.add_argument( + '--trace', + type=str, + default='trace', + help='trace directory with .ec and .profraw files. Default: ./trace') + parser.add_argument( + '--full-report', + action='store_true', + help='run all tests and compute coverage report') + args = parser.parse_args() + + coverage_out = Path(args.out) + shutil.rmtree(coverage_out, ignore_errors=True) + coverage_out.mkdir() + + if not args.full_report: + trace_path = Path(args.trace) + if (not trace_path.exists() or not trace_path.is_dir()): + sys.exit('Trace directory does not exist') + + if (args.java): + generate_java_coverage(args.apex_name, trace_path, coverage_out) + if (args.native): + generate_native_coverage(args.apex_name, trace_path, coverage_out) + + else: + # Compute Pandora coverage. + run_pts_bot() + coverage_out_pandora = Path(f'{coverage_out}/pandora') + coverage_out_pandora.mkdir() + trace_pandora = Path('trace_pandora') + shutil.rmtree(trace_pandora, ignore_errors=True) + subprocess.run(['adb', 'pull', '/data/misc/trace', trace_pandora]) + generate_java_coverage(args.apex_name, trace_pandora, + coverage_out_pandora) + generate_native_coverage(args.apex_name, trace_pandora, + coverage_out_pandora) + + # # Compute all coverage. + run_unit_tests() + coverage_out_mainline = Path(f'{coverage_out}/mainline') + coverage_out_mainline.mkdir() + trace_all = Path('trace_all') + shutil.rmtree(trace_all, ignore_errors=True) + subprocess.run(['adb', 'pull', '/data/misc/trace', trace_all]) + generate_java_coverage(args.apex_name, trace_all, coverage_out_mainline) + generate_native_coverage(args.apex_name, trace_all, + coverage_out_mainline) diff --git a/android/pandora/mmi2grpc/_build/protoc-gen-custom_grpc b/android/pandora/mmi2grpc/_build/protoc-gen-custom_grpc index b560c5ba76..bf09ea5a80 100755 --- a/android/pandora/mmi2grpc/_build/protoc-gen-custom_grpc +++ b/android/pandora/mmi2grpc/_build/protoc-gen-custom_grpc @@ -101,21 +101,92 @@ def generate_service(imports, file, service): f' {methods}\n' ).split('\n') +def generate_servicer_method(method): + input_mode = 'stream' if method.client_streaming else 'unary' + + if input_mode == 'stream': + return ( + f'def {method.name}(self, request_iterator, context):\n' + f' context.set_code(grpc.StatusCode.UNIMPLEMENTED)\n' + f' context.set_details("Method not implemented!")\n' + f' raise NotImplementedError("Method not implemented!")' + ).split('\n') + else: + return ( + f'def {method.name}(self, request, context):\n' + f' context.set_code(grpc.StatusCode.UNIMPLEMENTED)\n' + f' context.set_details("Method not implemented!")\n' + f' raise NotImplementedError("Method not implemented!")' + ).split('\n') + + +def generate_servicer(service): + methods = '\n\n '.join([ + '\n '.join( + generate_servicer_method(method) + ) for method in service.method + ]) + if len(methods) == 0: + methods = 'pass' + return ( + f'class {service.name}Servicer:\n' + f'\n' + f' {methods}\n' + ).split('\n') + +def generate_rpc_method_handler(imports, method): + input_mode = 'stream' if method.client_streaming else 'unary' + output_mode = 'stream' if method.server_streaming else 'unary' + + input_type = import_type(imports, method.input_type) + output_type = import_type(imports, method.output_type) + + return ( + f"'{method.name}': grpc.{input_mode}_{output_mode}_rpc_method_handler(\n" + f' servicer.{method.name},\n' + f' request_deserializer={input_type}.FromString,\n' + f' response_serializer={output_type}.SerializeToString,\n' + f' ),\n' + ).split('\n') + +def generate_add_servicer_to_server_method(imports, file, service): + method_handlers = ' '.join([ + '\n '.join( + generate_rpc_method_handler(imports, method) + ) for method in service.method + ]) + return ( + f'def add_{service.name}Servicer_to_server(servicer, server):\n' + f' rpc_method_handlers = {{\n' + f' {method_handlers}\n' + f' }}\n' + f' generic_handler = grpc.method_handlers_generic_handler(\n' + f" '{file.package}.{service.name}', rpc_method_handlers)\n" + f' server.add_generic_rpc_handlers((generic_handler,))' + ).split('\n') files = [] for file_name in request.file_to_generate: file = next(filter(lambda x: x.name == file_name, request.proto_file)) - imports = set([]) + imports = set(['import grpc']) services = '\n'.join(sum([ generate_service(imports, file, service) for service in file.service ], [])) + servicers = '\n'.join(sum([ + generate_servicer(service) for service in file.service + ], [])) + + add_servicer_methods = '\n'.join(sum([ + generate_add_servicer_to_server_method(imports, file, service) for service in file.service + ], [])) + files.append(CodeGeneratorResponse.File( name=file_name.replace('.proto', '_grpc.py'), - content='\n'.join(imports) + '\n\n' + services + content='\n'.join(imports) + '\n\n' + services + '\n\n' + servicers + '\n\n' + add_servicer_methods + '\n' )) reponse = CodeGeneratorResponse(file=files) diff --git a/android/pandora/mmi2grpc/mmi2grpc/__init__.py b/android/pandora/mmi2grpc/mmi2grpc/__init__.py index a4895909c8..fe6f4844dd 100644 --- a/android/pandora/mmi2grpc/mmi2grpc/__init__.py +++ b/android/pandora/mmi2grpc/mmi2grpc/__init__.py @@ -36,7 +36,8 @@ from mmi2grpc._rootcanal import RootCanal from pandora_experimental.host_grpc import Host -GRPC_PORT = 8999 +PANDORA_SERVER_PORT = 8999 +ROOTCANAL_CONTROL_PORT = 6212 MAX_RETRIES = 10 GRPC_SERVER_INIT_TIMEOUT = 10 # seconds @@ -48,15 +49,15 @@ class IUT: proxy which translates MMI calls to gRPC calls to the IUT. """ - def __init__(self, test: str, args: List[str], port: int = GRPC_PORT, **kwargs): + def __init__(self, test: str, args: List[str], **kwargs): """Init IUT class for a given test. Args: test: PTS test id. args: test arguments. - port: gRPC port exposed by the IUT test server. """ - self.port = port + self.pandora_server_port = int(args[0]) if len(args) > 0 else PANDORA_SERVER_PORT + self.rootcanal_control_port = int(args[1]) if len(args) > 1 else ROOTCANAL_CONTROL_PORT self.test = test self.rootcanal = None @@ -73,12 +74,12 @@ class IUT: def __enter__(self): """Resets the IUT when starting a PTS test.""" - self.rootcanal = RootCanal() + self.rootcanal = RootCanal(port=self.rootcanal_control_port) self.rootcanal.reconnect_phone() # Note: we don't keep a single gRPC channel instance in the IUT class # because reset is allowed to close the gRPC server. - with grpc.insecure_channel(f'localhost:{self.port}') as channel: + with grpc.insecure_channel(f'localhost:{self.pandora_server_port}') as channel: self._retry(Host(channel).HardReset)(wait_for_ready=True) def __exit__(self, exc_type, exc_value, exc_traceback): @@ -121,7 +122,7 @@ class IUT: mut_address = None def read_local_address(): - with grpc.insecure_channel(f'localhost:{self.port}') as channel: + with grpc.insecure_channel(f'localhost:{self.pandora_server_port}') as channel: nonlocal mut_address mut_address = self._retry(Host(channel).ReadLocalAddress)(wait_for_ready=True).address @@ -151,47 +152,47 @@ class IUT: # Handles A2DP and AVDTP MMIs. if profile in ('A2DP', 'AVDTP'): if not self._a2dp: - self._a2dp = A2DPProxy(grpc.insecure_channel(f'localhost:{self.port}')) + self._a2dp = A2DPProxy(grpc.insecure_channel(f'localhost:{self.pandora_server_port}')) return self._a2dp.interact(test, interaction, description, pts_address) # Handles AVRCP and AVCTP MMIs. if profile in ('AVRCP', 'AVCTP'): if not self._avrcp: - self._avrcp = AVRCPProxy(grpc.insecure_channel(f'localhost:{self.port}')) + self._avrcp = AVRCPProxy(grpc.insecure_channel(f'localhost:{self.pandora_server_port}')) return self._avrcp.interact(test, interaction, description, pts_address) # Handles GATT MMIs. if profile in ('GATT'): if not self._gatt: - self._gatt = GATTProxy(grpc.insecure_channel(f'localhost:{self.port}')) + self._gatt = GATTProxy(grpc.insecure_channel(f'localhost:{self.pandora_server_port}')) return self._gatt.interact(test, interaction, description, pts_address) # Handles HFP MMIs. if profile in ('HFP'): if not self._hfp: - self._hfp = HFPProxy(grpc.insecure_channel(f'localhost:{self.port}')) + self._hfp = HFPProxy(grpc.insecure_channel(f'localhost:{self.pandora_server_port}')) return self._hfp.interact(test, interaction, description, pts_address) # Handles HID MMIs. if profile in ('HID'): if not self._hid: - self._hid = HIDProxy(grpc.insecure_channel(f'localhost:{self.port}'), self.rootcanal) + self._hid = HIDProxy(grpc.insecure_channel(f'localhost:{self.pandora_server_port}'), self.rootcanal) return self._hid.interact(test, interaction, description, pts_address) # Handles HOGP MMIs. if profile in ('HOGP'): if not self._hogp: - self._hogp = HOGPProxy(grpc.insecure_channel(f'localhost:{self.port}')) + self._hogp = HOGPProxy(grpc.insecure_channel(f'localhost:{self.pandora_server_port}')) return self._hogp.interact(test, interaction, description, pts_address) # Instantiates L2CAP proxy and reroutes corresponding MMIs to it. if profile in ('L2CAP'): if not self._l2cap: - self._l2cap = L2CAPProxy(grpc.insecure_channel(f'localhost:{self.port}')) + self._l2cap = L2CAPProxy(grpc.insecure_channel(f'localhost:{self.pandora_server_port}')) return self._l2cap.interact(test, interaction, description, pts_address) # Handles SDP MMIs. if profile in ('SDP'): if not self._sdp: - self._sdp = SDPProxy(grpc.insecure_channel(f'localhost:{self.port}')) + self._sdp = SDPProxy(grpc.insecure_channel(f'localhost:{self.pandora_server_port}')) return self._sdp.interact(test, interaction, description, pts_address) # Handles SM MMIs. if profile in ('SM'): if not self._sm: - self._sm = SMProxy(grpc.insecure_channel(f'localhost:{self.port}')) + self._sm = SMProxy(grpc.insecure_channel(f'localhost:{self.pandora_server_port}')) return self._sm.interact(test, interaction, description, pts_address) # Handles unsupported profiles. diff --git a/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py b/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py index 60401d5283..c7e6b9ab2c 100644 --- a/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py +++ b/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py @@ -76,9 +76,8 @@ class TestChannel: class RootCanal: - def __init__(self): - # port is CONTROL_ROOTCANAL_PORT defined in tradefed - self.channel = TestChannel(port=6212) + def __init__(self, port): + self.channel = TestChannel(port) self.disconnected_dev_phys = None # discard initialization messages @@ -128,6 +127,8 @@ class RootCanal: target_phys = [le_phy] elif "hci_device" in name: target_phys = [classic_phy, le_phy] + else: + target_phys = [] for phy in target_phys: if dev_i not in self._parse_phy(devices["Phys"][phy])[1]: diff --git a/android/pandora/mmi2grpc/mmi2grpc/a2dp.py b/android/pandora/mmi2grpc/mmi2grpc/a2dp.py index 8614c20612..a169d8a188 100644 --- a/android/pandora/mmi2grpc/mmi2grpc/a2dp.py +++ b/android/pandora/mmi2grpc/mmi2grpc/a2dp.py @@ -11,7 +11,6 @@ # 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. - """A2DP proxy module.""" import time @@ -49,15 +48,12 @@ class A2DPProxy(ProfileProxy): def convert_frame(data): return PlaybackAudioRequest(data=data, source=self.source) - self.audio = AudioSignal( - lambda frames: self.a2dp.PlaybackAudio(map(convert_frame, frames)), - AUDIO_SIGNAL_AMPLITUDE, - AUDIO_SIGNAL_SAMPLING_RATE - ) + + self.audio = AudioSignal(lambda frames: self.a2dp.PlaybackAudio(map(convert_frame, frames)), + AUDIO_SIGNAL_AMPLITUDE, AUDIO_SIGNAL_SAMPLING_RATE) @assert_description - def TSC_AVDTP_mmi_iut_accept_connect( - self, test: str, pts_addr: bytes, **kwargs): + def TSC_AVDTP_mmi_iut_accept_connect(self, test: str, pts_addr: bytes, **kwargs): """ If necessary, take action to accept the AVDTP Signaling Channel Connection initiated by the tester. @@ -71,28 +67,35 @@ class A2DPProxy(ProfileProxy): """ if "SRC" in test: - self.connection = self.host.WaitConnection( - address=pts_addr).connection + self.connection = self.host.WaitConnection(address=pts_addr).connection try: if "INT" in test: - self.source = self.a2dp.OpenSource( - connection=self.connection).source + self.source = self.a2dp.OpenSource(connection=self.connection).source else: - self.source = self.a2dp.WaitSource( - connection=self.connection).source + self.source = self.a2dp.WaitSource(connection=self.connection).source except RpcError: pass else: - self.connection = self.host.WaitConnection( - address=pts_addr).connection + self.connection = self.host.WaitConnection(address=pts_addr).connection try: - self.sink = self.a2dp.WaitSink( - connection=self.connection).sink + self.sink = self.a2dp.WaitSink(connection=self.connection).sink except RpcError: pass return "OK" @assert_description + def TSC_AVDTP_mmi_iut_accept_disconnect(self, **kwargs): + """ + If necessary, take action to accept the AVDTP Signaling Channnel + Disconnection initiated by the tester. + + Note: If an AVCTP signaling + channel was established it will also be disconnected. + """ + + return "OK" + + @assert_description def TSC_AVDTP_mmi_iut_initiate_discover(self, **kwargs): """ Send a discover command to PTS. @@ -152,8 +155,7 @@ class A2DPProxy(ProfileProxy): return "OK" @assert_description - def TSC_AVDTP_mmi_iut_initiate_out_of_range( - self, pts_addr: bytes, **kwargs): + def TSC_AVDTP_mmi_iut_initiate_out_of_range(self, pts_addr: bytes, **kwargs): """ Move the IUT out of range to create a link loss scenario. @@ -162,8 +164,7 @@ class A2DPProxy(ProfileProxy): """ if self.connection is None: - self.connection = self.host.GetConnection( - address=pts_addr).connection + self.connection = self.host.GetConnection(address=pts_addr).connection self.host.Disconnect(connection=self.connection) self.connection = None self.sink = None @@ -181,11 +182,8 @@ class A2DPProxy(ProfileProxy): if test == "AVDTP/SRC/ACP/SIG/SMG/BI-29-C": time.sleep(2) # TODO: Remove, AVRCP SegFault - if test in ("A2DP/SRC/CC/BV-09-I", - "A2DP/SRC/SET/BV-04-I", - "AVDTP/SRC/ACP/SIG/SMG/BV-18-C", - "AVDTP/SRC/ACP/SIG/SMG/BV-20-C", - "AVDTP/SRC/ACP/SIG/SMG/BV-22-C"): + if test in ("A2DP/SRC/CC/BV-09-I", "A2DP/SRC/SET/BV-04-I", "AVDTP/SRC/ACP/SIG/SMG/BV-18-C", + "AVDTP/SRC/ACP/SIG/SMG/BV-20-C", "AVDTP/SRC/ACP/SIG/SMG/BV-22-C"): time.sleep(1) # TODO: Remove, AVRCP SegFault if test == "A2DP/SRC/SUS/BV-01-I": # Stream is not suspended when we receive the interaction diff --git a/android/pandora/mmi2grpc/mmi2grpc/avrcp.py b/android/pandora/mmi2grpc/mmi2grpc/avrcp.py index 2b89e0905b..b6317b66eb 100644 --- a/android/pandora/mmi2grpc/mmi2grpc/avrcp.py +++ b/android/pandora/mmi2grpc/mmi2grpc/avrcp.py @@ -59,7 +59,7 @@ class AVRCPProxy(ProfileProxy): the IUT connects to PTS to establish pairing. """ - if "CT" in test: + if ("TG" in test and "TG/VLH" not in test) or "CT/VLH" in test: self.connection = self.host.WaitConnection(address=pts_addr).connection try: @@ -149,11 +149,7 @@ class AVRCPProxy(ProfileProxy): Take action to disconnect all A2DP and/or AVRCP connections. """ - if self.connection is None: - self.connection = self.host.GetConnection(address=pts_addr).connection - self.host.Disconnect(connection=self.connection) - self.connection = None - self.sink = None + self.a2dp.Close(source=self.source) self.source = None return "OK" @@ -202,6 +198,7 @@ class AVRCPProxy(ProfileProxy): The IUT should then initiate an L2CAP_ConnectRsp and L2CAP_ConfigRsp. """ + return "OK" @assert_description @@ -241,6 +238,7 @@ class AVRCPProxy(ProfileProxy): Press 'OK' to continue once the IUT has responded. """ + return "OK" @assert_description @@ -258,6 +256,7 @@ class AVRCPProxy(ProfileProxy): Press 'OK' to continue once the IUT has responded. """ + return "OK" @assert_description @@ -275,6 +274,7 @@ class AVRCPProxy(ProfileProxy): the following parameter values: * BD_ADDR = BD_ADDRLower_Tester """ + return "OK" @assert_description @@ -298,6 +298,7 @@ class AVRCPProxy(ProfileProxy): DATA[]Lower_Tester * Length = LengthOf(DATA[]Lower_Tester) """ + return "OK" @assert_description @@ -327,6 +328,7 @@ class AVRCPProxy(ProfileProxy): * Length = LengthOf(DATA[]Lower_Tester) """ + return "OK" @assert_description @@ -356,4 +358,187 @@ class AVRCPProxy(ProfileProxy): continue once the IUT has responded. """ #TODO: Remove trailing space post "values:" from docstring description + + return "OK" + + @assert_description + def TSC_AVDTP_mmi_iut_initiate_connect(self, pts_addr: bytes, **kwargs): + """ + Create an AVDTP signaling channel. + + Action: Create an audio or video + connection with PTS. + """ + self.connection = self.host.Connect(address=pts_addr).connection + self.source = self.a2dp.OpenSource(connection=self.connection).source + return "OK" + + @assert_description + def _mmi_690(self, **kwargs): + """ + Press 'YES' if the IUT indicated receiving the [PLAY] command. Press + 'NO' otherwise. + + Description: Verify that the Implementation Under Test + (IUT) successfully indicated that the current operation was pressed. Not + all commands (fast forward and rewind for example) have a noticeable + effect when pressed for a short period of time. For commands like that + it is acceptable to assume the effect took place and press 'YES'. + """ + + return "Yes" + + @assert_description + def _mmi_691(self, **kwargs): + """ + Press 'YES' if the IUT indicated receiving the [STOP] command. Press + 'NO' otherwise. + + Description: Verify that the Implementation Under Test + (IUT) successfully indicated that the current operation was pressed. Not + all commands (fast forward and rewind for example) have a noticeable + effect when pressed for a short period of time. For commands like that + it is acceptable to assume the effect took place and press 'YES'. + """ + + return "Yes" + + @assert_description + def _mmi_540(self, **kwargs): + """ + Press 'YES' if the IUT supports press and hold functionality for the + [PLAY] command. Press 'NO' otherwise. + + Description: Verify press and + hold functionality of passthrough operations that support press and + hold. Not all operations support press and hold, pressing 'NO' will not + fail the test case. + """ + + return "Yes" + + @assert_description + def _mmi_615(self, **kwargs): + """ + Press 'YES' if the IUT indicated press and hold functionality for the + [PLAY] command. Press 'NO' otherwise. + + Description: Verify that the + Implementation Under Test (IUT) successfully indicated that the current + operation was held. + """ + + return "Yes" + + @assert_description + def _mmi_541(self, **kwargs): + """ + Press 'YES' if the IUT supports press and hold functionality for the + [STOP] command. Press 'NO' otherwise. + + Description: Verify press and + hold functionality of passthrough operations that support press and + hold. Not all operations support press and hold, pressing 'NO' will not + fail the test case. + """ + + return "Yes" + + @assert_description + def _mmi_616(self, **kwargs): + """ + Press 'YES' if the IUT indicated press and hold functionality for the + [STOP] command. Press 'NO' otherwise. + + Description: Verify that the + Implementation Under Test (IUT) successfully indicated that the current + operation was held. + """ + + return "Yes" + + @assert_description + def TSC_AVRCP_mmi_user_confirm_media_is_streaming(self, **kwargs): + """ + Press 'OK' when the IUT is in a state where media is playing. + Description: PTS is preparing the streaming state for the next + passthrough command, if the current streaming state is not relevant to + this IUT, please press 'OK to continue. + """ + if not self.a2dp.IsSuspended(source=self.source).is_suspended: + return "Yes" + else: + return "No" + + @assert_description + def TSC_AVRCP_mmi_iut_reject_invalid_get_capabilities(self, **kwargs): + """ + The IUT should reject the invalid Get Capabilities command sent by PTS. + Description: Verify that the IUT can properly reject a Get Capabilities + command that contains an invalid capability. + """ + + return "OK" + + @assert_description + def TSC_AVRCP_mmi_iut_accept_get_capabilities(self, **kwargs): + """ + Take action to send a valid response to the [Get Capabilities] command + sent by the PTS. + """ + # This will be done as part as the a2dp.OpenSource or a2dp.WaitSource + + return "OK" + + @assert_description + def TSC_AVRCP_mmi_iut_accept_get_element_attributes(self, **kwargs): + """ + Take action to send a valid response to the [Get Element Attributes] + command sent by the PTS. + """ + # This will be done as part as the a2dp.OpenSource or a2dp.WaitSource + + return "OK" + + @assert_description + def TSC_AVRCP_mmi_iut_reject_invalid_command_control_channel(self, **kwargs): + """ + PTS has sent an invalid command over the control channel. The IUT must + respond with a general reject on the control channel. + + Description: + Verify that the IUT can properly reject an invalid command sent over the + control channel. + """ + + return "OK" + + @assert_description + def TSC_AVRCP_mmi_iut_reject_invalid_command_browsing_channel(self, **kwargs): + """ + PTS has sent an invalid command over the browsing channel. The IUT must + respond with a general reject on the browsing channel. + + Description: + Verify that the IUT can properly reject an invalid command sent over the + browsing channel. + """ + + return "OK" + + @assert_description + def TSC_AVCTP_mmi_register_ConnectCfm_CB(self, **kwargs): + """ + Using the Upper Tester send an AVCT_EventRegistration command from the + AVCTP Upper Interface to the IUT with the following input parameter + values: + * Event = AVCT_Connect_Cfm + * Callback = + ConnectCfm_CBTest_System + * PID = PIDTest_System + + Press 'OK' to + continue once the IUT has responded. + """ + return "OK"
\ No newline at end of file diff --git a/android/pandora/mmi2grpc/mmi2grpc/l2cap.py b/android/pandora/mmi2grpc/mmi2grpc/l2cap.py index c3f54dce58..730f560f4c 100644 --- a/android/pandora/mmi2grpc/mmi2grpc/l2cap.py +++ b/android/pandora/mmi2grpc/mmi2grpc/l2cap.py @@ -1,13 +1,15 @@ import time +import sys from mmi2grpc._helpers import assert_description from mmi2grpc._helpers import match_description from mmi2grpc._proxy import ProfileProxy + from pandora_experimental.host_grpc import Host -from pandora_experimental.host_pb2 import Connection +from pandora_experimental.host_pb2 import Connection, ConnectabilityMode, AddressType from pandora_experimental.l2cap_grpc import L2CAP + from typing import Optional -import sys class L2CAPProxy(ProfileProxy): @@ -88,7 +90,10 @@ class L2CAPProxy(ProfileProxy): """ Place the IUT into LE connectable mode. """ - self.host.SetLEConnectable() + self.host.StartAdvertising( + connectability_mode=ConnectabilityMode.CONNECTABILITY_CONNECTABLE, + own_address_type=AddressType.PUBLIC, + ) # not strictly necessary, but can save time on waiting connection tests_to_open_bluetooth_server_socket = [ "L2CAP/LE/CFC/BV-03-C", @@ -97,6 +102,7 @@ class L2CAPProxy(ProfileProxy): "L2CAP/LE/CFC/BV-09-C", "L2CAP/LE/CFC/BV-13-C", "L2CAP/LE/CFC/BV-20-C", + "L2CAP/LE/CFC/BI-01-C", ] tests_require_secure_connection = [ "L2CAP/LE/CFC/BV-13-C", @@ -335,3 +341,21 @@ class L2CAPProxy(ProfileProxy): print('error in MMI_UPPER_TESTER_CONFIRM_RECEIVE_REJECT_RESOURCES', file=sys.stderr) raise Exception("Unexpected RECEIVE_COMMAND") return "OK" + + def MMI_IUT_ENABLE_LE_CONNECTION(self, pts_addr: bytes, **kwargs): + """ + Initiate or create LE ACL connection to the PTS. + """ + self.connection = self.host.ConnectLE(address=pts_addr).connection + return "OK" + + @assert_description + def MMI_IUT_SEND_ACL_DISCONNECTION(self, **kwargs): + """ + Initiate an ACL disconnection from the IUT to the PTS. + Description : + The Implementation Under Test(IUT) should disconnect ACL channel by + sending a disconnect request to PTS. + """ + self.host.DisconnectLE(connection=self.connection) + return "OK" diff --git a/android/pandora/mmi2grpc/mmi2grpc/sm.py b/android/pandora/mmi2grpc/mmi2grpc/sm.py index 697d2d36a4..42174cbde5 100644 --- a/android/pandora/mmi2grpc/mmi2grpc/sm.py +++ b/android/pandora/mmi2grpc/mmi2grpc/sm.py @@ -12,45 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. """SMP proxy module.""" +from queue import Empty, Queue +from threading import Thread import sys +import time -from mmi2grpc._helpers import assert_description +from mmi2grpc._helpers import assert_description, match_description from mmi2grpc._proxy import ProfileProxy from mmi2grpc._streaming import StreamWrapper from pandora_experimental.security_grpc import Security from pandora_experimental.host_grpc import Host - -# The tests needs the MMI to accept pairing confirmation request. -NEEDS_PAIRING_CONFIRMATION = { - "SM/CEN/EKS/BV-01-C", - "SM/CEN/JW/BI-04-C", - "SM/CEN/JW/BI-01-C", - "SM/CEN/KDU/BV-04-C", - "SM/CEN/KDU/BV-05-C", - "SM/CEN/KDU/BV-06-C", - "SM/CEN/KDU/BV-10-C", - "SM/CEN/KDU/BV-11-C", -} - -ACCEPTS_REMOTE_PAIRING_CONFIRMATION = { - "SM/CEN/KDU/BI-01-C", - "SM/CEN/KDU/BI-02-C", - "SM/CEN/KDU/BI-03-C", -} +from pandora_experimental.host_pb2 import ConnectabilityMode, AddressType def debug(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) -def get_event(pairing_stream: StreamWrapper, addr: str): - for event in pairing_stream: - if event.address == addr: - return event - return None - - class SMProxy(ProfileProxy): def __init__(self, channel): @@ -59,33 +38,25 @@ class SMProxy(ProfileProxy): self.host = Host(channel) self.connection = None self.pairing_stream = None + self.passkey_queue = Queue() + self._handle_pairing_requests() @assert_description - def MMI_IUT_ENABLE_CONNECTION_SM(self, test, pts_addr: bytes, **kwargs): + def MMI_IUT_ENABLE_CONNECTION_SM(self, pts_addr: bytes, **kwargs): """ Initiate an connection from the IUT to the PTS. """ self.connection = self.host.ConnectLE(address=pts_addr).connection - self.pairing_stream = self.security.OnPairing() - - if self.connection and test in ACCEPTS_REMOTE_PAIRING_CONFIRMATION: - event = get_event(pairing_stream=self.pairing_stream, addr=pts_addr) - self.pairing_stream.send(event=event, confirm=True) - self.pairing_stream.close() return "OK" @assert_description - def MMI_ASK_IUT_PERFORM_PAIRING_PROCESS(self, test, pts_addr: bytes, **kwargs): + def MMI_ASK_IUT_PERFORM_PAIRING_PROCESS(self, **kwargs): """ Please start pairing process. """ if self.connection: self.security.Pair(connection=self.connection) - if test in NEEDS_PAIRING_CONFIRMATION: - event = get_event(pairing_stream=self.pairing_stream, addr=pts_addr) - self.pairing_stream.send(event=event, confirm=True) - self.pairing_stream.close() - return "OK" + return "OK" @assert_description def MMI_IUT_SEND_DISCONNECTION_REQUEST(self, **kwargs): @@ -104,7 +75,6 @@ class SMProxy(ProfileProxy): """ Please confirm the following number matches IUT: 385874. """ - return "OK" @assert_description @@ -114,3 +84,97 @@ class SMProxy(ProfileProxy): """ self.host.SoftReset() return "OK" + + @assert_description + def MMI_TESTER_ENABLE_CONNECTION_SM(self, **kwargs): + """ + Action: Place the IUT in connectable mode + """ + self.host.StartAdvertising( + connectability_mode=ConnectabilityMode.CONNECTABILITY_CONNECTABLE, + own_address_type=AddressType.PUBLIC, + ) + return "OK" + + @assert_description + def MMI_IUT_SMP_TIMEOUT_30_SECONDS(self, **kwargs): + """ + Wait for the 30 seconds. Lower tester will not send corresponding or + next SMP message. + """ + return "OK" + + @assert_description + def MMI_IUT_SMP_TIMEOUT_ADDITIONAL_10_SECONDS(self, **kwargs): + """ + Wait for an additional 10 seconds. Lower test will send corresponding or + next SMP message. + """ + return "OK" + + @match_description + def MMI_DISPLAY_PASSKEY_CODE(self, passkey: str, **kwargs): + """ + Please enter (?P<passkey>[0-9]*) in the IUT. + """ + self.passkey_queue.put(passkey) + return "OK" + + @assert_description + def MMI_ENTER_PASSKEY_CODE(self, **kwargs): + """ + Please enter 6 digit passkey code. + """ + + return "OK" + + @assert_description + def MMI_ENTER_WRONG_DYNAMIC_PASSKEY_CODE(self, **kwargs): + """ + Please enter invalid 6 digit pin code. + """ + + return "OK" + + @assert_description + def MMI_IUT_ABORT_PAIRING_PROCESS_DISCONNECT(self, **kwargs): + """ + Lower tester expects IUT aborts pairing process, and disconnect. + """ + + return "OK" + + @assert_description + def MMI_IUT_ACCEPT_CONNECTION_BR_EDR(self, **kwargs): + """ + Please prepare IUT into a connectable mode in BR/EDR. + + Description: + Verify that the Implementation Under Test (IUT) can accept a connect + request from PTS. + """ + + return "OK" + + @assert_description + def _mmi_2001(self, **kwargs): + """ + Please verify the passKey is correct: 000000 + """ + return "OK" + + def _handle_pairing_requests(self): + + def task(): + pairing_events = self.security.OnPairing() + for event in pairing_events: + if event.just_works or event.numeric_comparison: + pairing_events.send(event=event, confirm=True) + if event.passkey_entry_request: + try: + passkey = self.passkey_queue.get(timeout=15) + pairing_events.send(event=event, passkey=int(passkey)) + except Empty: + debug("No passkey provided within 15 seconds") + + Thread(target=task).start() diff --git a/android/pandora/server/Android.bp b/android/pandora/server/Android.bp index 362fa453db..69a2b21b66 100644 --- a/android/pandora/server/Android.bp +++ b/android/pandora/server/Android.bp @@ -44,6 +44,7 @@ android_test_helper_app { }, test_suites: [ + "general-tests", "device-tests", "mts-bluetooth", ], diff --git a/android/pandora/server/configs/PtsBotTest.xml b/android/pandora/server/configs/PtsBotTest.xml index 43f1c9e7a5..42f3ec965a 100644 --- a/android/pandora/server/configs/PtsBotTest.xml +++ b/android/pandora/server/configs/PtsBotTest.xml @@ -20,10 +20,14 @@ </target_preparer> <test class="com.android.tradefed.testtype.pandora.PtsBotTest" > + <!-- Creates a randomized temp dir for pts-bot binaries and avoid + conflicts when running multiple pts-bot on the same machine --> + <option name="create-bin-temp-dir" value="true"/> <!-- mmi2grpc is contained inside pts-bot folder --> <option name="mmi2grpc" value="pts-bot" /> <option name="tests-config-file" value="pts_bot_tests_config.json" /> - <option name="inconclusive-max-retries" value="3" /> + <option name="max-flaky-tests" value="2" /> + <option name="max-retries-per-test" value="3" /> <option name="physical" value="false" /> <option name="profile" value="A2DP/SRC" /> <option name="profile" value="A2DP/SNK" /> @@ -42,10 +46,23 @@ <option name="profile" value="HFP/AG/TCA" /> <option name="profile" value="HID/HOS" /> <option name="profile" value="HOGP/RH" /> - <option name="profile" value="L2CAP/LE/CFC" /> + <option name="profile" value="L2CAP/LE" /> <option name="profile" value="SDP/SR" /> <option name="profile" value="SM/CEN/EKS" /> <option name="profile" value="SM/CEN/JW" /> <option name="profile" value="SM/CEN/KDU" /> + <option name="profile" value="SM/CEN/PROT" /> + <option name="profile" value="SM/CEN/PKE" /> + <option name="profile" value="SM/CEN/SCJW" /> + <option name="profile" value="SM/CEN/SCPK" /> + <option name="profile" value="SM/PER/PROT" /> + <option name="profile" value="SM/PER/JW" /> + <option name="profile" value="SM/PER/PKE" /> + <option name="profile" value="SM/PER/EKS" /> + <option name="profile" value="SM/PER/KDU" /> + <option name="profile" value="SM/PER/SCJW" /> + <option name="profile" value="SM/PER/SCPK" /> + <option name="profile" value="SM/PER/SCCT" /> + <option name="profile" value="HOGP/RH" /> </test> </configuration> diff --git a/android/pandora/server/configs/PtsBotTestMts.xml b/android/pandora/server/configs/PtsBotTestMts.xml index f7212158dd..05a9f06ac5 100644 --- a/android/pandora/server/configs/PtsBotTestMts.xml +++ b/android/pandora/server/configs/PtsBotTestMts.xml @@ -20,9 +20,14 @@ </target_preparer> <test class="com.android.tradefed.testtype.pandora.PtsBotTest" > + <!-- Creates a randomized temp dir for pts-bot binaries and avoid + conflicts when running multiple pts-bot on the same machine --> + <option name="create-bin-temp-dir" value="true"/> <!-- mmi2grpc is contained inside testcases folder --> <option name="mmi2grpc" value="testcases" /> <option name="tests-config-file" value="pts_bot_tests_config.json" /> + <option name="max-flaky-tests" value="3" /> + <option name="max-retries-per-test" value="3" /> <option name="physical" value="false" /> <option name="profile" value="A2DP/SRC" /> <option name="profile" value="A2DP/SNK" /> @@ -39,7 +44,7 @@ <option name="profile" value="HFP/AG/PSI" /> <option name="profile" value="HFP/AG/SLC" /> <option name="profile" value="HFP/AG/TCA" /> - <option name="profile" value="L2CAP/LE/CFC" /> + <option name="profile" value="L2CAP/LE" /> <option name="profile" value="SDP/SR" /> <option name="profile" value="SM/CEN/EKS" /> <option name="profile" value="SM/CEN/JW" /> diff --git a/android/pandora/server/configs/pts_bot_tests_config.json b/android/pandora/server/configs/pts_bot_tests_config.json index d422be86c5..8f4b8655b1 100644 --- a/android/pandora/server/configs/pts_bot_tests_config.json +++ b/android/pandora/server/configs/pts_bot_tests_config.json @@ -8,6 +8,7 @@ "A2DP/SRC/SET/BV-02-I", "A2DP/SRC/SET/BV-04-I", "A2DP/SNK/AS/BV-01-I", + "A2DP/SNK/AS/BV-02-I", "A2DP/SNK/CC/BV-01-I", "A2DP/SNK/CC/BV-02-I", "A2DP/SNK/CC/BV-05-I", @@ -15,6 +16,7 @@ "A2DP/SNK/CC/BV-07-I", "A2DP/SNK/CC/BV-08-I", "A2DP/SNK/REL/BV-01-I", + "A2DP/SNK/REL/BV-02-I", "A2DP/SNK/SET/BV-01-I", "A2DP/SNK/SET/BV-02-I", "A2DP/SNK/SET/BV-03-I", @@ -91,13 +93,25 @@ "AVDTP/SNK/INT/SIG/SMG/BI-3", "AVRCP/TG/CEC/BV-01-I", "AVRCP/TG/CRC/BV-01-I", + "AVRCP/TG/CRC/BV-02-I", "AVRCP/TG/ICC/BV-01-I", "AVRCP/TG/ICC/BV-02-I", + "AVRCP/TG/INV/BI-01-C", + "AVRCP/TG/INV/BI-02-C", + "AVRCP/TG/MDI/BV-02-C", + "AVRCP/TG/MDI/BV-04-C", + "AVRCP/TG/MDI/BV-05-C", + "AVRCP/TG/MPS/BV-02-C", "AVRCP/TG/MPS/BV-01-I", + "AVRCP/TG/MPS/BV-02-I", + "AVRCP/TG/MPS/BV-03-I", + "AVRCP/TG/PTT/BV-01-I", + "AVRCP/TG/PTT/BV-05-I", "AVRCP/CT/CEC/BV-02-I", "AVRCP/CT/CRC/BV-02-I", - "AVRCP/TG/MDI/BV-02-C", - "AVRCP/TG/MPS/BV-02-C", + "AVRCP/TG/CON/BV-04-C", + "AVRCP/TG/CFG/BV-02-C", + "AVRCP/TG/CFG/BI-01-C", "GATT/CL/GAC/BV-01-C", "GATT/CL/GAD/BV-01-C", "GATT/CL/GAD/BV-02-C", @@ -135,7 +149,6 @@ "HID/HOS/HCE/BV-01-I", "HID/HOS/HCE/BV-03-I", "HID/HOS/HCE/BV-04-I", - "HID/HOS/HCR/BV-01-I", "HID/HOS/HCR/BV-02-I", "HID/HOS/HDT/BV-01-I", "HID/HOS/HDT/BV-02-I", @@ -165,6 +178,7 @@ "HOGP/RH/HGRF/BV-05-I", "HOGP/RH/HGRF/BV-10-I", "HOGP/RH/HGRF/BV-12-I", + "L2CAP/LE/CFC/BI-01-C", "L2CAP/LE/CFC/BV-01-C", "L2CAP/LE/CFC/BV-02-C", "L2CAP/LE/CFC/BV-03-C", @@ -180,6 +194,10 @@ "L2CAP/LE/CFC/BV-19-C", "L2CAP/LE/CFC/BV-20-C", "L2CAP/LE/CFC/BV-21-C", + "L2CAP/LE/CPU/BV-02-C", + "L2CAP/LE/CPU/BI-01-C", + "L2CAP/LE/CPU/BI-02-C", + "L2CAP/LE/REJ/BI-01-C", "SDP/SR/BRW/BV-02-C", "SDP/SR/SA/BI-01-C", "SDP/SR/SA/BI-02-C", @@ -225,7 +243,34 @@ "SM/CEN/KDU/BV-06-C", "SM/CEN/KDU/BV-10-C", "SM/CEN/KDU/BI-03-C", - "SM/CEN/KDU/BV-11-C" + "SM/CEN/KDU/BV-11-C", + "SM/CEN/PROT/BV-01-C", + "SM/CEN/PKE/BV-04-C", + "SM/CEN/PKE/BI-01-C", + "SM/CEN/PKE/BI-02-C", + "SM/CEN/SCJW/BV-04-C", + "SM/CEN/SCJW/BI-01-C", + "SM/CEN/SCPK/BI-01-C", + "SM/PER/PROT/BV-02-C", + "SM/PER/JW/BV-02-C", + "SM/PER/JW/BI-03-C", + "SM/PER/JW/BI-02-C", + "SM/PER/PKE/BV-02-C", + "SM/PER/PKE/BV-05-C", + "SM/PER/PKE/BI-03-C", + "SM/PER/EKS/BV-02-C", + "SM/PER/EKS/BI-02-C", + "SM/PER/KDU/BV-01-C", + "SM/PER/KDU/BV-02-C", + "SM/PER/KDU/BV-03-C", + "SM/PER/KDU/BV-07-C", + "SM/PER/KDU/BV-08-C", + "SM/PER/KDU/BV-09-C", + "SM/PER/KDU/BI-01-C", + "SM/PER/SCJW/BV-03-C", + "SM/PER/SCJW/BI-02-C", + "SM/PER/SCPK/BV-02-C", + "SM/PER/SCPK/BI-03-C" ], "skip": [ "A2DP/SRC/AS/BV-01-I", @@ -238,8 +283,6 @@ "A2DP/SRC/SUS/BV-01-I", "A2DP/SRC/SUS/BV-02-I", "A2DP/SRC/SYN/BV-02-I", - "A2DP/SNK/AS/BV-02-I", - "A2DP/SNK/REL/BV-02-I", "A2DP/SNK/SDP/BV-02-I", "A2DP/SNK/SET/BV-04-I", "A2DP/SNK/SET/BV-05-I", @@ -279,10 +322,6 @@ "AVDTP/SNK/INT/SIG/SMG/ESR05/BV-13-C", "AVDTP/SNK/INT/SIG/SYN/BV-02-C", "AVDTP/SNK/INT/SIG/SYN/BV-04-C", - "AVRCP/TG/PTT/BV-01-I", - "AVRCP/TG/PTT/BV-05-I", - "AVRCP/TG/MPS/BV-02-I", - "AVRCP/TG/MPS/BV-03-I", "AVRCP/TG/MCN/CB/BV-01-I", "AVRCP/TG/MCN/CB/BV-02-I", "AVRCP/TG/MCN/CB/BV-03-I", @@ -292,18 +331,11 @@ "AVRCP/TG/MCN/NP/BV-04-I", "AVRCP/TG/MCN/NP/BV-05-I", "AVRCP/TG/MCN/NP/BV-06-I", - "AVRCP/TG/CFG/BV-02-C", - "AVRCP/TG/CFG/BI-01-C", - "AVRCP/TG/MDI/BV-04-C", - "AVRCP/TG/MDI/BV-05-C", - "AVRCP/TG/CON/BV-04-C", "AVRCP/TG/NFY/BV-02-C", "AVRCP/TG/NFY/BV-04-C", "AVRCP/TG/NFY/BV-06-C", "AVRCP/TG/NFY/BV-07-C", "AVRCP/TG/NFY/BI-01-C", - "AVRCP/TG/INV/BI-01-C", - "AVRCP/TG/INV/BI-02-C", "AVRCP/TG/MPS/BI-01-C", "AVRCP/TG/MPS/BV-04-C", "AVRCP/TG/MPS/BI-02-C", @@ -329,7 +361,6 @@ "AVRCP/CT/PTT/BV-01-I", "AVRCP/CT/PTH/BV-01-C", "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", @@ -366,11 +397,27 @@ "HFP/AG/TCA/BV-03-I", "HFP/AG/TCA/BV-04-I", "HFP/AG/TCA/BV-05-I", - "L2CAP/LE/CFC/BI-01-C", + "HID/HOS/HCR/BV-01-I", "L2CAP/LE/CFC/BV-07-C", "L2CAP/LE/CFC/BV-11-C", "L2CAP/LE/CFC/BV-13-C", - "L2CAP/LE/CFC/BV-15-C" + "L2CAP/LE/CFC/BV-15-C", + "L2CAP/LE/CID/BV-01-C", + "L2CAP/LE/CID/BV-02-C", + "L2CAP/LE/CPU/BV-01-C", + "L2CAP/LE/REJ/BI-02-C", + "SM/CEN/PKE/BV-01-C", + "SM/CEN/SCJW/BV-01-C", + "SM/CEN/SCPK/BI-02-C", + "SM/CEN/SCPK/BV-01-C", + "SM/CEN/SCPK/BV-04-C", + "SM/PER/SCJW/BV-02-C", + "SM/PER/SCPK/BI-04-C", + "SM/PER/SCPK/BV-03-C", + "SM/PER/SCCT/BV-04-C", + "SM/PER/SCCT/BV-06-C", + "SM/PER/SCCT/BV-08-C", + "SM/PER/SCCT/BV-10-C" ], "ics": { "TSPC_4.0HCI_1a_2": true, @@ -1399,4 +1446,4 @@ "SPP": {}, "SUM ICS": {} } -}
\ No newline at end of file +} diff --git a/android/pandora/server/proto/pandora_experimental/gatt.proto b/android/pandora/server/proto/pandora_experimental/gatt.proto index 455005273e..4004513414 100644 --- a/android/pandora/server/proto/pandora_experimental/gatt.proto +++ b/android/pandora/server/proto/pandora_experimental/gatt.proto @@ -34,6 +34,9 @@ service GATT { // Reads characteristic with given descriptor handle. rpc ReadCharacteristicDescriptorFromHandle(ReadCharacteristicDescriptorRequest) returns (ReadCharacteristicDescriptorResponse); + + // Register a GATT service + rpc RegisterService(RegisterServiceRequest) returns (RegisterServiceResponse); } enum AttStatusCode { @@ -172,3 +175,22 @@ message ReadCharacteristicDescriptorResponse { AttValue value = 1; AttStatusCode status = 2; } + +message GattServiceParams { + string uuid = 1; + repeated GattCharacteristicParams characteristics = 2; +} + +message GattCharacteristicParams { + uint32 properties = 1; + uint32 permissions = 2; + string uuid = 3; +} + +message RegisterServiceRequest { + GattServiceParams service = 1; +} + +message RegisterServiceResponse { + GattService service = 1; +} diff --git a/android/pandora/server/proto/pandora_experimental/host.proto b/android/pandora/server/proto/pandora_experimental/host.proto index 346b56293c..44b46106e6 100644 --- a/android/pandora/server/proto/pandora_experimental/host.proto +++ b/android/pandora/server/proto/pandora_experimental/host.proto @@ -41,8 +41,23 @@ service Host { rpc GetLEConnection(GetLEConnectionRequest) returns (GetLEConnectionResponse); // Disconnect ongoing LE connection. rpc DisconnectLE(DisconnectLERequest) returns (google.protobuf.Empty); - // Start LE advertisement - rpc SetLEConnectable(google.protobuf.Empty) returns (google.protobuf.Empty); + // Create and enable an advertising set using legacy or extended advertising, + // except periodic advertising. + rpc StartAdvertising(StartAdvertisingRequest) returns (StartAdvertisingResponse); + // Create and enable a periodic advertising set. + rpc StartPeriodicAdvertising(StartPeriodicAdvertisingRequest) returns (StartPeriodicAdvertisingResponse); + // Remove an advertising set. + rpc StopAdvertising(StopAdvertisingRequest) returns (StopAdvertisingResponse); + // Run BR/EDR inquiry and returns each device found + rpc RunInquiry(RunInquiryRequest) returns (stream RunInquiryResponse); + // Run LE discovery (scanning) and return each device found + rpc RunDiscovery(RunDiscoveryRequest) returns (stream RunDiscoveryResponse); + // Set BREDR connectability mode + rpc SetConnectabilityMode(SetConnectabilityModeRequest) returns (SetConnectabilityModeResponse); + // Set BREDR discoverable mode + rpc SetDiscoverabilityMode(SetDiscoverabilityModeRequest) returns (SetDiscoverabilityModeResponse); + // Get device name from connection + rpc GetDeviceName(GetDeviceNameRequest) returns (GetDeviceNameResponse); } // Response of the `ReadLocalAddress` method. @@ -54,15 +69,33 @@ message ReadLocalAddressResponse { // A Token representing an ACL connection. // It's acquired via a Connect on the Host service. message Connection { - // Opaque value filled by the gRPC server, must not - // be modified nor crafted. + // Opaque value filled by the gRPC server, must not be modified nor crafted + // Android specific: it's secretly an encoded InternelConnectionRef created using newConnection bytes cookie = 1; } +// Internal representation of a Connection - not exposed to clients, included here +// just for code-generation convenience +message InternalConnectionRef { + bytes address = 1; + Transport transport = 2; +} + +// WARNING: Leaving this enum empty will default to BREDR, so make sure that this is a +// valid default whenever used, and that we always populate this value. +enum Transport { + TRANSPORT_BREDR = 0; + TRANSPORT_LE = 1; +} + // Request of the `Connect` method. message ConnectRequest { // Peer Bluetooth Device Address as array of 6 bytes. bytes address = 1; + // Whether we want to initiate pairing as part of the connection + bool skip_pairing = 2; + // Whether confirmation prompts should be auto-accepted or handled manually + bool manually_confirm = 3; } // Response of the `Connect` method. @@ -141,3 +174,126 @@ message GetLEConnectionResponse { message DisconnectLERequest { Connection connection = 1; } + +message AdvertisingHandle { + bytes cookie = 1; +} + +enum AddressType { + PUBLIC = 0x00; + RANDOM = 0x01; +} + +// Advertising Data including one or multiple AD types. +// Since the Flags AD type is mandatory, it must be automatically set by the +// IUT. +// include_<AD type> fields are used for AD type which are generally not +// exposed and that must be set by the IUT when specified. +// See Core Supplement, Part A, Data Types for details +message AdvertisingData { + repeated string service_uuids = 1; + bool include_local_name = 2; + bytes manufacturer_specific_data = 3; + bool include_tx_power_level = 4; + bool include_peripheral_connection_interval_range = 5; + repeated string service_solicitation = 6; + map<string, bytes> service_data = 7; + // Must be on 16 bits. + uint32 appearance = 8; + repeated bytes public_target_addresses = 9; + repeated bytes random_target_addresses = 10; + bool include_advertising_interval = 11; + bool include_le_address = 12; + bool include_le_role = 13; + string uri = 14; +} + +message StartAdvertisingRequest { + bool legacy = 1; + DiscoverabilityMode discovery_mode = 2; + ConnectabilityMode connectability_mode = 3; + AddressType own_address_type = 4; + // If none, undirected. + bytes peer_address = 5; + AdvertisingData advertising_data = 6; + // If none, not scannable. + AdvertisingData scan_response_data = 7; +} + +message StartAdvertisingResponse { + AdvertisingHandle handle = 1; +} + +message StartPeriodicAdvertisingRequest { + AddressType own_address_type = 1; + // If none, undirected. + bytes peer_address = 2; + uint32 interval_min = 3; + uint32 interval_max = 4; + AdvertisingData advertising_data = 5; +} + +message StartPeriodicAdvertisingResponse { + AdvertisingHandle handle = 1; +} + +message StopAdvertisingRequest { + AdvertisingHandle handle = 1; +} + +message StopAdvertisingResponse {} + +message RunInquiryRequest { +} + +message RunInquiryResponse { + repeated Device device = 1; +} + +message RunDiscoveryRequest { +} + +message RunDiscoveryResponse { + Device device = 1; + uint32 flags = 2; +} + +message Device { + string name = 1; + bytes address = 2; +} + +// 5.3 Vol 3C 4.1 Discoverability Modes +enum DiscoverabilityMode { + DISCOVERABILITY_UNSPECIFIED = 0; + DISCOVERABILITY_NONE = 1; + DISCOVERABILITY_LIMITED = 2; + DISCOVERABILITY_GENERAL = 3; +} + +// 5.3 Vol 3C 4.2 Connectability Modes +enum ConnectabilityMode { + CONNECTABILITY_UNSPECIFIED = 0; + CONNECTABILITY_NOT_CONNECTABLE = 1; + CONNECTABILITY_CONNECTABLE = 2; +} + +message SetConnectabilityModeRequest { + ConnectabilityMode connectability = 1; +} + +message SetConnectabilityModeResponse {} + +message SetDiscoverabilityModeRequest { + DiscoverabilityMode discoverability = 1; +} + +message SetDiscoverabilityModeResponse {} + +message GetDeviceNameRequest { + Connection connection = 1; +} + +message GetDeviceNameResponse { + string name = 1; +} diff --git a/android/pandora/server/src/com/android/pandora/A2dpSink.kt b/android/pandora/server/src/com/android/pandora/A2dpSink.kt new file mode 100644 index 0000000000..d1a0be36f8 --- /dev/null +++ b/android/pandora/server/src/com/android/pandora/A2dpSink.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 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.pandora + +import android.bluetooth.BluetoothA2dpSink +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothProfile +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.media.* +import android.util.Log +import io.grpc.Status +import io.grpc.stub.StreamObserver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn +import pandora.A2DPGrpc.A2DPImplBase +import pandora.A2dpProto.* + +@kotlinx.coroutines.ExperimentalCoroutinesApi +class A2dpSink(val context: Context) : A2DPImplBase() { + private val TAG = "PandoraA2dpSink" + + private val scope: CoroutineScope + private val flow: Flow<Intent> + + private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! + private val bluetoothAdapter = bluetoothManager.adapter + private val bluetoothA2dpSink = + getProfileProxy<BluetoothA2dpSink>(context, BluetoothProfile.A2DP_SINK) + + init { + scope = CoroutineScope(Dispatchers.Default) + val intentFilter = IntentFilter() + intentFilter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED) + + flow = intentFlow(context, intentFilter).shareIn(scope, SharingStarted.Eagerly) + } + + fun deinit() { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP_SINK, bluetoothA2dpSink) + scope.cancel() + } + + override fun waitSink( + request: WaitSinkRequest, + responseObserver: StreamObserver<WaitSinkResponse> + ) { + grpcUnary<WaitSinkResponse>(scope, responseObserver) { + val device = request.connection.toBluetoothDevice(bluetoothAdapter) + Log.i(TAG, "waitSink: device=$device") + + if (device.getBondState() != BluetoothDevice.BOND_BONDED) { + Log.e(TAG, "Device is not bonded, cannot wait for stream") + throw Status.UNKNOWN.asException() + } + + if (bluetoothA2dpSink.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { + val state = + flow + .filter { it.getAction() == BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED } + .filter { it.getBluetoothDeviceExtra() == device } + .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) } + .filter { + it == BluetoothProfile.STATE_CONNECTED || it == BluetoothProfile.STATE_DISCONNECTED + } + .first() + + if (state == BluetoothProfile.STATE_DISCONNECTED) { + Log.e(TAG, "waitStream failed, A2DP has been disconnected") + throw Status.UNKNOWN.asException() + } + } + + val sink = Sink.newBuilder().setConnection(request.connection).build() + WaitSinkResponse.newBuilder().setSink(sink).build() + } + } + + override fun close(request: CloseRequest, responseObserver: StreamObserver<CloseResponse>) { + grpcUnary<CloseResponse>(scope, responseObserver) { + val device = + if (request.hasSink()) { + request.sink.connection.toBluetoothDevice(bluetoothAdapter) + } else { + Log.e(TAG, "Sink device required") + throw Status.UNKNOWN.asException() + } + Log.i(TAG, "close: device=$device") + if (bluetoothA2dpSink.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { + Log.e(TAG, "Device is not connected, cannot close") + throw Status.UNKNOWN.asException() + } + + val a2dpConnectionStateChangedFlow = + flow + .filter { it.getAction() == BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED } + .filter { it.getBluetoothDeviceExtra() == device } + .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) } + bluetoothA2dpSink.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) + a2dpConnectionStateChangedFlow.filter { it == BluetoothProfile.STATE_DISCONNECTED }.first() + CloseResponse.getDefaultInstance() + } + } +} diff --git a/android/pandora/server/src/com/android/pandora/Gatt.kt b/android/pandora/server/src/com/android/pandora/Gatt.kt index 11ab70de19..122c634563 100644 --- a/android/pandora/server/src/com/android/pandora/Gatt.kt +++ b/android/pandora/server/src/com/android/pandora/Gatt.kt @@ -20,18 +20,20 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattService +import android.bluetooth.BluetoothGattService.SERVICE_TYPE_PRIMARY import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log -import com.google.protobuf.Empty import io.grpc.Status import io.grpc.stub.StreamObserver import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -45,15 +47,15 @@ import pandora.GattProto.* class Gatt(private val context: Context) : GATTImplBase() { private val TAG = "PandoraGatt" - private val mScope: CoroutineScope - private val flow: Flow<Intent> + private val mScope: CoroutineScope = CoroutineScope(Dispatchers.Default) private val mBluetoothManager = context.getSystemService(BluetoothManager::class.java)!! private val mBluetoothAdapter = mBluetoothManager.adapter - init { - mScope = CoroutineScope(Dispatchers.Default) + private val serverManager by lazy { GattServerManager(mBluetoothManager, context, mScope) } + private val flow: Flow<Intent> + init { val intentFilter = IntentFilter() intentFilter.addAction(BluetoothDevice.ACTION_UUID) @@ -61,14 +63,18 @@ class Gatt(private val context: Context) : GATTImplBase() { } fun deinit() { + serverManager.server.close() mScope.cancel() } - override fun exchangeMTU(request: ExchangeMTURequest, responseObserver: StreamObserver<ExchangeMTUResponse>) { + override fun exchangeMTU( + request: ExchangeMTURequest, + responseObserver: StreamObserver<ExchangeMTUResponse> + ) { grpcUnary<ExchangeMTUResponse>(mScope, responseObserver) { val mtu = request.mtu Log.i(TAG, "exchangeMTU MTU=$mtu") - if (!GattInstance.get(request.connection.cookie).mGatt.requestMtu(mtu)) { + if (!GattInstance.get(request.connection.address).mGatt.requestMtu(mtu)) { Log.e(TAG, "Error on requesting MTU $mtu") throw Status.UNKNOWN.asException() } @@ -77,45 +83,37 @@ class Gatt(private val context: Context) : GATTImplBase() { } override fun writeAttFromHandle( - request: WriteRequest, - responseObserver: StreamObserver<WriteResponse> + request: WriteRequest, + responseObserver: StreamObserver<WriteResponse> ) { grpcUnary<WriteResponse>(mScope, responseObserver) { Log.i(TAG, "writeAttFromHandle handle=${request.handle}") - val gattInstance = GattInstance.get(request.connection.cookie) + val gattInstance = GattInstance.get(request.connection.address) var characteristic: BluetoothGattCharacteristic? = - getCharacteristicWithHandle(request.handle, gattInstance) + getCharacteristicWithHandle(request.handle, gattInstance) if (characteristic == null) { val descriptor: BluetoothGattDescriptor? = - getDescriptorWithHandle(request.handle, gattInstance) - checkNotNull(descriptor) { "Found no characteristic or descriptor with handle ${request.handle}" } - val valueWrote = gattInstance.writeDescriptorBlocking( - descriptor, - request.value.toByteArray() - ) - WriteResponse.newBuilder() - .setHandle(valueWrote.handle) - .setStatus(valueWrote.status) - .build() + getDescriptorWithHandle(request.handle, gattInstance) + checkNotNull(descriptor) { + "Found no characteristic or descriptor with handle ${request.handle}" + } + val valueWrote = + gattInstance.writeDescriptorBlocking(descriptor, request.value.toByteArray()) + WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } else { - val valueWrote = gattInstance.writeCharacteristicBlocking( - characteristic, - request.value.toByteArray() - ) - WriteResponse.newBuilder() - .setHandle(valueWrote.handle) - .setStatus(valueWrote.status) - .build() + val valueWrote = + gattInstance.writeCharacteristicBlocking(characteristic, request.value.toByteArray()) + WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } } } override fun discoverServiceByUuid( - request: DiscoverServiceByUuidRequest, - responseObserver: StreamObserver<DiscoverServicesResponse> + request: DiscoverServiceByUuidRequest, + responseObserver: StreamObserver<DiscoverServicesResponse> ) { grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) { - val gattInstance = GattInstance.get(request.connection.cookie) + val gattInstance = GattInstance.get(request.connection.address) Log.i(TAG, "discoverServiceByUuid uuid=${request.uuid}") // In some cases, GATT starts a discovery immediately after being connected, so // we need to wait until the service discovery is finished to be able to discover again. @@ -130,32 +128,32 @@ class Gatt(private val context: Context) : GATTImplBase() { } override fun discoverServices( - request: DiscoverServicesRequest, - responseObserver: StreamObserver<DiscoverServicesResponse> + request: DiscoverServicesRequest, + responseObserver: StreamObserver<DiscoverServicesResponse> ) { grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) { Log.i(TAG, "discoverServices") - val gattInstance = GattInstance.get(request.connection.cookie) + val gattInstance = GattInstance.get(request.connection.address) check(gattInstance.mGatt.discoverServices()) gattInstance.waitForDiscoveryEnd() DiscoverServicesResponse.newBuilder() - .addAllServices(generateServicesList(gattInstance.mGatt.services, 1)) - .build() + .addAllServices(generateServicesList(gattInstance.mGatt.services, 1)) + .build() } } override fun discoverServicesSdp( - request: DiscoverServicesSdpRequest, - responseObserver: StreamObserver<DiscoverServicesSdpResponse> + request: DiscoverServicesSdpRequest, + responseObserver: StreamObserver<DiscoverServicesSdpResponse> ) { grpcUnary<DiscoverServicesSdpResponse>(mScope, responseObserver) { Log.i(TAG, "discoverServicesSdp") val bluetoothDevice = request.address.toBluetoothDevice(mBluetoothAdapter) check(bluetoothDevice.fetchUuidsWithSdp()) flow - .filter { it.getAction() == BluetoothDevice.ACTION_UUID } - .filter { it.getBluetoothDeviceExtra() == bluetoothDevice } - .first() + .filter { it.getAction() == BluetoothDevice.ACTION_UUID } + .filter { it.getBluetoothDeviceExtra() == bluetoothDevice } + .first() val uuidsList = arrayListOf<String>() for (parcelUuid in bluetoothDevice.getUuids()) { uuidsList.add(parcelUuid.toString()) @@ -164,76 +162,102 @@ class Gatt(private val context: Context) : GATTImplBase() { } } - override fun clearCache(request: ClearCacheRequest, responseObserver: StreamObserver<ClearCacheResponse>) { + override fun clearCache( + request: ClearCacheRequest, + responseObserver: StreamObserver<ClearCacheResponse> + ) { grpcUnary<ClearCacheResponse>(mScope, responseObserver) { Log.i(TAG, "clearCache") - val gattInstance = GattInstance.get(request.connection.cookie) + val gattInstance = GattInstance.get(request.connection.address) check(gattInstance.mGatt.refresh()) ClearCacheResponse.newBuilder().build() } } override fun readCharacteristicFromHandle( - request: ReadCharacteristicRequest, - responseObserver: StreamObserver<ReadCharacteristicResponse> + request: ReadCharacteristicRequest, + responseObserver: StreamObserver<ReadCharacteristicResponse> ) { grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) { Log.i(TAG, "readCharacteristicFromHandle handle=${request.handle}") - val gattInstance = GattInstance.get(request.connection.cookie) + val gattInstance = GattInstance.get(request.connection.address) val characteristic: BluetoothGattCharacteristic? = - getCharacteristicWithHandle(request.handle, gattInstance) + getCharacteristicWithHandle(request.handle, gattInstance) checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." } val readValue = gattInstance.readCharacteristicBlocking(characteristic) ReadCharacteristicResponse.newBuilder() - .setValue( - AttValue.newBuilder() - .setHandle(readValue.handle) - .setValue(readValue.value) - ) - .setStatus(readValue.status) - .build() + .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) + .setStatus(readValue.status) + .build() } } override fun readCharacteristicsFromUuid( - request: ReadCharacteristicsFromUuidRequest, - responseObserver: StreamObserver<ReadCharacteristicsFromUuidResponse> + request: ReadCharacteristicsFromUuidRequest, + responseObserver: StreamObserver<ReadCharacteristicsFromUuidResponse> ) { grpcUnary<ReadCharacteristicsFromUuidResponse>(mScope, responseObserver) { Log.i(TAG, "readCharacteristicsFromUuid uuid=${request.uuid}") - val gattInstance = GattInstance.get(request.connection.cookie) + val gattInstance = GattInstance.get(request.connection.address) tryDiscoverServices(gattInstance) val readValues = - gattInstance.readCharacteristicUuidBlocking( - UUID.fromString(request.uuid), - request.startHandle, - request.endHandle - ) + gattInstance.readCharacteristicUuidBlocking( + UUID.fromString(request.uuid), request.startHandle, request.endHandle) ReadCharacteristicsFromUuidResponse.newBuilder() - .addAllCharacteristicsRead(generateReadValuesList(readValues)) - .build() + .addAllCharacteristicsRead(generateReadValuesList(readValues)) + .build() } } override fun readCharacteristicDescriptorFromHandle( - request: ReadCharacteristicDescriptorRequest, - responseObserver: StreamObserver<ReadCharacteristicDescriptorResponse> + request: ReadCharacteristicDescriptorRequest, + responseObserver: StreamObserver<ReadCharacteristicDescriptorResponse> ) { grpcUnary<ReadCharacteristicDescriptorResponse>(mScope, responseObserver) { Log.i(TAG, "readCharacteristicDescriptorFromHandle handle=${request.handle}") - val gattInstance = GattInstance.get(request.connection.cookie) + val gattInstance = GattInstance.get(request.connection.address) val descriptor: BluetoothGattDescriptor? = - getDescriptorWithHandle(request.handle, gattInstance) + getDescriptorWithHandle(request.handle, gattInstance) checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." } val readValue = gattInstance.readDescriptorBlocking(descriptor) ReadCharacteristicDescriptorResponse.newBuilder() - .setValue( - AttValue.newBuilder() - .setHandle(readValue.handle) - .setValue(readValue.value) - ) - .setStatus(readValue.status) - .build() + .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) + .setStatus(readValue.status) + .build() + } + } + + override fun registerService( + request: RegisterServiceRequest, + responseObserver: StreamObserver<RegisterServiceResponse> + ) { + grpcUnary(mScope, responseObserver) { + val service = + BluetoothGattService(UUID.fromString(request.service.uuid), SERVICE_TYPE_PRIMARY) + for (characteristic in request.service.characteristicsList) { + service.addCharacteristic( + BluetoothGattCharacteristic( + UUID.fromString(characteristic.uuid), + characteristic.properties, + characteristic.permissions)) + } + + val fullService = coroutineScope { + val firstService = mScope.async { serverManager.newServiceFlow.first() } + serverManager.server.addService(service) + firstService.await() + } + + RegisterServiceResponse.newBuilder() + .setService( + GattService.newBuilder() + .setHandle(fullService.instanceId) + .setType(fullService.type) + .setUuid(fullService.uuid.toString()) + .addAllIncludedServices(generateServicesList(service.includedServices, 1)) + .addAllCharacteristics(generateCharacteristicsList(service.characteristics)) + .build()) + .build() } } @@ -242,8 +266,8 @@ class Gatt(private val context: Context) : GATTImplBase() { * package-private so we have to redefine it here. */ private suspend fun getCharacteristicWithHandle( - handle: Int, - gattInstance: GattInstance + handle: Int, + gattInstance: GattInstance ): BluetoothGattCharacteristic? { tryDiscoverServices(gattInstance) for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) { @@ -261,8 +285,8 @@ class Gatt(private val context: Context) : GATTImplBase() { * package-private so we have to redefine it here. */ private suspend fun getDescriptorWithHandle( - handle: Int, - gattInstance: GattInstance + handle: Int, + gattInstance: GattInstance ): BluetoothGattDescriptor? { tryDiscoverServices(gattInstance) for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) { @@ -279,18 +303,18 @@ class Gatt(private val context: Context) : GATTImplBase() { /** Generates a list of GattService from a list of BluetoothGattService. */ private fun generateServicesList( - servicesList: List<BluetoothGattService>, - dpth: Int + servicesList: List<BluetoothGattService>, + dpth: Int ): ArrayList<GattService> { val newServicesList = arrayListOf<GattService>() for (service in servicesList) { val serviceBuilder = - GattService.newBuilder() - .setHandle(service.getInstanceId()) - .setType(service.getType()) - .setUuid(service.getUuid().toString()) - .addAllIncludedServices(generateServicesList(service.getIncludedServices(), dpth + 1)) - .addAllCharacteristics(generateCharacteristicsList(service.characteristics)) + GattService.newBuilder() + .setHandle(service.getInstanceId()) + .setType(service.getType()) + .setUuid(service.getUuid().toString()) + .addAllIncludedServices(generateServicesList(service.getIncludedServices(), dpth + 1)) + .addAllCharacteristics(generateCharacteristicsList(service.characteristics)) newServicesList.add(serviceBuilder.build()) } return newServicesList @@ -298,17 +322,17 @@ class Gatt(private val context: Context) : GATTImplBase() { /** Generates a list of GattCharacteristic from a list of BluetoothGattCharacteristic. */ private fun generateCharacteristicsList( - characteristicsList: List<BluetoothGattCharacteristic> + characteristicsList: List<BluetoothGattCharacteristic> ): ArrayList<GattCharacteristic> { val newCharacteristicsList = arrayListOf<GattCharacteristic>() for (characteristic in characteristicsList) { val characteristicBuilder = - GattCharacteristic.newBuilder() - .setProperties(characteristic.getProperties()) - .setPermissions(characteristic.getPermissions()) - .setUuid(characteristic.getUuid().toString()) - .addAllDescriptors(generateDescriptorsList(characteristic.getDescriptors())) - .setHandle(characteristic.getInstanceId()) + GattCharacteristic.newBuilder() + .setProperties(characteristic.getProperties()) + .setPermissions(characteristic.getPermissions()) + .setUuid(characteristic.getUuid().toString()) + .addAllDescriptors(generateDescriptorsList(characteristic.getDescriptors())) + .setHandle(characteristic.getInstanceId()) newCharacteristicsList.add(characteristicBuilder.build()) } return newCharacteristicsList @@ -316,15 +340,15 @@ class Gatt(private val context: Context) : GATTImplBase() { /** Generates a list of GattCharacteristicDescriptor from a list of BluetoothGattDescriptor. */ private fun generateDescriptorsList( - descriptorsList: List<BluetoothGattDescriptor> + descriptorsList: List<BluetoothGattDescriptor> ): ArrayList<GattCharacteristicDescriptor> { val newDescriptorsList = arrayListOf<GattCharacteristicDescriptor>() for (descriptor in descriptorsList) { val descriptorBuilder = - GattCharacteristicDescriptor.newBuilder() - .setHandle(descriptor.getInstanceId()) - .setPermissions(descriptor.getPermissions()) - .setUuid(descriptor.getUuid().toString()) + GattCharacteristicDescriptor.newBuilder() + .setHandle(descriptor.getInstanceId()) + .setPermissions(descriptor.getPermissions()) + .setUuid(descriptor.getUuid().toString()) newDescriptorsList.add(descriptorBuilder.build()) } return newDescriptorsList @@ -332,18 +356,14 @@ class Gatt(private val context: Context) : GATTImplBase() { /** Generates a list of ReadCharacteristicResponse from a list of GattInstanceValueRead. */ private fun generateReadValuesList( - readValuesList: ArrayList<GattInstance.GattInstanceValueRead> + readValuesList: ArrayList<GattInstance.GattInstanceValueRead> ): ArrayList<ReadCharacteristicResponse> { val newReadValuesList = arrayListOf<ReadCharacteristicResponse>() for (readValue in readValuesList) { val readValueBuilder = - ReadCharacteristicResponse.newBuilder() - .setValue( - AttValue.newBuilder() - .setHandle(readValue.handle) - .setValue(readValue.value) - ) - .setStatus(readValue.status) + ReadCharacteristicResponse.newBuilder() + .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) + .setStatus(readValue.status) newReadValuesList.add(readValueBuilder.build()) } return newReadValuesList diff --git a/android/pandora/server/src/com/android/pandora/GattServerManager.kt b/android/pandora/server/src/com/android/pandora/GattServerManager.kt new file mode 100644 index 0000000000..d753a84876 --- /dev/null +++ b/android/pandora/server/src/com/android/pandora/GattServerManager.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 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.pandora + +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattServer +import android.bluetooth.BluetoothGattServerCallback +import android.bluetooth.BluetoothGattService +import android.bluetooth.BluetoothManager +import android.content.Context +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map + +class GattServerManager(bluetoothManager: BluetoothManager, context: Context, globalScope: CoroutineScope) { + val services = mutableMapOf<UUID, BluetoothGattService>() + val server: BluetoothGattServer + + val newServiceFlow = MutableSharedFlow<BluetoothGattService>(extraBufferCapacity = 8) + + init { + newServiceFlow.map { + services[it.uuid] = it + }.launchIn(globalScope) + } + + init { + val callback = + object : BluetoothGattServerCallback() { + override fun onServiceAdded(status: Int, service: BluetoothGattService) { + check(status == BluetoothGatt.GATT_SUCCESS) + check(newServiceFlow.tryEmit(service)) + } + } + server = bluetoothManager.openGattServer(context, callback) + } +} diff --git a/android/pandora/server/src/com/android/pandora/Host.kt b/android/pandora/server/src/com/android/pandora/Host.kt index 6862d75fd9..f0811488d4 100644 --- a/android/pandora/server/src/com/android/pandora/Host.kt +++ b/android/pandora/server/src/com/android/pandora/Host.kt @@ -17,33 +17,40 @@ package com.android.pandora import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothAssignedNumbers import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothDevice.ADDRESS_TYPE_PUBLIC +import android.bluetooth.BluetoothDevice.ADDRESS_TYPE_RANDOM import android.bluetooth.BluetoothDevice.BOND_BONDED +import android.bluetooth.BluetoothDevice.TRANSPORT_AUTO +import android.bluetooth.BluetoothDevice.TRANSPORT_BREDR import android.bluetooth.BluetoothDevice.TRANSPORT_LE import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.bluetooth.le.AdvertiseCallback import android.bluetooth.le.AdvertiseData import android.bluetooth.le.AdvertiseSettings -import android.bluetooth.le.AdvertisingSetParameters import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.MacAddress +import android.os.ParcelUuid import android.util.Log import com.google.protobuf.ByteString import com.google.protobuf.Empty import io.grpc.Status import io.grpc.stub.StreamObserver -import kotlin.Result.Companion.failure -import kotlin.Result.Companion.success -import kotlin.coroutines.suspendCoroutine +import java.io.IOException +import java.time.Duration +import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.sendBlocking import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -55,19 +62,25 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout import pandora.HostGrpc.HostImplBase import pandora.HostProto.* @kotlinx.coroutines.ExperimentalCoroutinesApi class Host(private val context: Context, private val server: Server) : HostImplBase() { private val TAG = "PandoraHost" - private val ADVERTISEMENT_DURATION_MILLIS: Int = 10000 + private val scope: CoroutineScope private val flow: Flow<Intent> private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! private val bluetoothAdapter = bluetoothManager.adapter + private var connectability = ConnectabilityMode.CONNECTABILITY_UNSPECIFIED + private var discoverability = DiscoverabilityMode.DISCOVERABILITY_UNSPECIFIED + + private val advertisers = mutableMapOf<UUID, AdvertiseCallback>() + init { scope = CoroutineScope(Dispatchers.Default) @@ -77,6 +90,9 @@ class Host(private val context: Context, private val server: Server) : HostImplB intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST) + intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED) + intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) + intentFilter.addAction(BluetoothDevice.ACTION_FOUND) // Creates a shared flow of intents that can be used in all methods in the coroutine scope. // This flow is started eagerly to make sure that the broadcast receiver is registered before @@ -216,81 +232,59 @@ class Host(private val context: Context, private val server: Server) : HostImplB acceptPairingAndAwaitBonded(bluetoothDevice) WaitConnectionResponse.newBuilder() - .setConnection( - Connection.newBuilder() - .setCookie(ByteString.copyFromUtf8(bluetoothDevice.address)) - .build() - ) + .setConnection(newConnection(bluetoothDevice, Transport.TRANSPORT_BREDR)) .build() } } - /** - * Set the device in advertisement mode for #ADVERTISEMENT_DURATION_MILLIS milliseconds. - * @param request Request sent by the client. - * @param responseObserver Response to build and set back to the client. - */ - override fun setLEConnectable( - request: Empty, - responseObserver: StreamObserver<Empty>, - ) { - // Creates a gRPC coroutine in a given coroutine scope which executes a given suspended function - // returning a gRPC response and sends it on a given gRPC stream observer. - grpcUnary<Empty>(scope, responseObserver) { - Log.i(TAG, "setLEConnectable") - val advertiser = bluetoothAdapter.getBluetoothLeAdvertiser() - val advertiseSettings = - AdvertiseSettings.Builder() - .setConnectable(true) - .setOwnAddressType(AdvertisingSetParameters.ADDRESS_TYPE_PUBLIC) - .setTimeout(ADVERTISEMENT_DURATION_MILLIS) - .build() - val advertiseData = AdvertiseData.Builder().build() - suspendCoroutine<Boolean> { continuation -> - val advertiseCallback = - object : AdvertiseCallback() { - override fun onStartFailure(errorCode: Int) { - Log.i(TAG, "Advertising failed: $errorCode") - continuation.resumeWith(failure(Exception("Advertising failed: $errorCode"))) - } - - override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { - Log.i(TAG, "Advertising success") - continuation.resumeWith(success(true)) - } - } - advertiser.startAdvertising(advertiseSettings, advertiseData, advertiseCallback) - } - - // Response sent to client - Empty.getDefaultInstance() - } - } - override fun connect(request: ConnectRequest, responseObserver: StreamObserver<ConnectResponse>) { grpcUnary(scope, responseObserver) { val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter) Log.i(TAG, "connect: address=$bluetoothDevice") + bluetoothAdapter.cancelDiscovery() + if (!bluetoothDevice.isConnected()) { - if (bluetoothDevice.bondState == BOND_BONDED) { - // already bonded, just reconnect - bluetoothDevice.connect() - waitConnectionIntent(bluetoothDevice) + if (request.skipPairing) { + // do an SDP request to trigger a temporary BREDR connection + try { + withTimeout(1500) { bluetoothDevice.createRfcommSocket(3).connect() } + } catch (e: IOException) { + // ignore + } } else { - // need to bond - bluetoothDevice.createBond() - acceptPairingAndAwaitBonded(bluetoothDevice) + if (bluetoothDevice.bondState == BOND_BONDED) { + // already bonded, just reconnect + bluetoothDevice.connect() + waitConnectionIntent(bluetoothDevice) + } else { + // need to bond + bluetoothDevice.createBond() + if (!request.manuallyConfirm) { + acceptPairingAndAwaitBonded(bluetoothDevice) + } + } } } ConnectResponse.newBuilder() - .setConnection( - Connection.newBuilder() - .setCookie(ByteString.copyFromUtf8(bluetoothDevice.address)) - .build() - ) + .setConnection(newConnection(bluetoothDevice, Transport.TRANSPORT_BREDR)) + .build() + } + } + + override fun getConnection( + request: GetConnectionRequest, + responseObserver: StreamObserver<GetConnectionResponse> + ) { + grpcUnary(scope, responseObserver) { + val device = bluetoothAdapter.getRemoteDevice(request.address.toByteArray()) + check( + device.isConnected() && device.type != BluetoothDevice.DEVICE_TYPE_LE + ) // either classic or dual + GetConnectionResponse.newBuilder() + .setConnection(newConnection(device, Transport.TRANSPORT_BREDR)) .build() } } @@ -316,6 +310,7 @@ class Host(private val context: Context, private val server: Server) : HostImplB bluetoothDevice.disconnect() connectionStateChangedFlow.filter { it == BluetoothAdapter.STATE_DISCONNECTED }.first() + DisconnectResponse.getDefaultInstance() } } @@ -326,13 +321,17 @@ class Host(private val context: Context, private val server: Server) : HostImplB ) { grpcUnary<ConnectLEResponse>(scope, responseObserver) { val address = request.address.decodeAsMacAddressToString() - Log.i(TAG, "connectLE: $address") - val device = scanLeDevice(address) - GattInstance(device!!, TRANSPORT_LE, context).waitForState(BluetoothProfile.STATE_CONNECTED) + Log.i(TAG, "connect LE: $address") + val device = scanLeDevice(address)!! + GattInstance(device, TRANSPORT_LE, context) + + flow + .filter { it.action == BluetoothDevice.ACTION_ACL_CONNECTED } + .filter { it.getBluetoothDeviceExtra() == device } + .first() + ConnectLEResponse.newBuilder() - .setConnection( - Connection.newBuilder().setCookie(ByteString.copyFromUtf8(device.address)).build() - ) + .setConnection(newConnection(device, Transport.TRANSPORT_LE)) .build() } } @@ -344,13 +343,10 @@ class Host(private val context: Context, private val server: Server) : HostImplB grpcUnary<GetLEConnectionResponse>(scope, responseObserver) { val address = request.address.decodeAsMacAddressToString() Log.i(TAG, "getLEConnection: $address") - val device = - bluetoothAdapter.getRemoteLeDevice(address, BluetoothDevice.ADDRESS_TYPE_PUBLIC) + val device = bluetoothAdapter.getRemoteLeDevice(address, BluetoothDevice.ADDRESS_TYPE_PUBLIC) if (device.isConnected) { GetLEConnectionResponse.newBuilder() - .setConnection( - Connection.newBuilder().setCookie(ByteString.copyFromUtf8(device.address)).build() - ) + .setConnection(newConnection(device, Transport.TRANSPORT_LE)) .build() } else { Log.e(TAG, "Device: $device is not connected") @@ -361,7 +357,7 @@ class Host(private val context: Context, private val server: Server) : HostImplB override fun disconnectLE(request: DisconnectLERequest, responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(scope, responseObserver) { - val address = request.connection.cookie.toByteArray().decodeToString() + val address = request.connection.address Log.i(TAG, "disconnectLE: $address") val gattInstance = GattInstance.get(address) @@ -405,4 +401,217 @@ class Host(private val context: Context, private val server: Server) : HostImplB } return bluetoothDevice } + + override fun startAdvertising( + request: StartAdvertisingRequest, + responseObserver: StreamObserver<StartAdvertisingResponse> + ) { + Log.d(TAG, "startAdvertising") + grpcUnary(scope, responseObserver) { + val handle = UUID.randomUUID() + + callbackFlow { + val callback = + object : AdvertiseCallback() { + override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { + sendBlocking( + StartAdvertisingResponse.newBuilder() + .setHandle( + AdvertisingHandle.newBuilder() + .setCookie(ByteString.copyFromUtf8(handle.toString())) + ) + .build() + ) + } + override fun onStartFailure(errorCode: Int) { + error("failed to start advertising") + } + } + + advertisers[handle] = callback + + val advertisingDataBuilder = AdvertiseData.Builder() + + for (service_uuid in request.advertisingData.serviceUuidsList) { + advertisingDataBuilder.addServiceUuid(ParcelUuid.fromString(service_uuid)) + } + + advertisingDataBuilder + .setIncludeDeviceName(request.advertisingData.includeLocalName) + .setIncludeTxPowerLevel(request.advertisingData.includeTxPowerLevel) + .addManufacturerData( + BluetoothAssignedNumbers.GOOGLE, + request.advertisingData.manufacturerSpecificData.toByteArray() + ) + + bluetoothAdapter.bluetoothLeAdvertiser.startAdvertising( + AdvertiseSettings.Builder() + .setConnectable( + request.connectabilityMode == ConnectabilityMode.CONNECTABILITY_CONNECTABLE + ) + .setOwnAddressType( + when (request.ownAddressType!!) { + AddressType.PUBLIC -> ADDRESS_TYPE_PUBLIC + AddressType.RANDOM -> ADDRESS_TYPE_RANDOM + AddressType.UNRECOGNIZED -> + error("unrecognized address type ${request.ownAddressType}") + } + ) + .build(), + advertisingDataBuilder.build(), + callback, + ) + + awaitClose { /* no-op */} + } + .first() + } + } + + override fun runInquiry( + request: RunInquiryRequest, + responseObserver: StreamObserver<RunInquiryResponse> + ) { + Log.d(TAG, "runInquiry") + grpcServerStream(scope, responseObserver) { + launch { + try { + bluetoothAdapter.startDiscovery() + awaitCancellation() + } finally { + bluetoothAdapter.cancelDiscovery() + } + } + flow + .filter { it.action == BluetoothDevice.ACTION_FOUND } + .map { + val device = it.getBluetoothDeviceExtra() + Log.i(TAG, "Device found: $device") + RunInquiryResponse.newBuilder() + .addDevice( + Device.newBuilder() + .setName(device.name) + .setAddress(device.toByteString()) + ) + .build() + } + } + } + + override fun setConnectabilityMode( + request: SetConnectabilityModeRequest, + responseObserver: StreamObserver<SetConnectabilityModeResponse> + ) { + grpcUnary(scope, responseObserver) { + Log.d(TAG, "setConnectabilityMode") + connectability = request.connectability!! + + val scanMode = + when (connectability) { + ConnectabilityMode.CONNECTABILITY_UNSPECIFIED, + ConnectabilityMode.UNRECOGNIZED -> null + ConnectabilityMode.CONNECTABILITY_NOT_CONNECTABLE -> { + BluetoothAdapter.SCAN_MODE_NONE + } + ConnectabilityMode.CONNECTABILITY_CONNECTABLE -> { + if ( + discoverability == DiscoverabilityMode.DISCOVERABILITY_LIMITED || + discoverability == DiscoverabilityMode.DISCOVERABILITY_GENERAL + ) { + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE + } else { + BluetoothAdapter.SCAN_MODE_CONNECTABLE + } + } + } + + if (scanMode != null) { + bluetoothAdapter.setScanMode(scanMode) + } + SetConnectabilityModeResponse.getDefaultInstance() + } + } + + override fun setDiscoverabilityMode( + request: SetDiscoverabilityModeRequest, + responseObserver: StreamObserver<SetDiscoverabilityModeResponse> + ) { + Log.d(TAG, "setDiscoverabilityMode") + grpcUnary(scope, responseObserver) { + discoverability = request.discoverability!! + + val scanMode = + when (discoverability) { + DiscoverabilityMode.DISCOVERABILITY_UNSPECIFIED, + DiscoverabilityMode.UNRECOGNIZED -> null + DiscoverabilityMode.DISCOVERABILITY_NONE -> + if (connectability == ConnectabilityMode.CONNECTABILITY_CONNECTABLE) { + BluetoothAdapter.SCAN_MODE_CONNECTABLE + } else { + BluetoothAdapter.SCAN_MODE_NONE + } + DiscoverabilityMode.DISCOVERABILITY_LIMITED, + DiscoverabilityMode.DISCOVERABILITY_GENERAL -> + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE + } + + if (scanMode != null) { + bluetoothAdapter.setScanMode(scanMode) + } + + if (request.discoverability == DiscoverabilityMode.DISCOVERABILITY_LIMITED) { + bluetoothAdapter.setDiscoverableTimeout( + Duration.ofSeconds(120) + ) // limited discoverability needs a timeout, 120s is Android default + } + SetDiscoverabilityModeResponse.getDefaultInstance() + } + } + + override fun runDiscovery( + request: RunDiscoveryRequest, + responseObserver: StreamObserver<RunDiscoveryResponse> + ) { + Log.d(TAG, "runDiscovery") + grpcServerStream(scope, responseObserver) { + callbackFlow { + val callback = + object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + sendBlocking( + RunDiscoveryResponse.newBuilder() + .setDevice( + Device.newBuilder() + .setAddress( + ByteString.copyFrom( + MacAddress.fromString(result.device.address).toByteArray() + ) + ) + .setName(result.device.name ?: "") + ) + .setFlags(result.scanRecord?.advertiseFlags ?: 0) + .build() + ) + } + + override fun onScanFailed(errorCode: Int) { + error("scan failed") + } + } + bluetoothAdapter.bluetoothLeScanner.startScan(callback) + + awaitClose { bluetoothAdapter.bluetoothLeScanner.stopScan(callback) } + } + } + } + + override fun getDeviceName( + request: GetDeviceNameRequest, + responseObserver: StreamObserver<GetDeviceNameResponse> + ) { + grpcUnary(scope, responseObserver) { + val device = request.connection.toBluetoothDevice(bluetoothAdapter) + GetDeviceNameResponse.newBuilder().setName(device.name).build() + } + } } diff --git a/android/pandora/server/src/com/android/pandora/Security.kt b/android/pandora/server/src/com/android/pandora/Security.kt index e7ded2990a..01247f03bd 100644 --- a/android/pandora/server/src/com/android/pandora/Security.kt +++ b/android/pandora/server/src/com/android/pandora/Security.kt @@ -18,9 +18,15 @@ package com.android.pandora import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothDevice.ACTION_PAIRING_REQUEST +import android.bluetooth.BluetoothDevice.BOND_BONDED import android.bluetooth.BluetoothDevice.DEVICE_TYPE_CLASSIC +import android.bluetooth.BluetoothDevice.DEVICE_TYPE_DUAL import android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE import android.bluetooth.BluetoothDevice.EXTRA_PAIRING_VARIANT +import android.bluetooth.BluetoothDevice.TRANSPORT_AUTO +import android.bluetooth.BluetoothDevice.TRANSPORT_BREDR +import android.bluetooth.BluetoothDevice.TRANSPORT_LE import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent @@ -31,6 +37,7 @@ import com.google.protobuf.Empty import io.grpc.stub.StreamObserver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -57,6 +64,7 @@ class Security(private val context: Context) : SecurityImplBase() { init { val intentFilter = IntentFilter() intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST) + intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) flow = intentFlow(context, intentFilter).shareIn(globalScope, SharingStarted.Eagerly) } @@ -68,8 +76,17 @@ class Security(private val context: Context) : SecurityImplBase() { override fun pair(request: PairRequest, responseObserver: StreamObserver<Empty>) { grpcUnary(globalScope, responseObserver) { val bluetoothDevice = request.connection.toBluetoothDevice(bluetoothAdapter) - Log.i(TAG, "pair: ${bluetoothDevice.address}") - bluetoothDevice.createBond() + Log.i( + TAG, + "pair: ${bluetoothDevice.address} (current bond state: ${bluetoothDevice.bondState})" + ) + bluetoothDevice.createBond( + when (request.connection.transport!!) { + Transport.TRANSPORT_LE -> TRANSPORT_LE + Transport.TRANSPORT_BREDR -> TRANSPORT_BREDR + Transport.UNRECOGNIZED -> TRANSPORT_AUTO + } + ) Empty.getDefaultInstance() } } @@ -78,26 +95,28 @@ class Security(private val context: Context) : SecurityImplBase() { request: DeletePairingRequest, responseObserver: StreamObserver<DeletePairingResponse> ) { - grpcUnary<DeletePairingResponse>(globalScope, responseObserver) { + grpcUnary(globalScope, responseObserver) { val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter) Log.i(TAG, "DeletePairing: device=$bluetoothDevice") + val unbonded = + globalScope.async { + flow + .filter { it.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED } + .filter { it.getBluetoothDeviceExtra() == bluetoothDevice } + .filter { + it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) == + BluetoothDevice.BOND_NONE + } + .first() + } + if (bluetoothDevice.removeBond()) { Log.i(TAG, "DeletePairing: device=$bluetoothDevice - wait BOND_NONE intent") - flow - .filter { it.getAction() == BluetoothDevice.ACTION_BOND_STATE_CHANGED } - .filter { it.getBluetoothDeviceExtra() == bluetoothDevice } - .filter { - it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) == - BluetoothDevice.BOND_NONE - } - .filter { - it.getIntExtra(BluetoothDevice.EXTRA_REASON, BluetoothAdapter.ERROR) == - BluetoothDevice.BOND_SUCCESS - } - .first() + unbonded.await() } else { Log.i(TAG, "DeletePairing: device=$bluetoothDevice - Already unpaired") + unbonded.cancel() } DeletePairingResponse.getDefaultInstance() } @@ -125,63 +144,62 @@ class Security(private val context: Context) : SecurityImplBase() { } .launchIn(this) - flow.map { intent -> - val device = intent.getBluetoothDeviceExtra() - val variant = intent.getIntExtra(EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR) - Log.i( - TAG, - "OnPairing: Handling PairingEvent ${variant} for device ${device.address}" - ) - val eventBuilder = - PairingEvent.newBuilder().setAddress(ByteString.copyFrom(device.toByteArray())) - when (variant) { - // SSP / LE Just Works - BluetoothDevice.PAIRING_VARIANT_CONSENT -> - eventBuilder.justWorks = Empty.getDefaultInstance() - - // SSP / LE Numeric Comparison - BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION -> - eventBuilder.numericComparison = - intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR) - BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY -> { - val passkey = - intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR) - eventBuilder.passkeyEntryNotification = passkey - } + flow + .filter { intent -> intent.action == ACTION_PAIRING_REQUEST } + .map { intent -> + val device = intent.getBluetoothDeviceExtra() + val variant = intent.getIntExtra(EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR) + Log.i(TAG, "OnPairing: Handling PairingEvent ${variant} for device ${device.address}") + val eventBuilder = + PairingEvent.newBuilder().setAddress(device.toByteString()) + when (variant) { + // SSP / LE Just Works + BluetoothDevice.PAIRING_VARIANT_CONSENT -> + eventBuilder.justWorks = Empty.getDefaultInstance() + + // SSP / LE Numeric Comparison + BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION -> + eventBuilder.numericComparison = + intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR) + BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY -> { + val passkey = + intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR) + eventBuilder.passkeyEntryNotification = passkey + } - // Out-Of-Band not currently supported - BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT -> - error("Received OOB pairing confirmation (UNSUPPORTED)") - - // Legacy PIN entry, or LE legacy passkey entry, depending on transport - BluetoothDevice.PAIRING_VARIANT_PIN -> - when (device.type) { - DEVICE_TYPE_CLASSIC -> eventBuilder.pinCodeRequest = Empty.getDefaultInstance() - DEVICE_TYPE_LE -> - eventBuilder.passkeyEntryRequest = Empty.getDefaultInstance() - else -> error("cannot determine pairing variant, since transport is unknown") + // Out-Of-Band not currently supported + BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT -> + error("Received OOB pairing confirmation (UNSUPPORTED)") + + // Legacy PIN entry, or LE legacy passkey entry, depending on transport + BluetoothDevice.PAIRING_VARIANT_PIN -> + when (device.type) { + DEVICE_TYPE_CLASSIC -> eventBuilder.pinCodeRequest = Empty.getDefaultInstance() + DEVICE_TYPE_LE -> eventBuilder.passkeyEntryRequest = Empty.getDefaultInstance() + else -> error("cannot determine pairing variant, since transport is unknown") + } + BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS -> + eventBuilder.pinCodeRequest = Empty.getDefaultInstance() + + // Legacy PIN entry or LE legacy passkey entry, except we just generate the PIN in + // the + // stack and display it to the user for convenience + BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN -> { + val passkey = + intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR) + when (device.type) { + DEVICE_TYPE_CLASSIC -> + eventBuilder.pinCodeNotification = + ByteString.copyFrom(passkey.toString().toByteArray()) + DEVICE_TYPE_LE -> eventBuilder.passkeyEntryNotification = passkey + else -> error("cannot determine pairing variant, since transport is unknown") + } } - BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS -> - eventBuilder.pinCodeRequest = Empty.getDefaultInstance() - - // Legacy PIN entry or LE legacy passkey entry, except we just generate the PIN in the - // stack and display it to the user for convenience - BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN -> { - val passkey = - intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR) - when (device.type) { - DEVICE_TYPE_CLASSIC -> - eventBuilder.pinCodeNotification = - ByteString.copyFrom(passkey.toString().toByteArray()) - DEVICE_TYPE_LE -> eventBuilder.passkeyEntryNotification = passkey - else -> error("cannot determine pairing variant, since transport is unknown") + else -> { + error("Received unknown pairing variant $variant") } } - else -> { - error("Received unknown pairing variant $variant") - } + eventBuilder.build() } - eventBuilder.build() - } } } diff --git a/android/pandora/server/src/com/android/pandora/Server.kt b/android/pandora/server/src/com/android/pandora/Server.kt index 4c55be6696..566c29d450 100644 --- a/android/pandora/server/src/com/android/pandora/Server.kt +++ b/android/pandora/server/src/com/android/pandora/Server.kt @@ -16,6 +16,8 @@ package com.android.pandora +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothProfile import android.content.Context import android.util.Log import io.grpc.Server as GrpcServer @@ -28,7 +30,8 @@ class Server(context: Context) { private val GRPC_PORT = 8999 private var host: Host - private var a2dp: A2dp + private var a2dp: A2dp? = null + private var a2dpSink: A2dpSink? = null private var avrcp: Avrcp private var gatt: Gatt private var hfp: Hfp @@ -39,24 +42,34 @@ class Server(context: Context) { init { host = Host(context, this) - a2dp = A2dp(context) avrcp = Avrcp(context) gatt = Gatt(context) hfp = Hfp(context) hid = Hid(context) l2cap = L2cap(context) security = Security(context) - grpcServer = + + val grpcServerBuilder = NettyServerBuilder.forPort(GRPC_PORT) .addService(host) - .addService(a2dp) .addService(avrcp) .addService(gatt) .addService(hfp) .addService(hid) .addService(l2cap) .addService(security) - .build() + + val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java)!!.adapter + val is_a2dp_source = bluetoothAdapter.getSupportedProfiles().contains(BluetoothProfile.A2DP) + if (is_a2dp_source) { + a2dp = A2dp(context) + grpcServerBuilder.addService(a2dp!!) + } else { + a2dpSink = A2dpSink(context) + grpcServerBuilder.addService(a2dpSink!!) + } + + grpcServer = grpcServerBuilder.build() Log.d(TAG, "Starting Pandora Server") grpcServer.start() @@ -69,7 +82,8 @@ class Server(context: Context) { fun deinit() { host.deinit() - a2dp.deinit() + a2dp?.deinit() + a2dpSink?.deinit() avrcp.deinit() gatt.deinit() hfp.deinit() diff --git a/android/pandora/server/src/com/android/pandora/Utils.kt b/android/pandora/server/src/com/android/pandora/Utils.kt index a50b823cbb..a59deb0859 100644 --- a/android/pandora/server/src/com/android/pandora/Utils.kt +++ b/android/pandora/server/src/com/android/pandora/Utils.kt @@ -29,6 +29,7 @@ import android.os.ParcelFileDescriptor import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.google.protobuf.ByteString +import io.grpc.stub.ServerCallStreamObserver import io.grpc.stub.StreamObserver import java.io.BufferedReader import java.io.InputStreamReader @@ -52,6 +53,8 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import pandora.HostProto.Connection +import pandora.HostProto.InternalConnectionRef +import pandora.HostProto.Transport fun shell(cmd: String): String { val fd = InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd) @@ -137,7 +140,6 @@ fun <T> grpcUnary( * Example usage: * ``` * override fun grpcMethod( - * request: TypeOfRequest, * responseObserver: StreamObserver<TypeOfResponse> { * grpcBidirectionalStream(scope, responseObserver) { * block @@ -196,6 +198,56 @@ fun <T, U> grpcBidirectionalStream( } /** + * Creates a gRPC coroutine in a given coroutine scope which executes a given suspended function + * taking in a Flow of gRPC requests and returning a Flow of gRPC responses and sends it on a given + * gRPC stream observer. + * + * @param T the type of gRPC response. + * @param scope coroutine scope used to run the coroutine. + * @param responseObserver the gRPC stream observer on which to send the response. + * @param block the suspended function producing the response Flow. + * @return a StreamObserver for the incoming requests. + * + * Example usage: + * ``` + * override fun grpcMethod( + * request: TypeOfRequest, + * responseObserver: StreamObserver<TypeOfResponse> { + * grpcServerStream(scope, responseObserver) { + * block + * } + * } + * } + * ``` + */ +@kotlinx.coroutines.ExperimentalCoroutinesApi +fun <T> grpcServerStream( + scope: CoroutineScope, + responseObserver: StreamObserver<T>, + block: CoroutineScope.() -> Flow<T> +) { + val serverCallStreamObserver = responseObserver as ServerCallStreamObserver<T> + + val job = + scope.launch { + block() + .onEach { responseObserver.onNext(it) } + .onCompletion { error -> + if (error == null) { + responseObserver.onCompleted() + } + } + .catch { + it.printStackTrace() + responseObserver.onError(it) + } + .launchIn(this) + } + + serverCallStreamObserver.setOnCancelHandler { job.cancel() } +} + +/** * Synchronous method to get a Bluetooth profile proxy. * * @param T the type of profile proxy (e.g. BluetoothA2dp) @@ -230,11 +282,11 @@ fun <T> getProfileProxy(context: Context, profile: Int): T { if (proxy == null) { Log.w(TAG, "profile proxy $profile is null") } - return proxy as T + return proxy!! as T } fun Intent.getBluetoothDeviceExtra(): BluetoothDevice = - this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) + this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)!! fun ByteString.decodeAsMacAddressToString(): String = MacAddress.fromBytes(this.toByteArray()).toString().uppercase() @@ -243,6 +295,24 @@ fun ByteString.toBluetoothDevice(adapter: BluetoothAdapter): BluetoothDevice = adapter.getRemoteDevice(this.decodeAsMacAddressToString()) fun Connection.toBluetoothDevice(adapter: BluetoothAdapter): BluetoothDevice = - adapter.getRemoteDevice(this.cookie.toByteArray().decodeToString()) + adapter.getRemoteDevice(address) + +val Connection.address: String + get() = InternalConnectionRef.parseFrom(this.cookie).address.decodeAsMacAddressToString() + +val Connection.transport: Transport + get() = InternalConnectionRef.parseFrom(this.cookie).transport + +fun newConnection(device: BluetoothDevice, transport: Transport) = + Connection.newBuilder() + .setCookie( + InternalConnectionRef.newBuilder() + .setAddress(device.toByteString()) + .setTransport(transport) + .build() + .toByteString() + ) + .build()!! -fun BluetoothDevice.toByteArray(): ByteArray = MacAddress.fromString(this.address).toByteArray() +fun BluetoothDevice.toByteString() = + ByteString.copyFrom(MacAddress.fromString(this.address).toByteArray())!! @@ -537,10 +537,16 @@ class HostBuild(): shutil.rmtree(self.output_dir) # Remove Cargo.lock that may have become generated - try: - os.remove(os.path.join(self.platform_dir, 'bt', 'Cargo.lock')) - except FileNotFoundError: - pass + cargo_lock_files = [ + os.path.join(self.platform_dir, 'bt', 'Cargo.lock'), + os.path.join(self.platform_dir, 'bt', 'tools', 'rootcanal', 'Cargo.lock'), + ] + for lock_file in cargo_lock_files: + try: + os.remove(lock_file) + print('Removed {}'.format(lock_file)) + except FileNotFoundError: + pass def _target_all(self): """ Build all common targets (skipping doc, test, and clean). diff --git a/system/audio_bluetooth_hw/Android.bp b/system/audio_bluetooth_hw/Android.bp index 9dc7f53234..cc89bdf7c8 100644 --- a/system/audio_bluetooth_hw/Android.bp +++ b/system/audio_bluetooth_hw/Android.bp @@ -9,8 +9,18 @@ package { default_applicable_licenses: ["system_bt_license"], } +cc_defaults { + name: "audio_bluetooth_hw_defaults", + defaults: ["fluoride_common_options"], + cflags: [ + // suppress the warning in stream_apis.cc + "-Wno-sign-compare", + ], +} + cc_library_shared { name: "audio.bluetooth.default", + defaults: ["audio_bluetooth_hw_defaults"], relative_install_path: "hw", proprietary: true, srcs: [ @@ -37,15 +47,11 @@ cc_library_shared { "libbluetooth_audio_session", "libhidlbase", ], - cflags: [ - "-Wall", - "-Werror", - "-Wno-unused-parameter", - ], } cc_test { name: "audio_bluetooth_hw_test", + defaults: ["audio_bluetooth_hw_defaults"], srcs: [ "utils.cc", "utils_unittest.cc", @@ -56,9 +62,4 @@ cc_test { "liblog", "libutils", ], - cflags: [ - "-Wall", - "-Werror", - "-Wno-unused-parameter", - ], } diff --git a/system/binder/android/bluetooth/IBluetoothLeAudio.aidl b/system/binder/android/bluetooth/IBluetoothLeAudio.aidl index 1859833d56..6fc9c8b20d 100644 --- a/system/binder/android/bluetooth/IBluetoothLeAudio.aidl +++ b/system/binder/android/bluetooth/IBluetoothLeAudio.aidl @@ -67,12 +67,15 @@ oneway interface IBluetoothLeAudio { void setCcidInformation(in ParcelUuid userUuid, in int ccid, in int contextType, in AttributionSource attributionSource, in SynchronousResultReceiver receiver); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") void setInCall(in boolean inCall, in AttributionSource attributionSource, in SynchronousResultReceiver receiver); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void setInactiveForHfpHandover(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver); /* Same value as bluetooth::groups::kGroupUnknown */ const int LE_AUDIO_GROUP_ID_INVALID = -1; const int GROUP_STATUS_INACTIVE = 0; const int GROUP_STATUS_ACTIVE = 1; + const int GROUP_STATUS_TURNED_IDLE_DURING_CALL = 2; const int GROUP_NODE_ADDED = 1; const int GROUP_NODE_REMOVED = 2; diff --git a/system/blueberry/facade/topshim/facade.proto b/system/blueberry/facade/topshim/facade.proto index 4d3f8b8625..3e3ac7e4b4 100644 --- a/system/blueberry/facade/topshim/facade.proto +++ b/system/blueberry/facade/topshim/facade.proto @@ -15,12 +15,15 @@ service AdapterService { rpc LeRand(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc SetEventFilterConnectionSetupAllDevices(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc AllowWakeByHid(google.protobuf.Empty) returns (google.protobuf.Empty) {} - rpc RemoveBond(RemoveBondRequest) returns (google.protobuf.Empty) {} rpc RestoreFilterAcceptList(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc SetDefaultEventMask(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc SetEventFilterInquiryResultAllDevices(google.protobuf.Empty) returns (google.protobuf.Empty) {} } +service SecurityService { + rpc RemoveBond(RemoveBondRequest) returns (google.protobuf.Empty) {} +} + service GattService { // Advertiser rpc RegisterAdvertiser(google.protobuf.Empty) returns (google.protobuf.Empty) {} diff --git a/system/blueberry/tests/topshim/adapter/adapter_test.py b/system/blueberry/tests/topshim/adapter/adapter_test.py index acc44c9dcd..811718e4be 100644 --- a/system/blueberry/tests/topshim/adapter/adapter_test.py +++ b/system/blueberry/tests/topshim/adapter/adapter_test.py @@ -14,8 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio - from blueberry.tests.gd.cert.truth import assertThat from blueberry.tests.topshim.lib.topshim_base_test import TopshimBaseTest from blueberry.tests.topshim.lib.adapter_client import AdapterClient @@ -25,15 +23,11 @@ from mobly import test_runner class AdapterTest(TopshimBaseTest): - async def __verify_enable_page_scan(self): - await self.dut_adapter.set_enable_page_scan() - return await self.dut_adapter.le_rand() - def test_verify_adapter_started(self): print("Adapter is verified when test starts") def test_enable_page_scan(self): - self.post(self.__verify_enable_page_scan()) + self.dut().enable_page_scan() if __name__ == "__main__": diff --git a/system/blueberry/tests/topshim/lib/adapter_client.py b/system/blueberry/tests/topshim/lib/adapter_client.py index 303fbbc856..a85e72e746 100644 --- a/system/blueberry/tests/topshim/lib/adapter_client.py +++ b/system/blueberry/tests/topshim/lib/adapter_client.py @@ -19,11 +19,12 @@ import grpc from blueberry.facade.topshim import facade_pb2 from blueberry.facade.topshim import facade_pb2_grpc +from blueberry.tests.topshim.lib.async_closable import AsyncClosable from google.protobuf import empty_pb2 as empty_proto -class AdapterClient(): +class AdapterClient(AsyncClosable): """ Wrapper gRPC interface to the Topshim/BTIF layer """ @@ -39,7 +40,7 @@ class AdapterClient(): self.__adapter_stub = facade_pb2_grpc.AdapterServiceStub(self.__channel) self.__adapter_event_stream = self.__adapter_stub.FetchEvents(facade_pb2.FetchEventsRequest()) - async def terminate(self): + async def close(self): for task in self.__task_list: task.cancel() task = None @@ -75,9 +76,15 @@ class AdapterClient(): await self.__adapter_stub.ToggleStack(facade_pb2.ToggleStackRequest(start_stack=is_start)) return await self._verify_adapter_started() - async def set_enable_page_scan(self): + async def enable_page_scan(self): """Enable page scan (might be used for A2dp sink to be discoverable)""" await self.__adapter_stub.SetDiscoveryMode(facade_pb2.SetDiscoveryModeRequest(enable_page_scan=True)) + return await self.le_rand() + + async def disable_page_scan(self): + """Enable page scan (might be used for A2dp sink to be discoverable)""" + await self.__adapter_stub.SetDiscoveryMode(facade_pb2.SetDiscoveryModeRequest(enable_page_scan=False)) + return await self.le_rand() async def clear_event_filter(self): await self.__adapter_stub.ClearEventFilter(empty_proto.Empty()) @@ -111,9 +118,6 @@ class AdapterClient(): async def allow_wake_by_hid(self): await self.__adapter_stub.AllowWakeByHid(empty_proto.Empty()) - async def remove_bond(self, address): - await self.__adapter_stub.RemoveBond(facade_pb2.RemoveBondRequest(address=address)) - class A2dpAutomationHelper(): """Invoke gRPC on topshim for A2DP testing""" diff --git a/system/blueberry/tests/topshim/lib/async_closable.py b/system/blueberry/tests/topshim/lib/async_closable.py new file mode 100644 index 0000000000..5eea802d68 --- /dev/null +++ b/system/blueberry/tests/topshim/lib/async_closable.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# 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. + +import asyncio +import time +from abc import ABC, abstractmethod +import logging + + +class AsyncClosable(ABC): + + async def __async_exit(self, type=None, value=None, traceback=None): + try: + return await self.close() + except Exception: + logging.warning("Failed to close or already closed") + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + asyncio.run_until_complete(self.__async_exit(type, value, traceback)) + return traceback is None + + def __del__(self): + asyncio.get_event_loop().run_until_complete(self.__async_exit()) + + @abstractmethod + async def close(self): + pass + + +async def asyncSafeClose(closable): + if closable is None: + logging.warn("Tried to close an object that is None") + return + await closable.close() diff --git a/system/blueberry/tests/topshim/lib/gatt_client.py b/system/blueberry/tests/topshim/lib/gatt_client.py index 3e397ef69b..dcd0b8b7a9 100644 --- a/system/blueberry/tests/topshim/lib/gatt_client.py +++ b/system/blueberry/tests/topshim/lib/gatt_client.py @@ -19,11 +19,13 @@ import grpc from blueberry.facade.topshim import facade_pb2 from blueberry.facade.topshim import facade_pb2_grpc +from blueberry.tests.topshim.lib.async_closable import AsyncClosable +from blueberry.tests.topshim.lib.async_closable import asyncSafeClose from google.protobuf import empty_pb2 as empty_proto -class GattClient(): +class GattClient(AsyncClosable): """ Wrapper gRPC interface to the GATT Service """ @@ -39,7 +41,7 @@ class GattClient(): self.__gatt_stub = facade_pb2_grpc.GattServiceStub(self.__channel) #self.__gatt_event_stream = self.__gatt_stub.FetchEvents(facade_pb2.FetchEventsRequest()) - async def terminate(self): + async def close(self): """ Terminate the current tasks """ diff --git a/system/blueberry/tests/topshim/lib/security_client.py b/system/blueberry/tests/topshim/lib/security_client.py new file mode 100644 index 0000000000..9e4ed57700 --- /dev/null +++ b/system/blueberry/tests/topshim/lib/security_client.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# 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. + +import grpc + +from blueberry.facade.topshim import facade_pb2 +from blueberry.facade.topshim import facade_pb2_grpc +from blueberry.tests.topshim.lib.async_closable import AsyncClosable +from blueberry.tests.topshim.lib.async_closable import asyncSafeClose + +from google.protobuf import empty_pb2 as empty_proto + + +class SecurityClient(AsyncClosable): + """ + Wrapper gRPC interface to the GATT Service + """ + # Timeout for async wait + DEFAULT_TIMEOUT = 2 + __task_list = [] + __channel = None + __security_stub = None + __adapter_event_stream = None + __adapter_client = None + + def __init__(self, adapter_client, port=8999): + self.__channel = grpc.aio.insecure_channel("localhost:%d" % port) + self.__security_stub = facade_pb2_grpc.SecurityServiceStub(self.__channel) + self.__adapter_client = adapter_client + #self.__gatt_event_stream = self.__security_stub.FetchEvents(facade_pb2.FetchEventsRequest()) + + async def close(self): + """ + Terminate the current tasks + """ + for task in self.__task_list: + task.cancel() + task = None + self.__task_list.clear() + await self.__channel.close() + + async def remove_bond(self, address): + """ + Removes a bonding entry for a given address + """ + await self.__security_stub.RemoveBond(facade_pb2.RemoveBondRequest(address=address)) + return await self.__adapter_client.le_rand() + + async def bond_using_numeric_comparison(self, address): + """ + Bond to a given address using numeric comparison method + """ + # Set IO Capabilities + # Enable Page scan + # Become discoverable + # Discover device + # Initiate bond + return await self.__adapter_client.le_rand() diff --git a/system/blueberry/tests/topshim/lib/topshim_base_test.py b/system/blueberry/tests/topshim/lib/topshim_base_test.py index 84a348012b..9fe907121a 100644 --- a/system/blueberry/tests/topshim/lib/topshim_base_test.py +++ b/system/blueberry/tests/topshim/lib/topshim_base_test.py @@ -32,7 +32,10 @@ from blueberry.tests.gd.cert.os_utils import TerminalColor from blueberry.tests.gd.cert.tracelogger import TraceLogger from blueberry.tests.gd.cert.truth import assertThat from blueberry.tests.topshim.lib.adapter_client import AdapterClient +from blueberry.tests.topshim.lib.async_closable import asyncSafeClose from blueberry.tests.topshim.lib.gatt_client import GattClient +from blueberry.tests.topshim.lib.security_client import SecurityClient +from blueberry.tests.topshim.lib.topshim_device import TopshimDevice from mobly import asserts from mobly import base_test @@ -157,32 +160,35 @@ def dump_crashes_core(dut, cert, rootcanal_running, rootcanal_process, rootcanal class TopshimBaseTest(base_test.BaseTestClass): - # TODO(optedoblivion): Make TopshimTestStack class - dut_adapter = None - dut_gatt = None + __dut = None + __cert = None - cert_adapter = None - cert_gatt = None - - async def _setup_adapter(self): - self.dut_adapter = AdapterClient(port=self.dut_port) - self.cert_adapter = AdapterClient(port=self.cert_port) - started = await self.dut_adapter._verify_adapter_started() + async def __setup_adapter(self): + dut_adapter = AdapterClient(port=self.dut_port) + cert_adapter = AdapterClient(port=self.cert_port) + started = await dut_adapter._verify_adapter_started() assertThat(started).isTrue() - started = await self.cert_adapter._verify_adapter_started() + started = started and await cert_adapter._verify_adapter_started() assertThat(started).isTrue() - self.dut_gatt = GattClient(port=self.dut_port) - self.cert_gatt = GattClient(port=self.cert_port) + self.__dut = TopshimDevice(dut_adapter, self.dut_port) + self.__cert = TopshimDevice(cert_adapter, self.cert_port) return started - async def _teardown_adapter(self): - await self.dut_adapter.terminate() - await self.dut_gatt.terminate() - await self.cert_adapter.terminate() - await self.cert_gatt.terminate() + async def __teardown_adapter(self): + await asyncSafeClose(self.__dut) + await asyncSafeClose(self.__cert) + + def dut(self): + """ + Get a handle on the DUT device + """ + return self.__dut - def post(self, async_fn): - asyncio.get_event_loop().run_until_complete(async_fn) + def cert(self): + """ + Get a handle on the CERT device + """ + return self.__cert def setup_class(self): """ @@ -217,7 +223,7 @@ class TopshimBaseTest(base_test.BaseTestClass): self.cert_port = controllers[0].grpc_port self.dut_port = controllers[1].grpc_port asyncio.set_event_loop(asyncio.new_event_loop()) - asyncio.get_event_loop().run_until_complete(self._setup_adapter()) + asyncio.get_event_loop().run_until_complete(self.__setup_adapter()) def teardown_class(self): _teardown_class_core( @@ -225,4 +231,4 @@ class TopshimBaseTest(base_test.BaseTestClass): rootcanal_process=self.rootcanal_process, rootcanal_logger=self.rootcanal_logger, subprocess_wait_timeout_seconds=1) - asyncio.get_event_loop().run_until_complete(self._teardown_adapter()) + asyncio.get_event_loop().run_until_complete(self.__teardown_adapter()) diff --git a/system/blueberry/tests/topshim/lib/topshim_device.py b/system/blueberry/tests/topshim/lib/topshim_device.py index 1e853d118e..2ff8043850 100644 --- a/system/blueberry/tests/topshim/lib/topshim_device.py +++ b/system/blueberry/tests/topshim/lib/topshim_device.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# # Copyright 2019 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import asyncio import logging from blueberry.tests.gd.cert.gd_device import GdHostOnlyDevice from blueberry.tests.gd.cert.gd_device import MOBLY_CONTROLLER_CONFIG_NAME from blueberry.tests.gd.cert.os_utils import get_gd_root +from blueberry.tests.topshim.lib.async_closable import AsyncClosable +from blueberry.tests.topshim.lib.async_closable import asyncSafeClose +from blueberry.tests.topshim.lib.gatt_client import GattClient +from blueberry.tests.topshim.lib.security_client import SecurityClient def create(configs): @@ -62,3 +66,89 @@ def get_instances_with_configs(configs): device.setup() devices.append(device) return devices + + +class TopshimDevice(AsyncClosable): + __adapter = None + __gatt = None + __security = None + + async def __le_rand_wrapper(self, async_fn): + await async_fn + + +# await self.__adapter.le_rand() + + def __post(self, async_fn): + asyncio.get_event_loop().run_until_complete(self.__le_rand_wrapper(async_fn)) + + def __init__(self, adapter, grpc_port): + self.__adapter = adapter + self.__gatt = GattClient(port=grpc_port) + self.__security = SecurityClient(adapter, port=grpc_port) + + async def close(self): + """ + Implement abstract method to close out any streams or jobs. + """ + await asyncSafeClose(self.__adapter) + await asyncSafeClose(self.__gatt) + await asyncSafeClose(self.__security) + + def enable_page_scan(self): + self.__post(self.__adapter.enable_page_scan()) + + def disable_page_scan(self): + self.__post(self.__adapter.disable_page_scan()) + + def start_advertising(self): + """ + Starts BLE Advertiser for the stack. + Assumes stack defaults. Which in our case would be RRPA + """ + self.__post(self.__gatt.advertising_enable()) + + def stop_advertising(self): + """ + Stop BLE Advertiser. + """ + self.__post(self.__gatt.advertising_disable()) + + def start_scanning(self): + pass + + def stop_scanning(self): + pass + + def clear_event_mask(self): + self.__post(self.__adapter.clear_event_mask()) + + def clear_event_filter(self): + self.__post(self.__adapter.clear_event_filter()) + + def clear_filter_accept_list(self): + self.__post(self.__adapter.clear_filter_accept_list()) + + def disconnect_all_acls(self): + self.__post(self.__adapter.disconnect_all_acls()) + + def allow_wake_by_hid(self): + self.__post(self.__adapter.allow_wake_by_hid()) + + def set_default_event_mask(self): + self.__post(self.__adapter.set_default_event_mask()) + + def set_event_filter_inquiry_result_all_devices(self): + self.__post(self.__adapter.set_event_filter_inquiry_result_all_devices()) + + def set_event_filter_connection_setup_all_devices(self): + self.__post(self.__adapter.set_event_filter_connection_setup_all_devices()) + + def le_rand(self): + self.__post(self.__adapter.le_rand()) + + def remove_bonded_device(self, address): + """ + Removes a bonding entry for a given address. + """ + self.__post(self.__security.remove_bond(address)) diff --git a/system/blueberry/tests/topshim/power/suspend_test.py b/system/blueberry/tests/topshim/power/suspend_test.py index 51b8b7c0f6..79f09004c9 100644 --- a/system/blueberry/tests/topshim/power/suspend_test.py +++ b/system/blueberry/tests/topshim/power/suspend_test.py @@ -23,79 +23,79 @@ from mobly import test_runner class SuspendTest(TopshimBaseTest): - async def __verify_no_wake_suspend(self): + def __verify_no_wake_suspend(self): # Start suspend work - await self.dut_adapter.clear_event_mask() - await self.dut_adapter.clear_event_filter() - await self.dut_adapter.clear_filter_accept_list() - await self.dut_gatt.advertising_disable() - await self.dut_gatt.stop_scan() - await self.dut_adapter.disconnect_all_acls() - return await self.dut_adapter.le_rand() - - async def __verify_no_wake_resume(self): + self.dut().clear_event_mask() + self.dut().clear_event_filter() + self.dut().clear_filter_accept_list() + self.dut().stop_advertising() + self.dut().stop_scanning() + self.dut().disconnect_all_acls() + self.dut().le_rand() + + def __verify_no_wake_resume(self): # Start resume work - await self.dut_adapter.set_default_event_mask() - await self.dut_adapter.set_event_filter_inquiry_result_all_devices() - await self.dut_adapter.set_event_filter_connection_setup_all_devices() - return await self.dut_adapter.le_rand() - - async def __verify_wakeful_suspend(self, is_a2dp_connected): - await self.dut_adapter.clear_event_mask() - await self.dut_adapter.clear_event_filter() - await self.dut_adapter.clear_filter_accept_list() - await self.dut_gatt.advertising_disable() - await self.dut_gatt.stop_scan() + self.dut().set_default_event_mask() + self.dut().set_event_filter_inquiry_result_all_devices() + self.dut().set_event_filter_connection_setup_all_devices() + self.dut().le_rand() + + def __verify_wakeful_suspend(self, is_a2dp_connected): + self.dut().clear_event_mask() + self.dut().clear_event_filter() + self.dut().clear_filter_accept_list() + self.dut().stop_advertising() + self.dut().stop_scanning() if is_a2dp_connected: - # await self.media_server.disconnect_a2dp() + # self.media_server.disconnect_a2dp() pass - await self.dut_adapter.disconnect_all_acls() - await self.dut_adapter.allow_wake_by_hid() - return await self.dut_adapter.le_rand() + self.dut().disconnect_all_acls() + self.dut().allow_wake_by_hid() + self.dut().le_rand() - async def __verify_wakeful_resume(self, was_a2dp_connected): + def __verify_wakeful_resume(self, was_a2dp_connected): # Start resume work - await self.dut_adapter.set_default_event_mask() - await self.dut_adapter.set_event_filter_inquiry_result_all_devices() - await self.dut_adapter.set_event_filter_connection_setup_all_devices() + self.dut().set_default_event_mask() + self.dut().set_event_filter_inquiry_result_all_devices() + self.dut().set_event_filter_connection_setup_all_devices() if was_a2dp_connected: # restore filter accept list? - await self.dut_adapter.restore_filter_accept_list() + self.dut().restore_filter_accept_list() # reconnect a2dp - # await self.media_server.reconnect_last_a2dp() - # await self.gatt.restart_all_previous_advertising() - await self.dut_gatt.advertising_enable() - return await self.dut_adapter.le_rand() + # self.media_server.reconnect_last_a2dp() + # self.gatt.restart_all_previous_advertising() + self.dut().start_advertising() + self.dut().le_rand() def test_no_wake_suspend(self): - self.post(self.__verify_no_wake_suspend()) + self.__verify_no_wake_suspend() def test_no_wake_resume(self): - self.post(self.__verify_no_wake_resume()) + self.__verify_no_wake_resume() def test_no_wake_suspend_then_resume(self): - self.post(self.__verify_no_wake_suspend()) - self.post(self.__verify_no_wake_resume()) + self.__verify_no_wake_suspend() + self.__verify_no_wake_resume() def test_no_wake_suspend_then_resume_then_suspend(self): - self.post(self.__verify_no_wake_suspend()) - self.post(self.__verify_no_wake_resume()) - self.post(self.__verify_no_wake_suspend()) + self.__verify_no_wake_suspend() + self.__verify_no_wake_resume() + self.__verify_no_wake_suspend() def test_wakeful_suspend_no_a2dp(self): - self.post(self.__verify_wakeful_suspend(False)) + self.__verify_wakeful_suspend(False) def test_wakeful_resume_no_a2dp(self): - self.post(self.__verify_wakeful_resume(False)) + self.__verify_wakeful_resume(False) def test_wakeful_suspend_then_resume_no_a2dp(self): - self.post(self.__verify_wakeful_suspend(False)) - self.post(self.__verify_wakeful_resume(False)) + self.__verify_wakeful_suspend(False) + self.__verify_wakeful_resume(False) def test_wakeful_suspend_then_resume_then_suspend_no_a2dp(self): - self.post(self.__verify_wakeful_suspend(False)) - self.post(self.__verify_wakeful_resume(False)) - self.post(self.__verify_wakeful_suspend(False)) + self.__verify_wakeful_suspend(False) + self.__verify_wakeful_resume(False) + self.__verify_wakeful_suspend(False) if __name__ == "__main__": diff --git a/system/blueberry/tests/topshim/security/security_test.py b/system/blueberry/tests/topshim/security/classic_security_test.py index 95ce05d17c..a34bd918a0 100644 --- a/system/blueberry/tests/topshim/security/security_test.py +++ b/system/blueberry/tests/topshim/security/classic_security_test.py @@ -21,16 +21,13 @@ from blueberry.tests.topshim.lib.adapter_client import AdapterClient from mobly import test_runner -class SecurityTest(TopshimBaseTest): +class ClassicSecurityTest(TopshimBaseTest): DEFAULT_ADDRESS = "01:02:03:04:05:06" - async def __test_remove_bond(self, address): - await self.dut_adapter.remove_bond(address) - return await self.dut_adapter.le_rand() - def test_remove_bond_with_no_bonded_devices(self): - self.post(self.__test_remove_bond(self.DEFAULT_ADDRESS)) + self.dut().remove_bonded_device(self.DEFAULT_ADDRESS) + self.dut().le_rand() if __name__ == "__main__": diff --git a/system/blueberry/tests/topshim/topshim_test_runner.py b/system/blueberry/tests/topshim/topshim_test_runner.py index 7b914f9878..64152c684f 100644 --- a/system/blueberry/tests/topshim/topshim_test_runner.py +++ b/system/blueberry/tests/topshim/topshim_test_runner.py @@ -16,12 +16,12 @@ from blueberry.tests.topshim.adapter.adapter_test import AdapterTest from blueberry.tests.topshim.power.suspend_test import SuspendTest -from blueberry.tests.topshim.security.security_test import SecurityTest +from blueberry.tests.topshim.security.classic_security_test import ClassicSecurityTest from mobly import suite_runner import argparse -ALL_TESTS = [AdapterTest, SecurityTest, SuspendTest] +ALL_TESTS = [AdapterTest, ClassicSecurityTest, SuspendTest] def main(): diff --git a/system/bta/dm/bta_dm_act.cc b/system/bta/dm/bta_dm_act.cc index f91b593dde..65389ad1ff 100644 --- a/system/bta/dm/bta_dm_act.cc +++ b/system/bta/dm/bta_dm_act.cc @@ -670,13 +670,6 @@ void bta_dm_remove_device(const RawAddress& bd_addr) { if (!other_address_connected && !other_address.IsEmpty()) { bta_dm_process_remove_device(other_address); } - - /* Check the length of the paired devices, and if 0 then reset IRK */ - auto paired_devices = btif_config_get_paired_devices(); - if (paired_devices.empty()) { - LOG_INFO("Last paired device removed, resetting IRK"); - btm_ble_reset_id(); - } } /******************************************************************************* @@ -4221,6 +4214,20 @@ void bta_dm_set_event_filter_inquiry_result_all_devices() { /******************************************************************************* * + * Function bta_dm_ble_reset_id + * + * Description Reset the local adapter BLE keys. + * + * Parameters: + * + ******************************************************************************/ +void bta_dm_ble_reset_id(void) { + VLOG(1) << "bta_dm_ble_reset_id in bta_dm_act"; + bluetooth::shim::BTM_BleResetId(); +} + +/******************************************************************************* + * * Function bta_dm_gattc_callback * * Description This is GATT client callback function used in DM. diff --git a/system/bta/dm/bta_dm_api.cc b/system/bta/dm/bta_dm_api.cc index 347abcff3a..01b497c367 100644 --- a/system/bta/dm/bta_dm_api.cc +++ b/system/bta/dm/bta_dm_api.cc @@ -719,3 +719,17 @@ void BTA_DmSetEventFilterInquiryResultAllDevices() { FROM_HERE, base::Bind(bta_dm_set_event_filter_inquiry_result_all_devices)); } + +/******************************************************************************* + * + * Function BTA_DmBleResetId + * + * Description This function resets the ble keys such as IRK + * + * Returns void + * + ******************************************************************************/ +void BTA_DmBleResetId(void) { + APPL_TRACE_API("BTA_DmBleResetId"); + do_in_main_thread(FROM_HERE, base::Bind(bta_dm_ble_reset_id)); +} diff --git a/system/bta/dm/bta_dm_int.h b/system/bta/dm/bta_dm_int.h index f02441c886..2491af1d57 100644 --- a/system/bta/dm/bta_dm_int.h +++ b/system/bta/dm/bta_dm_int.h @@ -552,6 +552,8 @@ extern void bta_dm_restore_filter_accept_list(); extern void bta_dm_set_default_event_mask(); extern void bta_dm_set_event_filter_inquiry_result_all_devices(); +extern void bta_dm_ble_reset_id(void); + uint8_t bta_dm_search_get_state(); void bta_dm_search_set_state(uint8_t state); diff --git a/system/bta/gatt/bta_gattc_act.cc b/system/bta/gatt/bta_gattc_act.cc index 9de15c9422..ecb9a87259 100644 --- a/system/bta/gatt/bta_gattc_act.cc +++ b/system/bta/gatt/bta_gattc_act.cc @@ -768,6 +768,26 @@ void bta_gattc_start_discover(tBTA_GATTC_CLCB* p_clcb, p_clcb->p_srcb->update_count = 0; p_clcb->p_srcb->state = BTA_GATTC_SERV_DISC_ACT; + /* This is workaround for the embedded devices being already on the market + * and having a serious problem with handle Read By Type with + * GATT_UUID_DATABASE_HASH. With this workaround, Android will assume that + * embedded device having LMP version lower than 5.1 (0x0a), it does not + * support GATT Caching. + */ + uint8_t lmp_version = 0; + if (!BTM_ReadRemoteVersion(p_clcb->bda, &lmp_version, nullptr, nullptr)) { + LOG_WARN("Could not read remote version for %s", + p_clcb->bda.ToString().c_str()); + } + + if (lmp_version < 0x0a) { + LOG_WARN( + " Device LMP version 0x%02x < Bluetooth 5.1. Ignore database cache " + "read.", + lmp_version); + p_clcb->p_srcb->srvc_hdl_db_hash = false; + } + /* read db hash if db hash characteristic exists */ if (bta_gattc_is_robust_caching_enabled() && p_clcb->p_srcb->srvc_hdl_db_hash && diff --git a/system/bta/gatt/bta_gattc_utils.cc b/system/bta/gatt/bta_gattc_utils.cc index f49ae36e3c..1412702180 100644 --- a/system/bta/gatt/bta_gattc_utils.cc +++ b/system/bta/gatt/bta_gattc_utils.cc @@ -677,5 +677,5 @@ tBTA_GATTC_CLCB* bta_gattc_find_int_disconn_clcb(tBTA_GATTC_DATA* p_msg) { * ******************************************************************************/ bool bta_gattc_is_robust_caching_enabled() { - return bluetooth::common::init_flags::gatt_robust_caching_is_enabled(); + return bluetooth::common::init_flags::gatt_robust_caching_client_is_enabled(); } diff --git a/system/bta/hf_client/bta_hf_client_rfc.cc b/system/bta/hf_client/bta_hf_client_rfc.cc index d964003b57..2999eb9ed1 100644 --- a/system/bta/hf_client/bta_hf_client_rfc.cc +++ b/system/bta/hf_client/bta_hf_client_rfc.cc @@ -116,6 +116,7 @@ static void bta_hf_client_mgmt_cback(uint32_t code, uint16_t port_handle) { if (client_cb == NULL) { APPL_TRACE_ERROR("%s: error allocating a new handle", __func__); p_buf->hdr.event = BTA_HF_CLIENT_RFC_CLOSE_EVT; + RFCOMM_RemoveConnection(port_handle); } else { // Set the connection fields for this new CB client_cb->conn_handle = port_handle; diff --git a/system/bta/include/bta_api.h b/system/bta/include/bta_api.h index c140247b74..f7cad22c53 100644 --- a/system/bta/include/bta_api.h +++ b/system/bta/include/bta_api.h @@ -1289,4 +1289,15 @@ extern void BTA_DmSetDefaultEventMask(); *******************************************************************************/ extern void BTA_DmSetEventFilterInquiryResultAllDevices(); +/******************************************************************************* + * + * Function BTA_DmBleResetId + * + * Description This function resets the ble keys such as IRK + * + * Returns void + * + ******************************************************************************/ +extern void BTA_DmBleResetId(void); + #endif /* BTA_API_H */ diff --git a/system/bta/le_audio/client.cc b/system/bta/le_audio/client.cc index ccc68bc3e6..7467ca9ec2 100644 --- a/system/bta/le_audio/client.cc +++ b/system/bta/le_audio/client.cc @@ -240,7 +240,8 @@ class LeAudioClientImpl : public LeAudioClient { lc3_decoder_right(nullptr), audio_source_instance_(nullptr), audio_sink_instance_(nullptr), - suspend_timeout_(alarm_new("LeAudioSuspendTimeout")) { + suspend_timeout_(alarm_new("LeAudioSuspendTimeout")), + disable_timer_(alarm_new("LeAudioDisableTimer")) { LeAudioGroupStateMachine::Initialize(state_machine_callbacks_); groupStateMachine_ = LeAudioGroupStateMachine::Get(); @@ -634,11 +635,72 @@ class LeAudioClientImpl : public LeAudioClient { group_remove_node(group, address, true); } + AudioContexts adjustMetadataContexts(AudioContexts metadata_context_type) { + /* This function takes already filtered contexts which we are plannig to use + * in the Enable or UpdateMetadata command. + * Note we are not changing stream configuration here, but just the list of + * the contexts in the Metadata which will be provide to remote side. + * Ideally, we should send all the bits we have, but not all headsets like + * it. + */ + if (osi_property_get_bool(kAllowMultipleContextsInMetadata, false)) { + return metadata_context_type; + } + + LOG_DEBUG("Converting to single context type: %lu", + metadata_context_type.to_ulong()); + + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::CONVERSATIONAL)) { + return static_cast<uint16_t>(LeAudioContextType::CONVERSATIONAL); + } + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::GAME)) { + return static_cast<uint16_t>(LeAudioContextType::GAME); + } + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::EMERGENCYALARM)) { + return static_cast<uint16_t>(LeAudioContextType::EMERGENCYALARM); + } + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::ALERTS)) { + return static_cast<uint16_t>(LeAudioContextType::ALERTS); + } + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::RINGTONE)) { + return static_cast<uint16_t>(LeAudioContextType::RINGTONE); + } + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::VOICEASSISTANTS)) { + return static_cast<uint16_t>(LeAudioContextType::VOICEASSISTANTS); + } + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::INSTRUCTIONAL)) { + return static_cast<uint16_t>(LeAudioContextType::INSTRUCTIONAL); + } + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::NOTIFICATIONS)) { + return static_cast<uint16_t>(LeAudioContextType::NOTIFICATIONS); + } + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::LIVE)) { + return static_cast<uint16_t>(LeAudioContextType::LIVE); + } + if (metadata_context_type.to_ulong() & + static_cast<uint16_t>(LeAudioContextType::MEDIA)) { + return static_cast<uint16_t>(LeAudioContextType::MEDIA); + } + + return static_cast<uint16_t>(LeAudioContextType::UNSPECIFIED); + } + 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; + auto adjusted_metadata_context_type = + adjustMetadataContexts(metadata_context_type); DLOG(INFO) << __func__; if (context_type >= static_cast<uint16_t>(LeAudioContextType::RFU)) { LOG(ERROR) << __func__ << ", stream context type is not supported: " @@ -679,7 +741,8 @@ class LeAudioClientImpl : public LeAudioClient { bool result = groupStateMachine_->StartStream( group, static_cast<LeAudioContextType>(final_context_type), - metadata_context_type, GetAllCcids(metadata_context_type)); + adjusted_metadata_context_type, + GetAllCcids(adjusted_metadata_context_type)); if (result) stream_setup_start_timestamp_ = bluetooth::common::time_get_os_boottime_us(); @@ -833,15 +896,16 @@ class LeAudioClientImpl : public LeAudioClient { return; } + auto group_id_to_close = active_group_id_; + active_group_id_ = bluetooth::groups::kGroupUnknown; + if (alarm_is_scheduled(suspend_timeout_)) alarm_cancel(suspend_timeout_); StopAudio(); ClientAudioIntefraceRelease(); - GroupStop(active_group_id_); - callbacks_->OnGroupStatus(active_group_id_, GroupStatus::INACTIVE); - active_group_id_ = group_id; - + GroupStop(group_id_to_close); + callbacks_->OnGroupStatus(group_id_to_close, GroupStatus::INACTIVE); return; } @@ -1144,6 +1208,7 @@ class LeAudioClientImpl : public LeAudioClient { BtaGattQueue::Clean(leAudioDevice->conn_id_); BTA_GATTC_Close(leAudioDevice->conn_id_); leAudioDevice->conn_id_ = GATT_INVALID_CONN_ID; + leAudioDevice->mtu_ = 0; } void DeregisterNotifications(LeAudioDevice* leAudioDevice) { @@ -1464,11 +1529,7 @@ class LeAudioClientImpl : public LeAudioClient { leAudioDevice->connecting_actively_ = false; leAudioDevice->conn_id_ = conn_id; - - if (mtu == GATT_DEF_BLE_MTU_SIZE) { - LOG(INFO) << __func__ << ", Configure MTU"; - BtaGattQueue::ConfigureMtu(leAudioDevice->conn_id_, 240); - } + leAudioDevice->mtu_ = mtu; if (BTM_SecIsSecurityPending(address)) { /* if security collision happened, wait for encryption done @@ -1506,6 +1567,14 @@ class LeAudioClientImpl : public LeAudioClient { void RegisterKnownNotifications(LeAudioDevice* leAudioDevice) { LOG(INFO) << __func__ << " device: " << leAudioDevice->address_; + if (leAudioDevice->ctp_hdls_.val_hdl == 0) { + LOG_ERROR( + "Control point characteristic is mandatory - disconnecting device %s", + leAudioDevice->address_.ToString().c_str()); + DisconnectDevice(leAudioDevice); + return; + } + /* GATTC will ommit not registered previously handles */ for (auto pac_tuple : leAudioDevice->snk_pacs_) { subscribe_for_notification(leAudioDevice->conn_id_, @@ -1537,14 +1606,19 @@ class LeAudioClientImpl : public LeAudioClient { leAudioDevice->address_, leAudioDevice->audio_supp_cont_hdls_); - if (leAudioDevice->ctp_hdls_.val_hdl != 0) - subscribe_for_notification(leAudioDevice->conn_id_, - leAudioDevice->address_, - leAudioDevice->ctp_hdls_); - for (struct ase& ase : leAudioDevice->ases_) subscribe_for_notification(leAudioDevice->conn_id_, leAudioDevice->address_, ase.hdls); + + subscribe_for_notification(leAudioDevice->conn_id_, leAudioDevice->address_, + leAudioDevice->ctp_hdls_); + } + + void changeMtuIfPossible(LeAudioDevice* leAudioDevice) { + if (leAudioDevice->mtu_ == GATT_DEF_BLE_MTU_SIZE) { + LOG(INFO) << __func__ << ", Configure MTU"; + BtaGattQueue::ConfigureMtu(leAudioDevice->conn_id_, GATT_MAX_MTU_SIZE); + } } void OnEncryptionComplete(const RawAddress& address, uint8_t status) { @@ -1569,6 +1643,8 @@ class LeAudioClientImpl : public LeAudioClient { return; } + changeMtuIfPossible(leAudioDevice); + /* If we know services, register for notifications */ if (leAudioDevice->known_service_handles_) RegisterKnownNotifications(leAudioDevice); @@ -1584,7 +1660,7 @@ class LeAudioClientImpl : public LeAudioClient { * just notify connected */ if (leAudioDevice->known_service_handles_ && !leAudioDevice->notify_connected_after_read_) { - connectionReady(leAudioDevice); + LOG_INFO("Wait for CCC registration and MTU change request"); return; } @@ -1610,6 +1686,7 @@ class LeAudioClientImpl : public LeAudioClient { callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, address); leAudioDevice->conn_id_ = GATT_INVALID_CONN_ID; + leAudioDevice->mtu_ = 0; leAudioDevice->closing_stream_for_disconnection_ = false; leAudioDevice->encrypted_ = false; @@ -1687,6 +1764,16 @@ class LeAudioClientImpl : public LeAudioClient { DeregisterNotifications(leAudioDevice); } + void OnMtuChanged(uint16_t conn_id, uint16_t mtu) { + LeAudioDevice* leAudioDevice = leAudioDevices_.FindByConnId(conn_id); + if (!leAudioDevice) { + LOG_DEBUG("Unknown connectect id %d", conn_id); + return; + } + + leAudioDevice->mtu_ = mtu; + } + void OnGattServiceDiscoveryDone(const RawAddress& address) { LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address); if (!leAudioDevice) { @@ -2075,6 +2162,15 @@ class LeAudioClientImpl : public LeAudioClient { if (status == GATT_SUCCESS) { LOG(INFO) << __func__ << ", successfully registered on ccc: " << loghex(hdl); + + if (leAudioDevice->ctp_hdls_.ccc_hdl == hdl && + leAudioDevice->known_service_handles_ && + !leAudioDevice->notify_connected_after_read_) { + /* Reconnection case. Control point is the last CCC LeAudio is + * registering for on reconnection */ + connectionReady(leAudioDevice); + } + return; } @@ -2898,13 +2994,31 @@ class LeAudioClientImpl : public LeAudioClient { return; } + if (stack_config_get_interface() + ->get_pts_le_audio_disable_ases_before_stopping()) { + LOG_INFO("Stream disable_timer_ started"); + if (alarm_is_scheduled(disable_timer_)) alarm_cancel(disable_timer_); + + alarm_set_on_mloop( + disable_timer_, kAudioDisableTimeoutMs, + [](void* data) { + if (instance) instance->GroupSuspend(PTR_TO_INT(data)); + }, + INT_TO_PTR(active_group_id_)); + } + /* Group should tie in time to get requested status */ uint64_t timeoutMs = kAudioSuspentKeepIsoAliveTimeoutMs; timeoutMs = osi_property_get_int32(kAudioSuspentKeepIsoAliveTimeoutMsProp, timeoutMs); - DLOG(INFO) << __func__ - << " Stream suspend_timeout_ started: " << suspend_timeout_; + if (stack_config_get_interface() + ->get_pts_le_audio_disable_ases_before_stopping()) { + timeoutMs += kAudioDisableTimeoutMs; + } + + LOG_DEBUG("Stream suspend_timeout_ started: %d ms", + static_cast<int>(timeoutMs)); if (alarm_is_scheduled(suspend_timeout_)) alarm_cancel(suspend_timeout_); alarm_set_on_mloop( @@ -2916,9 +3030,9 @@ class LeAudioClientImpl : public LeAudioClient { } void OnAudioSinkSuspend() { - DLOG(INFO) << __func__ - << " IN: audio_receiver_state_: " << audio_receiver_state_ - << " audio_sender_state_: " << audio_sender_state_; + LOG_DEBUG(" IN: audio_receiver_state_: %s, audio_sender_state_: %s", + ToString(audio_receiver_state_).c_str(), + ToString(audio_sender_state_).c_str()); /* Note: This callback is from audio hal driver. * Bluetooth peer is a Sink for Audio Framework. @@ -3060,9 +3174,9 @@ class LeAudioClientImpl : public LeAudioClient { } void OnAudioSourceSuspend() { - DLOG(INFO) << __func__ - << " IN: audio_receiver_state_: " << audio_receiver_state_ - << " audio_sender_state_: " << audio_sender_state_; + LOG_DEBUG(" IN: audio_receiver_state_: %s, audio_sender_state_: %s", + ToString(audio_receiver_state_).c_str(), + ToString(audio_sender_state_).c_str()); /* Note: This callback is from audio hal driver. * Bluetooth peer is a Source for Audio Framework. @@ -3223,20 +3337,22 @@ class LeAudioClientImpl : public LeAudioClient { return LeAudioContextType::UNSPECIFIED; } + auto adjusted_contexts = adjustMetadataContexts(available_contexts); + using T = std::underlying_type<LeAudioContextType>::type; /* Mini policy. Voice is prio 1, game prio 2, media is prio 3 */ - if ((available_contexts & + if ((adjusted_contexts & AudioContexts(static_cast<T>(LeAudioContextType::CONVERSATIONAL))) .any()) return LeAudioContextType::CONVERSATIONAL; - if ((available_contexts & + if ((adjusted_contexts & AudioContexts(static_cast<T>(LeAudioContextType::GAME))) .any()) return LeAudioContextType::GAME; - if ((available_contexts & + if ((adjusted_contexts & AudioContexts(static_cast<T>(LeAudioContextType::RINGTONE))) .any()) { if (!in_call_) { @@ -3245,7 +3361,7 @@ class LeAudioClientImpl : public LeAudioClient { return LeAudioContextType::RINGTONE; } - if ((available_contexts & + if ((adjusted_contexts & AudioContexts(static_cast<T>(LeAudioContextType::MEDIA))) .any()) return LeAudioContextType::MEDIA; @@ -3253,8 +3369,8 @@ class LeAudioClientImpl : public LeAudioClient { /*TODO do something smarter here */ /* Get context for the first non-zero bit */ uint16_t context_type = 0b1; - while (available_contexts != 0b1) { - available_contexts = available_contexts >> 1; + while (adjusted_contexts != 0b1) { + adjusted_contexts = adjusted_contexts >> 1; context_type = context_type << 1; } @@ -3311,7 +3427,14 @@ class LeAudioClientImpl : public LeAudioClient { bool is_group_streaming = (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); - metadata_context_types_ = GetAllowedAudioContextsFromSourceMetadata( + if (audio_receiver_state_ == AudioState::STARTED) { + /* If the receiver is starte. Take into account current context type */ + metadata_context_types_ = adjustMetadataContexts(metadata_context_types_); + } else { + metadata_context_types_ = 0; + } + + metadata_context_types_ |= GetAllowedAudioContextsFromSourceMetadata( source_metadata, group->GetActiveContexts()); if (stack_config_get_interface() @@ -3675,6 +3798,20 @@ class LeAudioClientImpl : public LeAudioClient { } } + void NotifyUpperLayerGroupTurnedIdleDuringCall(int group_id) { + if (!osi_property_get_bool(kNotifyUpperLayerAboutGroupBeingInIdleDuringCall, + false)) { + return; + } + /* If group is inactive, phone is in call and Group is not having CIS + * connected, notify upper layer about it, so it can decide to create SCO if + * it is in the handover case + */ + if (in_call_ && active_group_id_ == bluetooth::groups::kGroupUnknown) { + callbacks_->OnGroupStatus(group_id, GroupStatus::TURNED_IDLE_DURING_CALL); + } + } + void StatusReportCb(int group_id, GroupStreamStatus status) { LOG_INFO("status: %d , audio_sender_state %s, audio_receiver_state %s", static_cast<int>(status), @@ -3736,15 +3873,19 @@ class LeAudioClientImpl : public LeAudioClient { stream_setup_start_timestamp_ = 0; if (group && group->IsPendingConfiguration()) { SuspendedForReconfiguration(); + auto adjusted_metedata_context_type = + adjustMetadataContexts(metadata_context_types_); if (groupStateMachine_->ConfigureStream( - group, configuration_context_type_, metadata_context_types_, - GetAllCcids(metadata_context_types_))) { + group, configuration_context_type_, + adjusted_metedata_context_type, + GetAllCcids(adjusted_metedata_context_type))) { /* If configuration succeed wait for new status. */ return; } } CancelStreamingRequest(); if (group) { + NotifyUpperLayerGroupTurnedIdleDuringCall(group->group_id_); HandlePendingAvailableContexts(group); HandlePendingDeviceDisconnection(group); } @@ -3772,6 +3913,8 @@ class LeAudioClientImpl : public LeAudioClient { LeAudioGroupStateMachine* groupStateMachine_; int active_group_id_; LeAudioContextType configuration_context_type_; + static constexpr char kAllowMultipleContextsInMetadata[] = + "persist.bluetooth.leaudio.allow.multiple.contexts"; AudioContexts metadata_context_types_; uint64_t stream_setup_start_timestamp_; uint64_t stream_setup_end_timestamp_; @@ -3782,6 +3925,8 @@ class LeAudioClientImpl : public LeAudioClient { AudioState audio_sender_state_; /* Keep in call state. */ bool in_call_; + static constexpr char kNotifyUpperLayerAboutGroupBeingInIdleDuringCall[] = + "persist.bluetooth.leaudio.notify.idle.during.call"; /* Current stream configuration */ LeAudioCodecConfiguration current_source_codec_config; @@ -3820,9 +3965,11 @@ class LeAudioClientImpl : public LeAudioClient { const void* audio_source_instance_; const void* audio_sink_instance_; static constexpr uint64_t kAudioSuspentKeepIsoAliveTimeoutMs = 5000; + static constexpr uint64_t kAudioDisableTimeoutMs = 3000; static constexpr char kAudioSuspentKeepIsoAliveTimeoutMsProp[] = "persist.bluetooth.leaudio.audio.suspend.timeoutms"; alarm_t* suspend_timeout_; + alarm_t* disable_timer_; static constexpr uint64_t kDeviceAttachDelayMs = 500; std::vector<int16_t> cached_channel_data_; @@ -3903,6 +4050,7 @@ void le_audio_gattc_callback(tBTA_GATTC_EVT event, tBTA_GATTC* p_data) { instance->OnServiceChangeEvent(p_data->remote_bda); break; case BTA_GATTC_CFG_MTU_EVT: + instance->OnMtuChanged(p_data->cfg_mtu.conn_id, p_data->cfg_mtu.mtu); break; default: diff --git a/system/bta/le_audio/client_audio.h b/system/bta/le_audio/client_audio.h index 4da151b710..28c913e475 100644 --- a/system/bta/le_audio/client_audio.h +++ b/system/bta/le_audio/client_audio.h @@ -134,7 +134,7 @@ class LeAudioClientAudioSource { const void* Acquire(bool is_broadcasting_session_type); bool InitAudioSinkThread(const std::string name); - bluetooth::common::MessageLoopThread* worker_thread_; + bluetooth::common::MessageLoopThread* worker_thread_ = nullptr; private: bool SinkOnResumeReq(bool start_media_task); @@ -147,9 +147,9 @@ class LeAudioClientAudioSource { bluetooth::common::RepeatingTimer audio_timer_; LeAudioCodecConfiguration source_codec_config_; - LeAudioClientAudioSinkReceiver* audioSinkReceiver_; + LeAudioClientAudioSinkReceiver* audioSinkReceiver_ = nullptr; bluetooth::audio::le_audio::LeAudioClientInterface::Sink* - sinkClientInterface_; + sinkClientInterface_ = nullptr; /* Guard audio sink receiver mutual access from stack with internal mutex */ std::mutex sinkInterfaceMutex_; @@ -180,9 +180,9 @@ class LeAudioUnicastClientAudioSink { bool SourceOnSuspendReq(); bool SourceOnMetadataUpdateReq(const sink_metadata_t& sink_metadata); - LeAudioClientAudioSourceReceiver* audioSourceReceiver_; + LeAudioClientAudioSourceReceiver* audioSourceReceiver_ = nullptr; bluetooth::audio::le_audio::LeAudioClientInterface::Source* - sourceClientInterface_; + sourceClientInterface_ = nullptr; }; class LeAudioUnicastClientAudioSource : public LeAudioClientAudioSource { diff --git a/system/bta/le_audio/devices.cc b/system/bta/le_audio/devices.cc index 1541ca7068..4e4283d8dd 100644 --- a/system/bta/le_audio/devices.cc +++ b/system/bta/le_audio/devices.cc @@ -200,18 +200,22 @@ void LeAudioDeviceGroup::SetCigState(le_audio::types::CigState state) { } bool LeAudioDeviceGroup::Activate(LeAudioContextType context_type) { + bool is_activate = false; for (auto leAudioDevice : leAudioDevices_) { if (leAudioDevice.expired()) continue; bool activated = leAudioDevice.lock()->ActivateConfiguredAses(context_type); - LOG_INFO("Device %s is activated now: ", - leAudioDevice.lock().get()->address_.ToString().c_str()); + LOG_INFO("Device %s is %s", + leAudioDevice.lock().get()->address_.ToString().c_str(), + activated ? "activated" : " not activated"); if (activated) { - if (!CigAssignCisIds(leAudioDevice.lock().get())) return false; + if (!CigAssignCisIds(leAudioDevice.lock().get())) { + return false; + } + is_activate = true; } } - - return true; + return is_activate; } LeAudioDevice* LeAudioDeviceGroup::GetFirstDevice(void) { @@ -406,17 +410,6 @@ LeAudioDevice* LeAudioDeviceGroup::GetNextActiveDeviceByDataPathState( return iter->lock().get(); } -bool LeAudioDeviceGroup::SetContextType(LeAudioContextType context_type) { - /* XXX: group context policy ? / may it disallow to change type ?) */ - context_type_ = context_type; - - return true; -} - -LeAudioContextType LeAudioDeviceGroup::GetContextType(void) { - return context_type_; -} - uint32_t LeAudioDeviceGroup::GetSduInterval(uint8_t direction) { for (LeAudioDevice* leAudioDevice = GetFirstActiveDevice(); leAudioDevice != nullptr; @@ -1575,9 +1568,6 @@ const set_configurations::AudioSetConfiguration* LeAudioDeviceGroup::GetActiveConfiguration(void) { return active_context_to_configuration_map[active_context_type_]; } -AudioContexts LeAudioDeviceGroup::GetActiveContexts(void) { - return active_contexts_mask_; -} std::optional<LeAudioCodecConfiguration> LeAudioDeviceGroup::GetCodecConfigurationByDirection( @@ -1773,10 +1763,6 @@ void LeAudioDeviceGroup::CreateStreamVectorForOffloader(uint8_t direction) { } } -types::LeAudioContextType LeAudioDeviceGroup::GetCurrentContextType(void) { - return active_context_type_; -} - bool LeAudioDeviceGroup::IsPendingConfiguration(void) { return stream_conf.pending_configuration; } @@ -2431,8 +2417,7 @@ bool LeAudioDevice::ActivateConfiguredAses(LeAudioContextType context_type) { LOG_INFO(" Configuring device %s", address_.ToString().c_str()); for (auto& ase : ases_) { - if (!ase.active && - ase.state == AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED && + if (ase.state == AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED && ase.configured_for_context_type == context_type) { LOG_INFO( " conn_id: %d, ase id %d, cis id %d, cis_handle 0x%04x is activated.", @@ -2446,14 +2431,13 @@ bool LeAudioDevice::ActivateConfiguredAses(LeAudioContextType context_type) { } void LeAudioDevice::DeactivateAllAses(void) { - /* Just clear states and keep previous configuration for use - * in case device will get reconnected - */ for (auto& ase : ases_) { if (ase.active) { ase.state = AseState::BTA_LE_AUDIO_ASE_STATE_IDLE; ase.data_path_state = AudioStreamDataPathState::IDLE; ase.active = false; + ase.cis_id = le_audio::kInvalidCisId; + ase.cis_conn_hdl = 0; } } } @@ -2636,7 +2620,11 @@ void LeAudioDevices::Dump(int fd, int group_id) { void LeAudioDevices::Cleanup(void) { for (auto const& device : leAudioDevices_) { - device->DisconnectAcl(); + if (device->conn_id_ != GATT_INVALID_CONN_ID) { + BtaGattQueue::Clean(device->conn_id_); + BTA_GATTC_Close(device->conn_id_); + device->DisconnectAcl(); + } } leAudioDevices_.clear(); } diff --git a/system/bta/le_audio/devices.h b/system/bta/le_audio/devices.h index 29cb9725bc..cd5a0fe8a3 100644 --- a/system/bta/le_audio/devices.h +++ b/system/bta/le_audio/devices.h @@ -60,6 +60,7 @@ class LeAudioDevice { bool connecting_actively_; bool closing_stream_for_disconnection_; uint16_t conn_id_; + uint16_t mtu_; bool encrypted_; int group_id_; bool csis_member_; @@ -93,6 +94,7 @@ class LeAudioDevice { connecting_actively_(first_connection), closing_stream_for_disconnection_(false), conn_id_(GATT_INVALID_CONN_ID), + mtu_(0), encrypted_(false), group_id_(group_id), csis_member_(false), @@ -212,10 +214,10 @@ class LeAudioDeviceGroup { transport_latency_stom_us_(0), active_context_type_(types::LeAudioContextType::UNINITIALIZED), metadata_context_type_(0), + active_contexts_mask_(0), pending_update_available_contexts_(std::nullopt), target_state_(types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE), - current_state_(types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE), - context_type_(types::LeAudioContextType::UNINITIALIZED) {} + current_state_(types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {} ~LeAudioDeviceGroup(void); void AddNode(const std::shared_ptr<LeAudioDevice>& leAudioDevice); @@ -263,8 +265,6 @@ class LeAudioDeviceGroup { 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); uint8_t GetSCA(void); uint8_t GetPacking(void); @@ -284,14 +284,12 @@ class LeAudioDeviceGroup { bool ReloadAudioLocations(void); bool ReloadAudioDirections(void); const set_configurations::AudioSetConfiguration* GetActiveConfiguration(void); - types::LeAudioContextType GetCurrentContextType(void); bool IsPendingConfiguration(void); void SetPendingConfiguration(void); void ClearPendingConfiguration(void); bool IsConfigurationSupported( LeAudioDevice* leAudioDevice, const set_configurations::AudioSetConfiguration* audio_set_conf); - types::AudioContexts GetActiveContexts(void); std::optional<LeAudioCodecConfiguration> GetCodecConfigurationByDirection( types::LeAudioContextType group_context_type, uint8_t direction); bool IsContextSupported(types::LeAudioContextType group_context_type); @@ -323,10 +321,18 @@ class LeAudioDeviceGroup { pending_update_available_contexts_ = audio_contexts; } + inline types::LeAudioContextType GetCurrentContextType(void) const { + return active_context_type_; + } + inline types::AudioContexts GetMetadataContextType(void) const { return metadata_context_type_; } + inline types::AudioContexts GetActiveContexts(void) { + return active_contexts_mask_; + } + bool IsInTransition(void); bool IsReleasing(void); void Dump(int fd); @@ -351,6 +357,7 @@ class LeAudioDeviceGroup { types::LeAudioContextType active_context_type_; types::AudioContexts metadata_context_type_; types::AudioContexts active_contexts_mask_; + std::optional<types::AudioContexts> pending_update_available_contexts_; std::map<types::LeAudioContextType, const set_configurations::AudioSetConfiguration*> @@ -358,7 +365,6 @@ class LeAudioDeviceGroup { types::AseState target_state_; types::AseState current_state_; - types::LeAudioContextType context_type_; std::vector<std::weak_ptr<LeAudioDevice>> leAudioDevices_; }; diff --git a/system/bta/le_audio/devices_test.cc b/system/bta/le_audio/devices_test.cc index 78b5df187b..2077e37c18 100644 --- a/system/bta/le_audio/devices_test.cc +++ b/system/bta/le_audio/devices_test.cc @@ -651,6 +651,8 @@ class LeAudioAseConfigurationTest : public Test { void TestAsesInactivated(const LeAudioDevice* device) { for (const auto& ase : device->ases_) { ASSERT_FALSE(ase.active); + ASSERT_TRUE(ase.cis_id == ::le_audio::kInvalidCisId); + ASSERT_TRUE(ase.cis_conn_hdl == 0); } } @@ -948,10 +950,20 @@ TEST_F(LeAudioAseConfigurationTest, test_reconnection_media) { TestSingleAseConfiguration(LeAudioContextType::MEDIA, data, 2, configuration); - SetCisInformationToActiveAse(); + /* Generate CISes, symulate CIG creation and assign cis handles to ASEs.*/ + group_->CigGenerateCisIds(LeAudioContextType::MEDIA); + std::vector<uint16_t> handles = {0x0012, 0x0013}; + group_->CigAssignCisConnHandles(handles); + group_->CigAssignCisIds(left); + group_->CigAssignCisIds(right); + TestActiveAses(); /* Left got disconnected */ left->DeactivateAllAses(); + + /* Unassign from the group*/ + group_->CigUnassignCis(left); + TestAsesInactivated(left); /* Prepare reconfiguration */ @@ -979,6 +991,12 @@ TEST_F(LeAudioAseConfigurationTest, test_reconnection_media) { TestGroupAseConfigurationVerdict(data[i]); } + /* Before device is rejoining, and group already exist, cis handles are + * assigned before sending codec config + */ + group_->CigAssignCisIds(left); + group_->CigAssignCisConnHandlesToAses(left); + TestActiveAses(); } } // namespace diff --git a/system/bta/le_audio/le_audio_client_test.cc b/system/bta/le_audio/le_audio_client_test.cc index 071598be72..0507e68215 100644 --- a/system/bta/le_audio/le_audio_client_test.cc +++ b/system/bta/le_audio/le_audio_client_test.cc @@ -68,6 +68,11 @@ extern struct fake_osi_alarm_set_on_mloop fake_osi_alarm_set_on_mloop_; std::map<std::string, int> mock_function_count_map; constexpr int max_num_of_ases = 5; +static constexpr char kNotifyUpperLayerAboutGroupBeingInIdleDuringCall[] = + "persist.bluetooth.leaudio.notify.idle.during.call"; + +void osi_property_set_bool(const char* key, bool value); + // Disables most likely false-positives from base::SplitString() extern "C" const char* __asan_default_options() { return "detect_container_overflow=0"; @@ -143,6 +148,7 @@ bool get_pts_connect_eatt_before_encryption(void) { return false; } bool get_pts_unencrypt_broadcast(void) { return false; } bool get_pts_eatt_peripheral_collision_support(void) { return false; } bool get_pts_force_le_audio_multiple_contexts_metadata(void) { return false; } +bool get_pts_le_audio_disable_ases_before_stopping(void) { return false; } config_t* get_all(void) { return nullptr; } stack_config_t mock_stack_config{ @@ -164,6 +170,8 @@ stack_config_t mock_stack_config{ get_pts_eatt_peripheral_collision_support, .get_pts_force_le_audio_multiple_contexts_metadata = get_pts_force_le_audio_multiple_contexts_metadata, + .get_pts_le_audio_disable_ases_before_stopping = + get_pts_le_audio_disable_ases_before_stopping, .get_all = get_all, }; const stack_config_t* stack_config_get_interface(void) { @@ -672,8 +680,9 @@ class UnicastTestNoInit : public Test { return false; } - group->Configure(group->GetContextType(), - static_cast<uint16_t>(group->GetContextType()), {}); + group->Configure( + group->GetCurrentContextType(), + static_cast<uint16_t>(group->GetCurrentContextType()), {}); if (!group->CigAssignCisIds(leAudioDevice)) return false; group->CigAssignCisConnHandlesToAses(leAudioDevice); @@ -756,15 +765,6 @@ class UnicastTestNoInit : public Test { types::LeAudioContextType context_type, 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) { - /* TODO: Switch context of group */ - group->SetContextType(context_type); - } - return true; - } - /* Do what ReleaseCisIds(group) does: start */ LeAudioDevice* leAudioDevice = group->GetFirstDevice(); while (leAudioDevice != nullptr) { @@ -785,7 +785,6 @@ class UnicastTestNoInit : public Test { if (group->GetState() == types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) { group->CigGenerateCisIds(context_type); - group->SetContextType(context_type); std::vector<uint16_t> conn_handles; for (uint8_t i = 0; i < (uint8_t)(group->cises_.size()); i++) { @@ -3778,5 +3777,127 @@ TEST_F(UnicastTest, StartNotSupportedContextType) { * is needed to restart the stream */ SinkAudioResume(); } + +TEST_F(UnicastTest, NotifyAboutGroupTunrnedIdleEnabled) { + const RawAddress test_address0 = GetTestAddress(0); + int group_id = bluetooth::groups::kGroupUnknown; + + osi_property_set_bool(kNotifyUpperLayerAboutGroupBeingInIdleDuringCall, true); + + SetSampleDatabaseEarbudsValid( + 1, test_address0, codec_spec_conf::kLeAudioLocationStereo, + codec_spec_conf::kLeAudioLocationStereo, default_channel_cnt, + default_channel_cnt, 0x0004, false /*add_csis*/, true /*add_cas*/, + true /*add_pacs*/, default_ase_cnt /*add_ascs_cnt*/, 1 /*set_size*/, + 0 /*rank*/); + EXPECT_CALL(mock_client_callbacks_, + OnConnectionState(ConnectionState::CONNECTED, test_address0)) + .Times(1); + EXPECT_CALL(mock_client_callbacks_, + OnGroupNodeStatus(test_address0, _, GroupNodeStatus::ADDED)) + .WillOnce(DoAll(SaveArg<1>(&group_id))); + + ConnectLeAudio(test_address0); + ASSERT_NE(group_id, bluetooth::groups::kGroupUnknown); + + // Start streaming + uint8_t cis_count_out = 1; + uint8_t cis_count_in = 0; + + LeAudioClient::Get()->SetInCall(true); + + // Audio sessions are started only when device gets active + EXPECT_CALL(*mock_unicast_audio_source_, Start(_, _)).Times(1); + EXPECT_CALL(*mock_audio_sink_, Start(_, _)).Times(1); + LeAudioClient::Get()->GroupSetActive(group_id); + + StartStreaming(AUDIO_USAGE_NOTIFICATION_TELEPHONY_RINGTONE, + AUDIO_CONTENT_TYPE_UNKNOWN, group_id); + + Mock::VerifyAndClearExpectations(&mock_client_callbacks_); + Mock::VerifyAndClearExpectations(mock_unicast_audio_source_); + SyncOnMainLoop(); + + // Verify Data transfer on one audio source cis + TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 1920); + + // Release + + /* To be called twice + * 1. GroupStatus::INACTIVE + * 2. GroupStatus::TURNED_IDLE_DURING_CALL + */ + EXPECT_CALL(mock_client_callbacks_, OnGroupStatus(group_id, _)).Times(2); + + EXPECT_CALL(*mock_unicast_audio_source_, Stop()).Times(1); + EXPECT_CALL(*mock_unicast_audio_source_, Release(_)).Times(1); + EXPECT_CALL(*mock_audio_sink_, Release(_)).Times(1); + LeAudioClient::Get()->GroupSetActive(bluetooth::groups::kGroupUnknown); + + Mock::VerifyAndClearExpectations(mock_unicast_audio_source_); + + LeAudioClient::Get()->SetInCall(false); + osi_property_set_bool(kNotifyUpperLayerAboutGroupBeingInIdleDuringCall, + false); +} + +TEST_F(UnicastTest, NotifyAboutGroupTunrnedIdleDisabled) { + const RawAddress test_address0 = GetTestAddress(0); + int group_id = bluetooth::groups::kGroupUnknown; + + SetSampleDatabaseEarbudsValid( + 1, test_address0, codec_spec_conf::kLeAudioLocationStereo, + codec_spec_conf::kLeAudioLocationStereo, default_channel_cnt, + default_channel_cnt, 0x0004, false /*add_csis*/, true /*add_cas*/, + true /*add_pacs*/, default_ase_cnt /*add_ascs_cnt*/, 1 /*set_size*/, + 0 /*rank*/); + EXPECT_CALL(mock_client_callbacks_, + OnConnectionState(ConnectionState::CONNECTED, test_address0)) + .Times(1); + EXPECT_CALL(mock_client_callbacks_, + OnGroupNodeStatus(test_address0, _, GroupNodeStatus::ADDED)) + .WillOnce(DoAll(SaveArg<1>(&group_id))); + + ConnectLeAudio(test_address0); + ASSERT_NE(group_id, bluetooth::groups::kGroupUnknown); + + // Start streaming + uint8_t cis_count_out = 1; + uint8_t cis_count_in = 0; + + LeAudioClient::Get()->SetInCall(true); + + // Audio sessions are started only when device gets active + EXPECT_CALL(*mock_unicast_audio_source_, Start(_, _)).Times(1); + EXPECT_CALL(*mock_audio_sink_, Start(_, _)).Times(1); + LeAudioClient::Get()->GroupSetActive(group_id); + + StartStreaming(AUDIO_USAGE_NOTIFICATION_TELEPHONY_RINGTONE, + AUDIO_CONTENT_TYPE_UNKNOWN, group_id); + + Mock::VerifyAndClearExpectations(&mock_client_callbacks_); + Mock::VerifyAndClearExpectations(mock_unicast_audio_source_); + SyncOnMainLoop(); + + // Verify Data transfer on one audio source cis + TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 1920); + + // Release + + /* To be called once only + * 1. GroupStatus::INACTIVE + */ + EXPECT_CALL(mock_client_callbacks_, OnGroupStatus(group_id, _)).Times(1); + + EXPECT_CALL(*mock_unicast_audio_source_, Stop()).Times(1); + EXPECT_CALL(*mock_unicast_audio_source_, Release(_)).Times(1); + EXPECT_CALL(*mock_audio_sink_, Release(_)).Times(1); + LeAudioClient::Get()->GroupSetActive(bluetooth::groups::kGroupUnknown); + + Mock::VerifyAndClearExpectations(mock_unicast_audio_source_); + + LeAudioClient::Get()->SetInCall(false); +} + } // namespace } // namespace le_audio diff --git a/system/bta/le_audio/le_audio_types.cc b/system/bta/le_audio/le_audio_types.cc index b6e63e5e85..f6d03e1a62 100644 --- a/system/bta/le_audio/le_audio_types.cc +++ b/system/bta/le_audio/le_audio_types.cc @@ -579,8 +579,8 @@ uint32_t AdjustAllocationForOffloader(uint32_t allocation) { namespace types { std::ostream& operator<<(std::ostream& os, const types::CigState& state) { - static const char* char_value_[4] = {"NONE", "CREATING", "CREATED", - "REMOVING"}; + static const char* char_value_[5] = {"NONE", "CREATING", "CREATED", + "REMOVING", "RECOVERING"}; os << char_value_[static_cast<uint8_t>(state)] << " (" << "0x" << std::setfill('0') << std::setw(2) << static_cast<int>(state) diff --git a/system/bta/le_audio/le_audio_types.h b/system/bta/le_audio/le_audio_types.h index f37c1109f2..776ef52551 100644 --- a/system/bta/le_audio/le_audio_types.h +++ b/system/bta/le_audio/le_audio_types.h @@ -311,7 +311,7 @@ constexpr uint16_t kMaxTransportLatencyMin = 0x0005; constexpr uint16_t kMaxTransportLatencyMax = 0x0FA0; /* Enums */ -enum class CigState : uint8_t { NONE, CREATING, CREATED, REMOVING }; +enum class CigState : uint8_t { NONE, CREATING, CREATED, REMOVING, RECOVERING }; /* ASE states according to BAP defined state machine states */ enum class AseState : uint8_t { diff --git a/system/bta/le_audio/mock_iso_manager.cc b/system/bta/le_audio/mock_iso_manager.cc index fe521eb3bf..a276be6620 100644 --- a/system/bta/le_audio/mock_iso_manager.cc +++ b/system/bta/le_audio/mock_iso_manager.cc @@ -58,7 +58,9 @@ void IsoManager::ReconfigureCig( pimpl_->ReconfigureCig(cig_id, std::move(cig_params)); } -void IsoManager::RemoveCig(uint8_t cig_id) { pimpl_->RemoveCig(cig_id); } +void IsoManager::RemoveCig(uint8_t cig_id, bool force) { + pimpl_->RemoveCig(cig_id, force); +} void IsoManager::EstablishCis( struct iso_manager::cis_establish_params conn_params) { diff --git a/system/bta/le_audio/mock_iso_manager.h b/system/bta/le_audio/mock_iso_manager.h index 8db3ade2a5..e5a3247dd6 100644 --- a/system/bta/le_audio/mock_iso_manager.h +++ b/system/bta/le_audio/mock_iso_manager.h @@ -43,7 +43,7 @@ struct MockIsoManager { (void), ReconfigureCig, (uint8_t cig_id, struct bluetooth::hci::iso_manager::cig_create_params cig_params)); - MOCK_METHOD((void), RemoveCig, (uint8_t cig_id)); + MOCK_METHOD((void), RemoveCig, (uint8_t cig_id, bool force)); MOCK_METHOD( (void), EstablishCis, (struct bluetooth::hci::iso_manager::cis_establish_params conn_params)); diff --git a/system/bta/le_audio/state_machine.cc b/system/bta/le_audio/state_machine.cc index ff1381a76c..20ae741a04 100644 --- a/system/bta/le_audio/state_machine.cc +++ b/system/bta/le_audio/state_machine.cc @@ -172,16 +172,16 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { switch (group->GetState()) { case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED: if (group->GetCurrentContextType() == context_type) { - group->Activate(context_type); - if (!group->Activate(context_type)) { - LOG(ERROR) << __func__ << ", failed to activate ASEs"; - return false; + if (group->Activate(context_type)) { + SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); + if (CigCreate(group)) { + return true; + } } - SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); - CigCreate(group); - return true; + LOG_INFO("Could not activate device, try to configure it again"); } + /* We are going to reconfigure whole group. Clear Cises.*/ ReleaseCisIds(group); /* If configuration is needed */ @@ -193,7 +193,6 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { } group->CigGenerateCisIds(context_type); - group->SetContextType(context_type); /* All ASEs should aim to achieve target state */ SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); PrepareAndSendCodecConfigure(group, group->GetFirstActiveDevice()); @@ -362,6 +361,18 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { } if (status != HCI_SUCCESS) { + if (status == HCI_ERR_COMMAND_DISALLOWED) { + /* + * We are here, because stack has no chance to remove CIG when it was + * shut during streaming. In the same time, controller probably was not + * Reseted, which creates the issue. Lets remove CIG and try to create + * it again. + */ + group->SetCigState(CigState::RECOVERING); + IsoManager::GetInstance()->RemoveCig(group->group_id_, true); + return; + } + group->SetCigState(CigState::NONE); LOG_ERROR(", failed to create CIG, reason: 0x%02x, new cig state: %s", +status, ToString(group->cig_state_).c_str()); @@ -405,8 +416,33 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { leAudioDevice->link_quality_timer = nullptr; } + void ProcessHciNotifyOnCigRemoveRecovering(uint8_t status, + LeAudioDeviceGroup* group) { + group->SetCigState(CigState::NONE); + + if (status != HCI_SUCCESS) { + LOG_ERROR( + "Could not recover from the COMMAND DISALLOAD on CigCreate. Status " + "on CIG remove is 0x%02x", + status); + StopStream(group); + return; + } + LOG_INFO("Succeed on CIG Recover - back to creating CIG"); + if (!CigCreate(group)) { + LOG_ERROR("Could not create CIG. Stop the stream for group %d", + group->group_id_); + StopStream(group); + } + } + void ProcessHciNotifOnCigRemove(uint8_t status, LeAudioDeviceGroup* group) override { + if (group->GetCigState() == CigState::RECOVERING) { + ProcessHciNotifyOnCigRemoveRecovering(status, group); + return; + } + if (status != HCI_SUCCESS) { group->SetCigState(CigState::CREATED); LOG_ERROR( @@ -1135,7 +1171,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { group->CigUnassignCis(leAudioDevice); } - void CigCreate(LeAudioDeviceGroup* group) { + bool CigCreate(LeAudioDeviceGroup* group) { uint32_t sdu_interval_mtos, sdu_interval_stom; uint16_t max_trans_lat_mtos, max_trans_lat_stom; uint8_t packing, framing, sca; @@ -1147,7 +1183,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { if (group->GetCigState() != CigState::NONE) { LOG_WARN(" Group %p, id: %d has invalid cig state: %s ", group, group->group_id_, ToString(group->cig_state_).c_str()); - return; + return false; } sdu_interval_mtos = @@ -1238,6 +1274,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { IsoManager::GetInstance()->CreateCig(group->group_id_, std::move(param)); LOG_DEBUG("Group: %p, id: %d cig state: %s", group, group->group_id_, ToString(group->cig_state_).c_str()); + return true; } static void CisCreateForDevice(LeAudioDevice* leAudioDevice) { @@ -1577,7 +1614,11 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) { - CigCreate(group); + if (!CigCreate(group)) { + LOG_ERROR("Could not create CIG. Stop the stream for group %d", + group->group_id_); + StopStream(group); + } return; } @@ -1662,7 +1703,11 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine { if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) { - CigCreate(group); + if (!CigCreate(group)) { + LOG_ERROR("Could not create CIG. Stop the stream for group %d", + group->group_id_); + StopStream(group); + } return; } diff --git a/system/bta/le_audio/state_machine_test.cc b/system/bta/le_audio/state_machine_test.cc index b8a5fb2d14..56cb8cc4cd 100644 --- a/system/bta/le_audio/state_machine_test.cc +++ b/system/bta/le_audio/state_machine_test.cc @@ -249,13 +249,19 @@ class StateMachineTest : public Test { for (auto i = 0u; i < p.cis_cfgs.size(); ++i) { conn_handles.push_back(UNIQUE_CIS_CONN_HANDLE(cig_id, i)); } + auto status = HCI_SUCCESS; + if (group_create_command_disallowed_) { + group_create_command_disallowed_ = false; + status = HCI_ERR_COMMAND_DISALLOWED; + } + LeAudioGroupStateMachine::Get()->ProcessHciNotifOnCigCreate( - group.get(), 0, cig_id, conn_handles); + group.get(), status, cig_id, conn_handles); } }); ON_CALL(*mock_iso_manager_, RemoveCig) - .WillByDefault([this](uint8_t cig_id) { + .WillByDefault([this](uint8_t cig_id, bool force) { DLOG(INFO) << "CreateRemove"; auto& group = le_audio_device_groups_[cig_id]; @@ -1148,6 +1154,7 @@ class StateMachineTest : public Test { std::vector<std::shared_ptr<LeAudioDevice>> le_audio_devices_; std::map<uint8_t, std::unique_ptr<LeAudioDeviceGroup>> le_audio_device_groups_; + bool group_create_command_disallowed_ = false; }; TEST_F(StateMachineTest, testInit) { @@ -1263,7 +1270,48 @@ TEST_F(StateMachineTest, testConfigureQosSingle) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); + + InjectInitialIdleNotification(group); + + ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( + group, static_cast<types::LeAudioContextType>(context_type), + context_type)); + + // Check if group has transitioned to a proper state + ASSERT_EQ(group->GetState(), + types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED); +} + +TEST_F(StateMachineTest, testConfigureQosSingleRecoverCig) { + const auto context_type = kContextTypeRingtone; + const int leaudio_group_id = 3; + + /* Assume that on previous BT OFF CIG was not removed */ + group_create_command_disallowed_ = true; + + // Prepare fake connected device group + auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); + + /* Since we prepared device with Ringtone context in mind, only one ASE + * should have been configured. + */ + auto* leAudioDevice = group->GetFirstDevice(); + PrepareConfigureCodecHandler(group, 1); + PrepareConfigureQosHandler(group, 1); + + // Start the configuration and stream Media content + EXPECT_CALL(gatt_queue, + WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, + GATT_WRITE_NO_RSP, _, _)) + .Times(3); + + EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(2); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(1); + EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); + EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); InjectInitialIdleNotification(group); @@ -1307,7 +1355,7 @@ TEST_F(StateMachineTest, testConfigureQosMultiple) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); InjectInitialIdleNotification(group); @@ -1346,7 +1394,7 @@ TEST_F(StateMachineTest, testStreamSingle) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); InjectInitialIdleNotification(group); @@ -1391,7 +1439,7 @@ TEST_F(StateMachineTest, testStreamSkipEnablingSink) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); InjectInitialIdleNotification(group); @@ -1437,7 +1485,7 @@ TEST_F(StateMachineTest, testStreamSkipEnablingSinkSource) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); InjectInitialIdleNotification(group); @@ -1477,7 +1525,7 @@ TEST_F(StateMachineTest, testStreamMultipleConversational) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(4); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); InjectInitialIdleNotification(group); @@ -1529,7 +1577,7 @@ TEST_F(StateMachineTest, testStreamMultiple) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); InjectInitialIdleNotification(group); @@ -1592,7 +1640,7 @@ TEST_F(StateMachineTest, testDisableSingle) { _, bluetooth::hci::iso_manager::kRemoveIsoDataPathDirectionInput)) .Times(1); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); InjectInitialIdleNotification(group); @@ -1660,7 +1708,7 @@ TEST_F(StateMachineTest, testDisableMultiple) { _, bluetooth::hci::iso_manager::kRemoveIsoDataPathDirectionInput)) .Times(2); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); InjectInitialIdleNotification(group); @@ -1725,7 +1773,7 @@ TEST_F(StateMachineTest, testDisableBidirectional) { bluetooth::hci::iso_manager::kRemoveIsoDataPathDirectionOutput)) .Times(1); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( @@ -1771,7 +1819,7 @@ TEST_F(StateMachineTest, testReleaseSingle) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(1); InjectInitialIdleNotification(group); @@ -1827,7 +1875,7 @@ TEST_F(StateMachineTest, testReleaseCachingSingle) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(1); InjectInitialIdleNotification(group); @@ -1900,7 +1948,7 @@ TEST_F(StateMachineTest, testStreamCachingSingle) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(4); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(2); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(1); InjectInitialIdleNotification(group); @@ -1987,7 +2035,7 @@ TEST_F(StateMachineTest, testActivateStreamCachingSingle) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(5); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(2); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(1); InjectInitialIdleNotification(group); @@ -2069,7 +2117,7 @@ TEST_F(StateMachineTest, testReleaseMultiple) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(2); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(1); InjectInitialIdleNotification(group); @@ -2126,7 +2174,7 @@ TEST_F(StateMachineTest, testReleaseBidirectional) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(1); InjectInitialIdleNotification(group); @@ -2175,7 +2223,7 @@ TEST_F(StateMachineTest, testDisableAndReleaseBidirectional) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(1); // Start the configuration and stream Media content LeAudioGroupStateMachine::Get()->StartStream( @@ -2209,7 +2257,7 @@ TEST_F(StateMachineTest, testAseIdAssignmentIdle) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); for (auto* device = group->GetFirstDevice(); device != nullptr; device = group->GetNextDevice(device)) { @@ -2239,7 +2287,7 @@ TEST_F(StateMachineTest, testAseIdAssignmentCodecConfigured) { EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); - EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); + EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(0); for (auto* device = group->GetFirstDevice(); device != nullptr; device = group->GetNextDevice(device)) { @@ -2727,5 +2775,154 @@ TEST_F(StateMachineTest, testAttachDeviceToTheStream) { ASSERT_NE(std::find(ccids->begin(), ccids->end(), media_ccid), ccids->end()); } +TEST_F(StateMachineTest, StartStreamAfterConfigure) { + const auto context_type = kContextTypeMedia; + const auto leaudio_group_id = 6; + const auto num_devices = 2; + + ContentControlIdKeeper::GetInstance()->SetCcid(media_context, media_ccid); + + // Prepare multiple fake connected devices in a group + auto* group = + PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); + ASSERT_EQ(group->Size(), num_devices); + + PrepareConfigureCodecHandler(group, 0, true); + PrepareConfigureQosHandler(group); + PrepareEnableHandler(group); + PrepareDisableHandler(group); + PrepareReleaseHandler(group); + + InjectInitialIdleNotification(group); + + auto* leAudioDevice = group->GetFirstDevice(); + auto expected_devices_written = 0; + while (leAudioDevice) { + /* Three Writes: + * 1. Codec configure + * 2: Codec QoS + * 3: Enabling + */ + EXPECT_CALL(gatt_queue, + WriteCharacteristic(leAudioDevice->conn_id_, + leAudioDevice->ctp_hdls_.val_hdl, _, + GATT_WRITE_NO_RSP, _, _)) + .Times(3); + expected_devices_written++; + leAudioDevice = group->GetNextDevice(leAudioDevice); + } + ASSERT_EQ(expected_devices_written, num_devices); + + // Validate GroupStreamStatus + EXPECT_CALL(mock_callbacks_, + StatusReportCb( + leaudio_group_id, + bluetooth::le_audio::GroupStreamStatus::CONFIGURED_BY_USER)); + + // Start the configuration and stream Media content + group->SetPendingConfiguration(); + LeAudioGroupStateMachine::Get()->ConfigureStream( + group, static_cast<types::LeAudioContextType>(context_type), + context_type); + + testing::Mock::VerifyAndClearExpectations(&mock_callbacks_); + + group->ClearPendingConfiguration(); + // Validate GroupStreamStatus + EXPECT_CALL( + mock_callbacks_, + StatusReportCb(leaudio_group_id, + bluetooth::le_audio::GroupStreamStatus::STREAMING)); + + // Start the configuration and stream Media content + LeAudioGroupStateMachine::Get()->StartStream( + group, static_cast<types::LeAudioContextType>(context_type), + context_type); + + testing::Mock::VerifyAndClearExpectations(&mock_callbacks_); +} + +TEST_F(StateMachineTest, StartStreamCachedConfig) { + const auto context_type = kContextTypeMedia; + const auto leaudio_group_id = 6; + const auto num_devices = 2; + + ContentControlIdKeeper::GetInstance()->SetCcid(media_context, media_ccid); + + // Prepare multiple fake connected devices in a group + auto* group = + PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); + ASSERT_EQ(group->Size(), num_devices); + + PrepareConfigureCodecHandler(group, 0, true); + PrepareConfigureQosHandler(group); + PrepareEnableHandler(group); + PrepareDisableHandler(group); + PrepareReleaseHandler(group); + + InjectInitialIdleNotification(group); + + auto* leAudioDevice = group->GetFirstDevice(); + auto expected_devices_written = 0; + while (leAudioDevice) { + /* Three Writes: + * 1: Codec config + * 2: Codec QoS (+1 after restart) + * 3: Enabling (+1 after restart) + * 4: Release (1) + */ + EXPECT_CALL(gatt_queue, + WriteCharacteristic(leAudioDevice->conn_id_, + leAudioDevice->ctp_hdls_.val_hdl, _, + GATT_WRITE_NO_RSP, _, _)) + .Times(6); + expected_devices_written++; + leAudioDevice = group->GetNextDevice(leAudioDevice); + } + ASSERT_EQ(expected_devices_written, num_devices); + + // Validate GroupStreamStatus + EXPECT_CALL( + mock_callbacks_, + StatusReportCb(leaudio_group_id, + bluetooth::le_audio::GroupStreamStatus::STREAMING)); + + // Start the configuration and stream Media content + LeAudioGroupStateMachine::Get()->StartStream( + group, static_cast<types::LeAudioContextType>(context_type), + context_type); + + testing::Mock::VerifyAndClearExpectations(&mock_callbacks_); + + // Validate GroupStreamStatus + EXPECT_CALL( + mock_callbacks_, + StatusReportCb(leaudio_group_id, + bluetooth::le_audio::GroupStreamStatus::RELEASING)); + + EXPECT_CALL( + mock_callbacks_, + StatusReportCb( + leaudio_group_id, + bluetooth::le_audio::GroupStreamStatus::CONFIGURED_AUTONOMOUS)); + // Start the configuration and stream Media content + LeAudioGroupStateMachine::Get()->StopStream(group); + + testing::Mock::VerifyAndClearExpectations(&mock_callbacks_); + + // Restart stream + EXPECT_CALL( + mock_callbacks_, + StatusReportCb(leaudio_group_id, + bluetooth::le_audio::GroupStreamStatus::STREAMING)); + + // Start the configuration and stream Media content + LeAudioGroupStateMachine::Get()->StartStream( + group, static_cast<types::LeAudioContextType>(context_type), + context_type); + + testing::Mock::VerifyAndClearExpectations(&mock_callbacks_); +} + } // namespace internal } // namespace le_audio diff --git a/system/bta/vc/vc.cc b/system/bta/vc/vc.cc index 403b93d936..1d406a973a 100644 --- a/system/bta/vc/vc.cc +++ b/system/bta/vc/vc.cc @@ -743,13 +743,23 @@ class VolumeControlImpl : public VolumeControl { int group_id, bool is_autonomous, uint8_t opcode, std::vector<uint8_t>& arguments) { - DLOG(INFO) << __func__ << " num of devices: " << devices.size() - << " group_id: " << group_id - << " is_autonomous: " << is_autonomous << " opcode: " << +opcode - << " arg size: " << arguments.size(); - - ongoing_operations_.emplace_back(latest_operation_id_++, group_id, - is_autonomous, opcode, arguments, devices); + LOG_DEBUG( + "num of devices: %zu, group_id: %d, is_autonomous: %s opcode: %d, arg " + "size: %zu", + devices.size(), group_id, is_autonomous ? "true" : "false", +opcode, + arguments.size()); + + if (std::find_if(ongoing_operations_.begin(), ongoing_operations_.end(), + [opcode, &arguments](const VolumeOperation& op) { + return (op.opcode_ == opcode) && + std::equal(op.arguments_.begin(), + op.arguments_.end(), + arguments.begin()); + }) == ongoing_operations_.end()) { + ongoing_operations_.emplace_back(latest_operation_id_++, group_id, + is_autonomous, opcode, arguments, + devices); + } } void MuteUnmute(std::variant<RawAddress, int> addr_or_group_id, bool mute) { @@ -760,11 +770,13 @@ class VolumeControlImpl : public VolumeControl { if (std::holds_alternative<RawAddress>(addr_or_group_id)) { LOG_DEBUG("Address: %s: ", (std::get<RawAddress>(addr_or_group_id)).ToString().c_str()); - std::vector<RawAddress> devices = { - std::get<RawAddress>(addr_or_group_id)}; - - PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown, - false, opcode, arg); + VolumeControlDevice* dev = volume_control_devices_.FindByAddress( + std::get<RawAddress>(addr_or_group_id)); + if (dev && dev->IsConnected()) { + std::vector<RawAddress> devices = {dev->address}; + PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown, + false, opcode, arg); + } } else { /* Handle group change */ auto group_id = std::get<int>(addr_or_group_id); @@ -815,14 +827,17 @@ class VolumeControlImpl : public VolumeControl { uint8_t opcode = kControlPointOpcodeSetAbsoluteVolume; if (std::holds_alternative<RawAddress>(addr_or_group_id)) { - DLOG(INFO) << __func__ << " " << std::get<RawAddress>(addr_or_group_id); - std::vector<RawAddress> devices = { - std::get<RawAddress>(addr_or_group_id)}; - - RemovePendingVolumeControlOperations(devices, - bluetooth::groups::kGroupUnknown); - PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown, - false, opcode, arg); + LOG_DEBUG("Address: %s: ", + std::get<RawAddress>(addr_or_group_id).ToString().c_str()); + VolumeControlDevice* dev = volume_control_devices_.FindByAddress( + std::get<RawAddress>(addr_or_group_id)); + if (dev && dev->IsConnected() && (dev->volume != volume)) { + std::vector<RawAddress> devices = {dev->address}; + RemovePendingVolumeControlOperations(devices, + bluetooth::groups::kGroupUnknown); + PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown, + false, opcode, arg); + } } else { /* Handle group change */ auto group_id = std::get<int>(addr_or_group_id); diff --git a/system/bta/vc/vc_test.cc b/system/bta/vc/vc_test.cc index bdc93e1f40..71fca365c8 100644 --- a/system/bta/vc/vc_test.cc +++ b/system/bta/vc/vc_test.cc @@ -915,10 +915,37 @@ class VolumeControlValueSetTest : public VolumeControlTest { }; TEST_F(VolumeControlValueSetTest, test_set_volume) { - std::vector<uint8_t> expected_data({0x04, 0x00, 0x10}); - EXPECT_CALL(gatt_queue, WriteCharacteristic(conn_id, 0x0024, expected_data, - GATT_WRITE, _, _)); + ON_CALL(gatt_queue, WriteCharacteristic(conn_id, 0x0024, _, GATT_WRITE, _, _)) + .WillByDefault([this](uint16_t conn_id, uint16_t handle, + std::vector<uint8_t> value, + tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb, + void* cb_data) { + std::vector<uint8_t> ntf_value({ + value[2], // volume level + 0, // muted + static_cast<uint8_t>(value[1] + 1), // change counter + }); + GetNotificationEvent(0x0021, ntf_value); + }); + + const std::vector<uint8_t> vol_x10({0x04, 0x00, 0x10}); + EXPECT_CALL(gatt_queue, + WriteCharacteristic(conn_id, 0x0024, vol_x10, GATT_WRITE, _, _)) + .Times(1); + VolumeControl::Get()->SetVolume(test_address, 0x10); + + // Same volume level should not be applied twice + const std::vector<uint8_t> vol_x10_2({0x04, 0x01, 0x10}); + EXPECT_CALL(gatt_queue, + WriteCharacteristic(conn_id, 0x0024, vol_x10_2, GATT_WRITE, _, _)) + .Times(0); VolumeControl::Get()->SetVolume(test_address, 0x10); + + const std::vector<uint8_t> vol_x20({0x04, 0x01, 0x20}); + EXPECT_CALL(gatt_queue, + WriteCharacteristic(conn_id, 0x0024, vol_x20, GATT_WRITE, _, _)) + .Times(1); + VolumeControl::Get()->SetVolume(test_address, 0x20); } TEST_F(VolumeControlValueSetTest, test_mute) { diff --git a/system/btif/Android.bp b/system/btif/Android.bp index 732d585b80..f522cf469d 100644 --- a/system/btif/Android.bp +++ b/system/btif/Android.bp @@ -35,14 +35,11 @@ btifCommonIncludes = [ cc_library { name: "libstatslog_bt", + defaults: ["fluoride_common_options"], host_supported: true, generated_sources: ["statslog_bt.cpp"], generated_headers: ["statslog_bt.h"], export_generated_headers: ["statslog_bt.h"], - cflags: [ - "-Wall", - "-Werror", - ], shared_libs: [ "libcutils", ], diff --git a/system/btif/src/btif_a2dp_source.cc b/system/btif/src/btif_a2dp_source.cc index 55f46dd31c..0439ce47cd 100644 --- a/system/btif/src/btif_a2dp_source.cc +++ b/system/btif/src/btif_a2dp_source.cc @@ -1015,12 +1015,22 @@ static bool btif_a2dp_source_enqueue_callback(BT_HDR* p_buf, size_t frames_n, if (status != BTM_CMD_STARTED) { LOG_WARN("%s: Cannot read RSSI: status %d", __func__, status); } + + // Intel controllers don't handle ReadFailedContactCounter very well, it + // sends back Hardware Error event which will crash the daemon. So + // temporarily disable this for Floss. + // TODO(b/249876976): Intel controllers to handle this command correctly. + // And if the need for disabling metrics-related HCI call grows, consider + // creating a framework to avoid ifdefs. +#ifndef TARGET_FLOSS status = BTM_ReadFailedContactCounter(peer_bda, btm_read_failed_contact_counter_cb); if (status != BTM_CMD_STARTED) { LOG_WARN("%s: Cannot read Failed Contact Counter: status %d", __func__, status); } +#endif + status = BTM_ReadTxPower(peer_bda, BT_TRANSPORT_BR_EDR, btm_read_tx_power_cb); if (status != BTM_CMD_STARTED) { diff --git a/system/btif/src/btif_av.cc b/system/btif/src/btif_av.cc index d0ded18386..c3cb8d72ff 100644 --- a/system/btif/src/btif_av.cc +++ b/system/btif/src/btif_av.cc @@ -696,7 +696,9 @@ static void btif_av_handle_event(uint8_t peer_sep, tBTA_AV_HNDL bta_handle, const BtifAvEvent& btif_av_event); static void btif_report_connection_state(const RawAddress& peer_address, - btav_connection_state_t state); + btav_connection_state_t state, + const bt_status_t status, + uint8_t error_code); static void btif_report_audio_state(const RawAddress& peer_address, btav_audio_state_t state); static void btif_av_report_sink_audio_config_state( @@ -1548,8 +1550,6 @@ bool BtifAvStateMachine::StateIdle::ProcessEvent(uint32_t event, void* p_data) { case BTA_AV_OPEN_EVT: { tBTA_AV* p_bta_data = (tBTA_AV*)p_data; - btav_connection_state_t state; - int av_state; tBTA_AV_STATUS status = p_bta_data->open.status; bool can_connect = true; @@ -1560,44 +1560,55 @@ bool BtifAvStateMachine::StateIdle::ProcessEvent(uint32_t event, void* p_data) { (status == BTA_AV_SUCCESS) ? "SUCCESS" : "FAILED", p_bta_data->open.edr); + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_CONNECTING, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); + if (p_bta_data->open.status == BTA_AV_SUCCESS) { - state = BTAV_CONNECTION_STATE_CONNECTED; - av_state = BtifAvStateMachine::kStateOpened; peer_.SetEdr(p_bta_data->open.edr); CHECK(peer_.PeerSep() == p_bta_data->open.sep); - // Check whether connection is allowed - if (peer_.IsSink()) { - can_connect = btif_av_source.AllowedToConnect(peer_.PeerAddress()); - if (!can_connect) src_disconnect_sink(peer_.PeerAddress()); - } else if (peer_.IsSource()) { - can_connect = btif_av_sink.AllowedToConnect(peer_.PeerAddress()); - if (!can_connect) sink_disconnect_src(peer_.PeerAddress()); - } - } else { - state = BTAV_CONNECTION_STATE_DISCONNECTED; - av_state = BtifAvStateMachine::kStateIdle; - } - if (!can_connect) { - BTIF_TRACE_ERROR( - "%s: Cannot connect to peer %s: too many connected " - "peers", - __PRETTY_FUNCTION__, peer_.PeerAddress().ToString().c_str()); - } else { - // Report the connection state to the application - btif_report_connection_state(peer_.PeerAddress(), state); - // Change state to Open/Idle based on the status - peer_.StateMachine().TransitionTo(av_state); - if (peer_.IsSink()) { - // If queued PLAY command, send it now - btif_rc_check_handle_pending_play( - p_bta_data->open.bd_addr, - (p_bta_data->open.status == BTA_AV_SUCCESS)); - } else if (peer_.IsSource() && - (p_bta_data->open.status == BTA_AV_SUCCESS)) { - // Bring up AVRCP connection as well - BTA_AvOpenRc(peer_.BtaHandle()); + can_connect = peer_.IsSink() + ? btif_av_source.AllowedToConnect(peer_.PeerAddress()) + : btif_av_sink.AllowedToConnect(peer_.PeerAddress()); + + if (!can_connect) { + BTIF_TRACE_ERROR( + "%s: Cannot connect to peer %s: too many connected " + "peers", + __PRETTY_FUNCTION__, peer_.PeerAddress().ToString().c_str()); + + if (peer_.IsSink()) { + src_disconnect_sink(peer_.PeerAddress()); + } else if (peer_.IsSource()) { + sink_disconnect_src(peer_.PeerAddress()); + } + + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_NOMEM, BTA_AV_FAIL_RESOURCES); + peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateIdle); + } else { + if (peer_.IsSink()) { + // If queued PLAY command, send it now + btif_rc_check_handle_pending_play( + p_bta_data->open.bd_addr, + (p_bta_data->open.status == BTA_AV_SUCCESS)); + } else if (peer_.IsSource() && + (p_bta_data->open.status == BTA_AV_SUCCESS)) { + // Bring up AVRCP connection as well + BTA_AvOpenRc(peer_.BtaHandle()); + } + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_CONNECTED, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); + peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateOpened); } + } else { + btif_report_connection_state(peer_.PeerAddress(), + BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_FAIL, status); + peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateIdle); } btif_queue_advance(); } break; @@ -1649,7 +1660,8 @@ void BtifAvStateMachine::StateOpening::OnEnter() { // Inform the application that we are entering connecting state btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_CONNECTING); + BTAV_CONNECTION_STATE_CONNECTING, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); } void BtifAvStateMachine::StateOpening::OnExit() { @@ -1675,14 +1687,16 @@ bool BtifAvStateMachine::StateOpening::ProcessEvent(uint32_t event, // it is in an intermediate state. In other states we can handle // incoming/outgoing connect/disconnect requests. BTIF_TRACE_WARNING( - "%s: Peer %s : event=%s: transitioning to Idle due to ACL Disconnect", + "%s: Peer %s : event=%s: transitioning to Idle due to ACL " + "Disconnect", __PRETTY_FUNCTION__, peer_.PeerAddress().ToString().c_str(), BtifAvEvent::EventName(event).c_str()); log_counter_metrics_btif(android::bluetooth::CodePathCounterKeyEnum:: A2DP_CONNECTION_ACL_DISCONNECTED, 1); btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_DISCONNECTED); + BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_FAIL, BTA_AV_FAIL); peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateIdle); if (peer_.SelfInitiatedConnection()) { btif_queue_advance(); @@ -1696,8 +1710,9 @@ bool BtifAvStateMachine::StateOpening::ProcessEvent(uint32_t event, log_counter_metrics_btif(android::bluetooth::CodePathCounterKeyEnum:: A2DP_CONNECTION_REJECT_EVT, 1); - btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_DISCONNECTED); + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_AUTH_REJECTED, BTA_AV_FAIL); peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateIdle); if (peer_.SelfInitiatedConnection()) { btif_queue_advance(); @@ -1706,7 +1721,6 @@ bool BtifAvStateMachine::StateOpening::ProcessEvent(uint32_t event, case BTA_AV_OPEN_EVT: { tBTA_AV* p_bta_data = (tBTA_AV*)p_data; - btav_connection_state_t state; int av_state; tBTA_AV_STATUS status = p_bta_data->open.status; @@ -1718,10 +1732,13 @@ bool BtifAvStateMachine::StateOpening::ProcessEvent(uint32_t event, p_bta_data->open.edr); if (p_bta_data->open.status == BTA_AV_SUCCESS) { - state = BTAV_CONNECTION_STATE_CONNECTED; av_state = BtifAvStateMachine::kStateOpened; peer_.SetEdr(p_bta_data->open.edr); CHECK(peer_.PeerSep() == p_bta_data->open.sep); + // Report the connection state to the application + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_CONNECTED, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); log_counter_metrics_btif( android::bluetooth::CodePathCounterKeyEnum::A2DP_CONNECTION_SUCCESS, 1); @@ -1738,15 +1755,16 @@ bool BtifAvStateMachine::StateOpening::ProcessEvent(uint32_t event, BTA_AvCloseRc(peer_handle); } } - state = BTAV_CONNECTION_STATE_DISCONNECTED; av_state = BtifAvStateMachine::kStateIdle; + // Report the connection state to the application + btif_report_connection_state(peer_.PeerAddress(), + BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_FAIL, status); log_counter_metrics_btif( android::bluetooth::CodePathCounterKeyEnum::A2DP_CONNECTION_FAILURE, 1); } - // Report the connection state to the application - btif_report_connection_state(peer_.PeerAddress(), state); // Change state to Open/Idle based on the status peer_.StateMachine().TransitionTo(av_state); if (peer_.IsSink()) { @@ -1815,7 +1833,8 @@ bool BtifAvStateMachine::StateOpening::ProcessEvent(uint32_t event, case BTA_AV_CLOSE_EVT: btif_a2dp_on_stopped(nullptr); btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_DISCONNECTED); + BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_FAIL, BTA_AV_FAIL); peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateIdle); log_counter_metrics_btif( android::bluetooth::CodePathCounterKeyEnum::A2DP_CONNECTION_CLOSE, 1); @@ -1827,7 +1846,8 @@ bool BtifAvStateMachine::StateOpening::ProcessEvent(uint32_t event, case BTIF_AV_DISCONNECT_REQ_EVT: BTA_AvClose(peer_.BtaHandle()); btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_DISCONNECTED); + BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_FAIL, BTA_AV_FAIL); peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateIdle); log_counter_metrics_btif(android::bluetooth::CodePathCounterKeyEnum:: A2DP_CONNECTION_DISCONNECTED, @@ -1929,7 +1949,8 @@ bool BtifAvStateMachine::StateOpened::ProcessEvent(uint32_t event, return true; // If remote tries to start A2DP when DUT is A2DP Source, then Suspend. - // If A2DP is Sink and call is active, then disconnect the AVDTP channel. + // If A2DP is Sink and call is active, then disconnect the AVDTP + // channel. bool should_suspend = false; if (peer_.IsSink()) { if (!peer_.CheckFlags(BtifAvPeer::kFlagPendingStart | @@ -1945,8 +1966,8 @@ bool BtifAvStateMachine::StateOpened::ProcessEvent(uint32_t event, should_suspend = true; } - // If peer is A2DP Source, do ACK commands to audio HAL and start media - // task + // If peer is A2DP Source, do ACK commands to audio HAL and start + // media task if (btif_a2dp_on_started(peer_.PeerAddress(), &p_av->start)) { // Only clear pending flag after acknowledgement peer_.ClearFlags(BtifAvPeer::kFlagPendingStart); @@ -1977,8 +1998,9 @@ bool BtifAvStateMachine::StateOpened::ProcessEvent(uint32_t event, } // Inform the application that we are disconnecting - btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_DISCONNECTING); + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_DISCONNECTING, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); // Wait in closing state until fully closed peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateClosing); @@ -1986,6 +2008,10 @@ bool BtifAvStateMachine::StateOpened::ProcessEvent(uint32_t event, case BTA_AV_CLOSE_EVT: // AVDTP link is closed + // Inform the application that we are disconnecting + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_DISCONNECTING, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); // Change state to Idle, send acknowledgement if start is pending if (peer_.CheckFlags(BtifAvPeer::kFlagPendingStart)) { BTIF_TRACE_WARNING("%s: Peer %s : failed pending start request", @@ -2003,8 +2029,9 @@ bool BtifAvStateMachine::StateOpened::ProcessEvent(uint32_t event, } // Inform the application that we are disconnected - btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_DISCONNECTED); + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateIdle); break; @@ -2164,8 +2191,9 @@ bool BtifAvStateMachine::StateStarted::ProcessEvent(uint32_t event, } // Inform the application that we are disconnecting - btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_DISCONNECTING); + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_DISCONNECTING, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); // Wait in closing state until fully closed peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateClosing); @@ -2220,8 +2248,8 @@ bool BtifAvStateMachine::StateStarted::ProcessEvent(uint32_t event, peer_.SetFlags(BtifAvPeer::kFlagPendingStop); peer_.ClearFlags(BtifAvPeer::kFlagLocalSuspendPending); - // Don't change the encoder and audio provider state by a non-active peer - // since they are shared between peers + // Don't change the encoder and audio provider state by a non-active + // peer since they are shared between peers if (peer_.IsActivePeer() || !btif_av_stream_started_ready()) { btif_a2dp_on_stopped(&p_av->suspend); } @@ -2239,6 +2267,10 @@ bool BtifAvStateMachine::StateStarted::ProcessEvent(uint32_t event, peer_.PeerAddress().ToString().c_str(), BtifAvEvent::EventName(event).c_str(), peer_.FlagsToString().c_str()); + // Inform the application that we are disconnecting + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_DISCONNECTING, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); peer_.SetFlags(BtifAvPeer::kFlagPendingStop); @@ -2248,8 +2280,9 @@ bool BtifAvStateMachine::StateStarted::ProcessEvent(uint32_t event, } // Inform the application that we are disconnected - btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_DISCONNECTED); + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateIdle); break; @@ -2327,8 +2360,9 @@ bool BtifAvStateMachine::StateClosing::ProcessEvent(uint32_t event, case BTA_AV_CLOSE_EVT: // Inform the application that we are disconnecting - btif_report_connection_state(peer_.PeerAddress(), - BTAV_CONNECTION_STATE_DISCONNECTED); + btif_report_connection_state( + peer_.PeerAddress(), BTAV_CONNECTION_STATE_DISCONNECTED, + bt_status_t::BT_STATUS_SUCCESS, BTA_AV_SUCCESS); peer_.StateMachine().TransitionTo(BtifAvStateMachine::kStateIdle); break; @@ -2371,9 +2405,9 @@ bool BtifAvStateMachine::StateClosing::ProcessEvent(uint32_t event, } /** - * Timer to trigger AV Open on the Source if the remote Sink device establishes - * AVRCP connection without AV connection. The timer is needed to interoperate - * with headsets that do establish AV after AVRCP connection. + * Timer to trigger AV Open on the Source if the remote Sink device + * establishes AVRCP connection without AV connection. The timer is needed to + * interoperate with headsets that do establish AV after AVRCP connection. */ static void btif_av_source_initiate_av_open_timer_timeout(void* data) { BtifAvPeer* peer = (BtifAvPeer*)data; @@ -2399,8 +2433,8 @@ static void btif_av_source_initiate_av_open_timer_timeout(void* data) { } /** - * Timer to trigger AV Open on the Sink if the remote Source device establishes - * AVRCP connection without AV connection. + * Timer to trigger AV Open on the Sink if the remote Source device + * establishes AVRCP connection without AV connection. */ static void btif_av_sink_initiate_av_open_timer_timeout(void* data) { BtifAvPeer* peer = (BtifAvPeer*)data; @@ -2432,18 +2466,24 @@ static void btif_av_sink_initiate_av_open_timer_timeout(void* data) { * @param state the connection state */ static void btif_report_connection_state(const RawAddress& peer_address, - btav_connection_state_t state) { + btav_connection_state_t state, + bt_status_t status, + uint8_t error_code) { LOG_INFO("%s: peer_address=%s state=%d", __func__, peer_address.ToString().c_str(), state); if (btif_av_source.Enabled()) { - do_in_jni_thread(FROM_HERE, - base::Bind(btif_av_source.Callbacks()->connection_state_cb, - peer_address, state, btav_error_t{})); + do_in_jni_thread( + FROM_HERE, + base::Bind(btif_av_source.Callbacks()->connection_state_cb, + peer_address, state, + btav_error_t{.status = status, .error_code = error_code})); } else if (btif_av_sink.Enabled()) { - do_in_jni_thread(FROM_HERE, - base::Bind(btif_av_sink.Callbacks()->connection_state_cb, - peer_address, state, btav_error_t{})); + do_in_jni_thread( + FROM_HERE, + base::Bind(btif_av_sink.Callbacks()->connection_state_cb, peer_address, + state, + btav_error_t{.status = status, .error_code = error_code})); } } @@ -2528,8 +2568,8 @@ static void btif_av_report_sink_audio_config_state( } /** - * Call out to JNI / JAVA layers to retrieve whether the mandatory codec is more - * preferred than others. + * Call out to JNI / JAVA layers to retrieve whether the mandatory codec is + * more preferred than others. * * @param peer_address the peer address */ @@ -3252,8 +3292,8 @@ bt_status_t btif_av_source_execute_service(bool enable) { // Added BTA_AV_FEAT_NO_SCO_SSPD - this ensures that the BTA does not // auto-suspend av streaming on AG events(SCO or Call). The suspend shall // be initiated by the app/audioflinger layers. - // Support for browsing for SDP record should work only if we enable BROWSE - // while registering. + // Support for browsing for SDP record should work only if we enable + // BROWSE while registering. tBTA_AV_FEAT features = BTA_AV_FEAT_RCTG | BTA_AV_FEAT_METADATA | BTA_AV_FEAT_VENDOR | BTA_AV_FEAT_NO_SCO_SSPD; diff --git a/system/btif/src/btif_hf.cc b/system/btif/src/btif_hf.cc index 83197554e9..48eccff256 100644 --- a/system/btif/src/btif_hf.cc +++ b/system/btif/src/btif_hf.cc @@ -320,10 +320,17 @@ static void btif_hf_upstreams_evt(uint16_t event, char* p_param) { break; // RFCOMM connected or failed to connect case BTA_AG_OPEN_EVT: - // Check if an outoging connection is pending + bt_hf_callbacks->ConnectionStateCallback(BTHF_CONNECTION_STATE_CONNECTING, + &(p_data->open.bd_addr)); + // Check if an outgoing connection is pending if (btif_hf_cb[idx].is_initiator) { + // There is an outgoing connection. + // Check the incoming open event status and the outgoing connection + // state. if ((p_data->open.status != BTA_AG_SUCCESS) && btif_hf_cb[idx].state != BTHF_CONNECTION_STATE_CONNECTING) { + // Check if the incoming open event and the outgoing connection are + // for the same device. if (p_data->open.bd_addr == btif_hf_cb[idx].connected_bda) { LOG(WARNING) << __func__ << ": btif_hf_cb state[" << p_data->open.status @@ -350,10 +357,14 @@ static void btif_hf_upstreams_evt(uint16_t event, char* p_param) { break; } + // There is an outgoing connection. + // Check the outgoing connection state and address. CHECK_EQ(btif_hf_cb[idx].state, BTHF_CONNECTION_STATE_CONNECTING) << "Control block must be in connecting state when initiating"; CHECK(!btif_hf_cb[idx].connected_bda.IsEmpty()) << "Remote device address must not be empty when initiating"; + // Check if the incoming open event and the outgoing connection are + // for the same device. if (btif_hf_cb[idx].connected_bda != p_data->open.bd_addr) { LOG(WARNING) << __func__ << ": possible connection collision, ignore the " @@ -372,6 +383,8 @@ static void btif_hf_upstreams_evt(uint16_t event, char* p_param) { btif_queue_advance(); } } + + // There is no pending outgoing connection. if (p_data->open.status == BTA_AG_SUCCESS) { // In case this is an incoming connection btif_hf_cb[idx].connected_bda = p_data->open.bd_addr; @@ -408,12 +421,15 @@ static void btif_hf_upstreams_evt(uint16_t event, char* p_param) { "SLC and RFCOMM both disconnected event:%s idx:%d" " btif_hf_cb.handle:%d", dump_hf_event(event), idx, btif_hf_cb[idx].handle); + RawAddress connected_bda = btif_hf_cb[idx].connected_bda; + bt_hf_callbacks->ConnectionStateCallback( + BTHF_CONNECTION_STATE_DISCONNECTING, &connected_bda); // If AG_OPEN was received but SLC was not connected in time, then // AG_CLOSE may be received. We need to advance the queue here. bool failed_to_setup_slc = (btif_hf_cb[idx].state != BTHF_CONNECTION_STATE_SLC_CONNECTED) && btif_hf_cb[idx].is_initiator; - RawAddress connected_bda = btif_hf_cb[idx].connected_bda; + reset_control_block(&btif_hf_cb[idx]); bt_hf_callbacks->ConnectionStateCallback(btif_hf_cb[idx].state, &connected_bda); diff --git a/system/btif/src/btif_storage.cc b/system/btif/src/btif_storage.cc index d73934a3c3..470f04904a 100644 --- a/system/btif/src/btif_storage.cc +++ b/system/btif/src/btif_storage.cc @@ -969,6 +969,13 @@ bt_status_t btif_storage_remove_bonded_device( /* write bonded info immediately */ btif_config_flush(); + + /* Check the length of the paired devices, and if 0 then reset IRK */ + auto paired_devices = btif_config_get_paired_devices(); + if (paired_devices.empty()) { + LOG_INFO("Last paired device removed, resetting IRK"); + BTA_DmBleResetId(); + } return ret ? BT_STATUS_SUCCESS : BT_STATUS_FAIL; } @@ -1378,7 +1385,10 @@ bt_status_t btif_storage_add_ble_local_key(const Octet16& key, return BT_STATUS_FAIL; } int ret = btif_config_set_bin("Adapter", name, key.data(), key.size()); - btif_config_save(); + // Had to change this to flush to get it to work on test. + // Seems to work in the real world on a phone... but not sure why there's a + // race in test. Investigate b/239828132 + btif_config_flush(); return ret ? BT_STATUS_SUCCESS : BT_STATUS_FAIL; } diff --git a/system/build/Android.bp b/system/build/Android.bp index 32f79717e9..37dae66c30 100644 --- a/system/build/Android.bp +++ b/system/build/Android.bp @@ -23,8 +23,20 @@ bootstrap_go_package { pluginFor: ["soong_build"], } +cc_defaults { + name: "fluoride_common_options", + cflags: [ + "-Wall", + "-Wextra", + "-Werror", + // there are too many unused parameters in all the code. + "-Wno-unused-parameter", + ], +} + fluoride_defaults { name: "libchrome_support_defaults", + defaults: ["fluoride_common_options"], static_libs: [ "libchrome", "libmodpb64", @@ -33,11 +45,6 @@ fluoride_defaults { shared_libs: [ "libbase", ], - cflags: [ - "-Wall", - "-Wextra", - "-Werror", - ], target: { darwin: { enabled: false, @@ -54,12 +61,8 @@ fluoride_defaults { // default to be used only on platform libs that can rely on shared libchrome fluoride_defaults { name: "libchrome_shared_support_defaults", + defaults: ["fluoride_common_options"], shared_libs: ["libchrome"], - cflags: [ - "-Wall", - "-Wextra", - "-Werror", - ], target: { darwin: { enabled: false, @@ -72,13 +75,12 @@ fluoride_defaults { // requires no shared libraries, and no explicit sanitization. fluoride_defaults { name: "fluoride_types_defaults_fuzzable", + defaults: ["fluoride_common_options"], cflags: [ "-DEXPORT_SYMBOL=__attribute__((visibility(\"default\")))", "-fvisibility=hidden", // struct BT_HDR is defined as a variable-size header in a struct. "-Wno-gnu-variable-sized-type-not-at-end", - // there are too many unused parameters in all the code. - "-Wno-unused-parameter", "-DLOG_NDEBUG=1", ], conlyflags: [ diff --git a/system/conf/bt_stack.conf b/system/conf/bt_stack.conf index 84e1a99b6f..f82351bb63 100644 --- a/system/conf/bt_stack.conf +++ b/system/conf/bt_stack.conf @@ -97,6 +97,9 @@ TRC_HID_DEV=2 # Use EATT for all services #PTS_UseEattForAllServices=true +# Suspend stream after some timeout in LE Audio client module +#PTS_LeAudioSuspendStreaming=true + # Force to update metadata with multiple CCIDs #PTS_ForceLeAudioMultipleContextsMetadata=true diff --git a/system/embdrv/lc3/Android.bp b/system/embdrv/lc3/Android.bp index a16f30ed48..6f92aee786 100644 --- a/system/embdrv/lc3/Android.bp +++ b/system/embdrv/lc3/Android.bp @@ -21,9 +21,7 @@ cc_library_static { cflags: [ "-O3", "-ffast-math", - "-Werror", "-Wmissing-braces", - "-Wno-unused-parameter", "-Wno-#warnings", "-Wuninitialized", "-Wno-self-assign", diff --git a/system/gd/Android.bp b/system/gd/Android.bp index 4d971bf410..2c474757cd 100644 --- a/system/gd/Android.bp +++ b/system/gd/Android.bp @@ -10,6 +10,9 @@ package { cc_defaults { name: "gd_defaults", + defaults: [ + "fluoride_common_options", + ], tidy_checks: [ "-performance-unnecessary-value-param", ], @@ -42,14 +45,12 @@ cc_defaults { "-fvisibility=hidden", "-DLOG_NDEBUG=1", "-DGOOGLE_PROTOBUF_NO_RTTI", - "-Wno-unused-parameter", "-Wno-unused-result", ], conlyflags: [ "-std=c99", ], header_libs: ["jni_headers"], - } // Enables code coverage for a set of source files. Must be combined with diff --git a/system/gd/common/byte_array_test.cc b/system/gd/common/byte_array_test.cc index 114a79c344..7a3e3edb65 100644 --- a/system/gd/common/byte_array_test.cc +++ b/system/gd/common/byte_array_test.cc @@ -22,32 +22,128 @@ using bluetooth::common::ByteArray; -static const char* test_bytes = "4c68384139f574d836bcf34e9dfb01bf\0"; -static uint8_t test_data[16] = { +namespace { +const char* byte_string16 = "4c68384139f574d836bcf34e9dfb01bf\0"; +const uint8_t byte_data16[16] = { 0x4c, 0x68, 0x38, 0x41, 0x39, 0xf5, 0x74, 0xd8, 0x36, 0xbc, 0xf3, 0x4e, 0x9d, 0xfb, 0x01, 0xbf}; -static uint8_t data[16] = { - 0x4c, 0x87, 0x49, 0xe1, 0x2e, 0x55, 0x0f, 0x7f, 0x60, 0x8b, 0x4f, 0x96, 0xd7, 0xc5, 0xbc, 0x2a}; +const char* byte_string21 = "4c68384139f574d836bcf34e9dfb01bf0011223344\0"; +const uint8_t byte_data21[21] = {0x4c, 0x68, 0x38, 0x41, 0x39, 0xf5, 0x74, 0xd8, 0x36, 0xbc, 0xf3, + 0x4e, 0x9d, 0xfb, 0x01, 0xbf, 0x00, 0x11, 0x22, 0x33, 0x44}; +const char* byte_string23 = "4c68384139f574d836bcf34e9dfb01bf00112233445566\0"; +const uint8_t byte_data23[23] = {0x4c, 0x68, 0x38, 0x41, 0x39, 0xf5, 0x74, 0xd8, 0x36, 0xbc, 0xf3, 0x4e, + 0x9d, 0xfb, 0x01, 0xbf, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}; +const char* byte_string28 = "4c68384139f574d836bcf34e9dfb01bf00112233445566778899aabb\0"; +const uint8_t byte_data28[28] = {0x4c, 0x68, 0x38, 0x41, 0x39, 0xf5, 0x74, 0xd8, 0x36, 0xbc, 0xf3, 0x4e, 0x9d, 0xfb, + 0x01, 0xbf, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb}; -TEST(ByteArrayTest, test_constructor_array) { - ByteArray<16> byte_array(data); - - for (size_t i = 0; i < ByteArray<16>::kLength; i++) { +template <typename T, size_t N> +void simple_constructor_test(const T (&data)[N]) { + ByteArray<N> byte_array(data); + for (size_t i = 0; i < ByteArray<N>::kLength; i++) { ASSERT_EQ(data[i], byte_array.bytes[i]); } } -TEST(ByteArrayTest, test_from_str) { - auto byte_array = ByteArray<16>::FromString(test_bytes); +template <typename T, size_t N> +void simple_const_constructor_test(const T (&data)[N]) { + const ByteArray<N> byte_array(data); + for (size_t i = 0; i < ByteArray<N>::kLength; i++) { + ASSERT_EQ(data[i], byte_array.data()[i]); + } +} + +template <typename T, size_t N> +void simple_array_constructor_test(const T (&data)[N]) { + std::array<uint8_t, N> array_of_bytes; + std::copy(data, data + N, std::begin(array_of_bytes)); + + ByteArray<N> byte_array(array_of_bytes); + for (size_t i = 0; i < ByteArray<N>::kLength; i++) { + ASSERT_EQ(data[i], byte_array.data()[i]); + } +} + +template <typename T, size_t N> +void simple_from_string_test(const char* byte_string, const T (&data)[N]) { + auto byte_array = ByteArray<N>::FromString(byte_string); ASSERT_TRUE(byte_array); - for (size_t i = 0; i < ByteArray<16>::kLength; i++) { - ASSERT_EQ(test_data[i], byte_array->bytes[i]); + for (size_t i = 0; i < ByteArray<N>::kLength; i++) { + ASSERT_EQ(data[i], byte_array->bytes[i]); } } -TEST(ByteArrayTest, test_to_str) { - ByteArray<16> byte_array = { - {0x4C, 0x68, 0x38, 0x41, 0x39, 0xf5, 0x74, 0xd8, 0x36, 0xbc, 0xf3, 0x4e, 0x9d, 0xfb, 0x01, 0xbf}}; +template <typename T, size_t N> +void simple_to_string_test(const char* byte_string, const T (&data)[N]) { + const ByteArray<N> byte_array(data); std::string str = byte_array.ToString(); - ASSERT_STREQ(str.c_str(), test_bytes); -}
\ No newline at end of file + ASSERT_STREQ(str.c_str(), byte_string); +} + +template <typename T, size_t N> +void simple_from_legacy_string_test(const char* byte_string, const T (&data)[N]) { + auto byte_array = ByteArray<N>::FromLegacyConfigString(byte_string); + ASSERT_TRUE(byte_array); + + for (size_t i = 0; i < ByteArray<N>::kLength; i++) { + ASSERT_EQ(data[i], byte_array->bytes[i]); + } +} + +template <typename T, size_t N> +void simple_to_legacy_string_test(const char* byte_string, const T (&data)[N]) { + const ByteArray<N> byte_array(data); + std::string str = byte_array.ToLegacyConfigString(); + ASSERT_STREQ(str.c_str(), byte_string); +} + +} // namespace + +TEST(ByteArrayTest, test_simple_constructor) { + simple_constructor_test<const uint8_t, 16>(byte_data16); + simple_constructor_test<const uint8_t, 21>(byte_data21); + simple_constructor_test<const uint8_t, 23>(byte_data23); + simple_constructor_test<const uint8_t, 28>(byte_data28); +} + +TEST(ByteArrayTest, test_simple_const_constructor) { + simple_const_constructor_test<const uint8_t, 16>(byte_data16); + simple_const_constructor_test<const uint8_t, 21>(byte_data21); + simple_const_constructor_test<const uint8_t, 23>(byte_data23); + simple_const_constructor_test<const uint8_t, 28>(byte_data28); +} + +TEST(ByteArrayTest, test_simple_array_constructor) { + simple_array_constructor_test<const uint8_t, 16>(byte_data16); + simple_array_constructor_test<const uint8_t, 21>(byte_data21); + simple_array_constructor_test<const uint8_t, 23>(byte_data23); + simple_array_constructor_test<const uint8_t, 28>(byte_data28); +} + +TEST(ByteArrayTest, test_from_str) { + simple_from_string_test<const uint8_t, 16>(byte_string16, byte_data16); + simple_from_string_test<const uint8_t, 21>(byte_string21, byte_data21); + simple_from_string_test<const uint8_t, 23>(byte_string23, byte_data23); + simple_from_string_test<const uint8_t, 28>(byte_string28, byte_data28); +} + +TEST(ByteArrayTest, test_from_legacy_str) { + simple_from_legacy_string_test<const uint8_t, 16>(byte_string16, byte_data16); + simple_from_legacy_string_test<const uint8_t, 21>(byte_string21, byte_data21); + simple_from_legacy_string_test<const uint8_t, 23>(byte_string23, byte_data23); + simple_from_legacy_string_test<const uint8_t, 28>(byte_string28, byte_data28); +} + +TEST(ByteArrayTest, test_to_str) { + simple_to_string_test<const uint8_t, 16>(byte_string16, byte_data16); + simple_to_string_test<const uint8_t, 21>(byte_string21, byte_data21); + simple_to_string_test<const uint8_t, 23>(byte_string23, byte_data23); + simple_to_string_test<const uint8_t, 28>(byte_string28, byte_data28); +} + +TEST(ByteArrayTest, test_to_legacy_str) { + simple_to_legacy_string_test<const uint8_t, 16>(byte_string16, byte_data16); + simple_to_legacy_string_test<const uint8_t, 21>(byte_string21, byte_data21); + simple_to_legacy_string_test<const uint8_t, 23>(byte_string23, byte_data23); + simple_to_legacy_string_test<const uint8_t, 28>(byte_string28, byte_data28); +} diff --git a/system/gd/common/list_map_test.cc b/system/gd/common/list_map_test.cc index 580616f1a5..b587e01672 100644 --- a/system/gd/common/list_map_test.cc +++ b/system/gd/common/list_map_test.cc @@ -14,7 +14,6 @@ * limitations under the License. */ -#include <chrono> #include <memory> #include <gmock/gmock.h> @@ -329,7 +328,6 @@ TEST(ListMapTest, for_loop_test) { } TEST(ListMapTest, pressure_test) { - auto started = std::chrono::high_resolution_clock::now(); int num_entries = 0xFFFF; // 2^16 = 65535 ListMap<int, int> list_map; @@ -351,13 +349,6 @@ TEST(ListMapTest, pressure_test) { EXPECT_TRUE(list_map.extract(key)); } EXPECT_EQ(list_map.size(), 0ul); - - // test execution time - auto done = std::chrono::high_resolution_clock::now(); - int execution_time = std::chrono::duration_cast<std::chrono::microseconds>(done - started).count(); - // Shouldn't be more than 1000ms - int execution_time_per_cycle_us = 10; - EXPECT_LT(execution_time, execution_time_per_cycle_us * num_entries); } } // namespace testing diff --git a/system/gd/common/lru_cache_test.cc b/system/gd/common/lru_cache_test.cc index d4aebf3f6e..8018d2a62b 100644 --- a/system/gd/common/lru_cache_test.cc +++ b/system/gd/common/lru_cache_test.cc @@ -14,7 +14,6 @@ * limitations under the License. */ -#include <chrono> #include <limits> #include <gmock/gmock.h> @@ -419,7 +418,6 @@ TEST(LruCacheTest, for_loop_test) { } TEST(LruCacheTest, pressure_test) { - auto started = std::chrono::high_resolution_clock::now(); int capacity = 0xFFFF; // 2^16 = 65535 LruCache<int, int> cache(static_cast<size_t>(capacity)); @@ -449,13 +447,6 @@ TEST(LruCacheTest, pressure_test) { EXPECT_TRUE(cache.extract(key)); } EXPECT_EQ(cache.size(), 0ul); - - // test execution time - auto done = std::chrono::high_resolution_clock::now(); - int execution_time = std::chrono::duration_cast<std::chrono::microseconds>(done - started).count(); - // Shouldn't be more than 1120ms - int execution_time_per_cycle_us = 17; - EXPECT_LT(execution_time, execution_time_per_cycle_us * capacity); } } // namespace testing diff --git a/system/gd/dumpsys/Android.bp b/system/gd/dumpsys/Android.bp index 22456518e0..a7fdaa2dcd 100644 --- a/system/gd/dumpsys/Android.bp +++ b/system/gd/dumpsys/Android.bp @@ -177,7 +177,7 @@ cc_library { cc_test { name: "bluetooth_flatbuffer_tests", test_suites: ["device-tests"], - defaults: ["mts_defaults"], + defaults: ["fluoride_common_options", "mts_defaults"], host_supported: true, test_options: { unit_test: true, @@ -192,9 +192,4 @@ cc_test { generated_headers: [ "BluetoothFlatbufferTestData_h", ], - cflags: [ - "-Werror", - "-Wall", - "-Wextra", - ], } diff --git a/system/gd/dumpsys/bundler/Android.bp b/system/gd/dumpsys/bundler/Android.bp index ce2c4e289c..3754611b23 100644 --- a/system/gd/dumpsys/bundler/Android.bp +++ b/system/gd/dumpsys/bundler/Android.bp @@ -46,13 +46,8 @@ genrule { cc_defaults { name: "bluetooth_flatbuffer_bundler_defaults", + defaults: ["fluoride_common_options"], cpp_std: "c++17", - cflags: [ - "-Wall", - "-Werror", - "-Wno-unused-parameter", - "-Wno-unused-variable", - ], generated_headers: [ "BluetoothGeneratedBundlerSchema_h_bfbs", ], diff --git a/system/gd/dumpsys/bundler/bundler.cc b/system/gd/dumpsys/bundler/bundler.cc index 08a2cca6f1..f2e6559a37 100644 --- a/system/gd/dumpsys/bundler/bundler.cc +++ b/system/gd/dumpsys/bundler/bundler.cc @@ -153,7 +153,7 @@ void WriteHeaderFile(FILE* fp, const uint8_t* data, size_t data_len) { fprintf(fp, "extern const std::string& GetBundledSchemaData();\n"); fprintf(fp, "const unsigned char %sdata_[%zu] = {\n", namespace_prefix.c_str(), data_len); - for (auto i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { fprintf(fp, " 0x%02x", data[i]); if (i != data_len - 1) { fprintf(fp, ","); diff --git a/system/gd/dumpsys/bundler/test.cc b/system/gd/dumpsys/bundler/test.cc index 929b6fff33..24d2f913f2 100644 --- a/system/gd/dumpsys/bundler/test.cc +++ b/system/gd/dumpsys/bundler/test.cc @@ -66,7 +66,7 @@ TEST_F(BundlerTest, CreateBinarySchemaBundle) { std::vector<flatbuffers::Offset<bluetooth::dumpsys::BundledSchemaMap>> vector_map; std::list<std::string> bundled_names; ASSERT_TRUE(CreateBinarySchemaBundle(&builder, filenames, &vector_map, &bundled_names)); - ASSERT_EQ(0, vector_map.size()); + ASSERT_EQ((unsigned int)0, vector_map.size()); } TEST_F(BundlerTest, WriteHeaderFile) { diff --git a/system/gd/hal/hci_hal_host.cc b/system/gd/hal/hci_hal_host.cc index a68434b127..d026113081 100644 --- a/system/gd/hal/hci_hal_host.cc +++ b/system/gd/hal/hci_hal_host.cc @@ -362,6 +362,8 @@ class HciHalHost : public HciHal { ASSERT_LOG(received_size != -1, "Can't receive from socket: %s", strerror(errno)); if (received_size == 0) { LOG_WARN("Can't read H4 header. EOF received"); + // First close sock fd before raising sigint + close(sock_fd_); raise(SIGINT); return; } diff --git a/system/gd/hci/Android.bp b/system/gd/hci/Android.bp index a105c42a72..cc1439f083 100644 --- a/system/gd/hci/Android.bp +++ b/system/gd/hci/Android.bp @@ -34,6 +34,7 @@ filegroup { name: "BluetoothHciUnitTestSources", srcs: [ "acl_manager/le_impl_test.cc", + "acl_manager/classic_acl_connection_test.cc", "acl_builder_test.cc", "acl_manager_unittest.cc", "address_unittest.cc", diff --git a/system/gd/hci/acl_manager/classic_acl_connection_test.cc b/system/gd/hci/acl_manager/classic_acl_connection_test.cc new file mode 100644 index 0000000000..ea5d89ef84 --- /dev/null +++ b/system/gd/hci/acl_manager/classic_acl_connection_test.cc @@ -0,0 +1,338 @@ +/* + * 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 "hci/acl_manager/classic_acl_connection.h" + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include <chrono> +#include <cstdint> +#include <future> +#include <list> +#include <memory> +#include <mutex> +#include <queue> +#include <vector> + +#include "hci/acl_connection_interface.h" +#include "hci/acl_manager/connection_management_callbacks.h" +#include "hci/address.h" +#include "hci/hci_packets.h" +#include "os/handler.h" +#include "os/log.h" +#include "os/thread.h" + +using namespace bluetooth; +using namespace std::chrono_literals; + +namespace { +constexpr char kAddress[] = "00:11:22:33:44:55"; +constexpr uint16_t kConnectionHandle = 123; +constexpr size_t kQueueSize = 10; + +std::vector<hci::DisconnectReason> disconnect_reason_vector = { + hci::DisconnectReason::AUTHENTICATION_FAILURE, + hci::DisconnectReason::REMOTE_USER_TERMINATED_CONNECTION, + hci::DisconnectReason::REMOTE_DEVICE_TERMINATED_CONNECTION_LOW_RESOURCES, + hci::DisconnectReason::REMOTE_DEVICE_TERMINATED_CONNECTION_POWER_OFF, + hci::DisconnectReason::UNSUPPORTED_REMOTE_FEATURE, + hci::DisconnectReason::PAIRING_WITH_UNIT_KEY_NOT_SUPPORTED, + hci::DisconnectReason::UNACCEPTABLE_CONNECTION_PARAMETERS, +}; + +std::vector<hci::ErrorCode> error_code_vector = { + hci::ErrorCode::SUCCESS, + hci::ErrorCode::UNKNOWN_HCI_COMMAND, + hci::ErrorCode::UNKNOWN_CONNECTION, + hci::ErrorCode::HARDWARE_FAILURE, + hci::ErrorCode::PAGE_TIMEOUT, + hci::ErrorCode::AUTHENTICATION_FAILURE, + hci::ErrorCode::PIN_OR_KEY_MISSING, + hci::ErrorCode::MEMORY_CAPACITY_EXCEEDED, + hci::ErrorCode::CONNECTION_TIMEOUT, + hci::ErrorCode::CONNECTION_LIMIT_EXCEEDED, + hci::ErrorCode::SYNCHRONOUS_CONNECTION_LIMIT_EXCEEDED, + hci::ErrorCode::CONNECTION_ALREADY_EXISTS, + hci::ErrorCode::COMMAND_DISALLOWED, + hci::ErrorCode::CONNECTION_REJECTED_LIMITED_RESOURCES, + hci::ErrorCode::CONNECTION_REJECTED_SECURITY_REASONS, + hci::ErrorCode::CONNECTION_REJECTED_UNACCEPTABLE_BD_ADDR, + hci::ErrorCode::CONNECTION_ACCEPT_TIMEOUT, + hci::ErrorCode::UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE, + hci::ErrorCode::INVALID_HCI_COMMAND_PARAMETERS, + hci::ErrorCode::REMOTE_USER_TERMINATED_CONNECTION, + hci::ErrorCode::REMOTE_DEVICE_TERMINATED_CONNECTION_LOW_RESOURCES, + hci::ErrorCode::REMOTE_DEVICE_TERMINATED_CONNECTION_POWER_OFF, + hci::ErrorCode::CONNECTION_TERMINATED_BY_LOCAL_HOST, + hci::ErrorCode::REPEATED_ATTEMPTS, + hci::ErrorCode::PAIRING_NOT_ALLOWED, + hci::ErrorCode::UNKNOWN_LMP_PDU, + hci::ErrorCode::UNSUPPORTED_REMOTE_OR_LMP_FEATURE, + hci::ErrorCode::SCO_OFFSET_REJECTED, + hci::ErrorCode::SCO_INTERVAL_REJECTED, + hci::ErrorCode::SCO_AIR_MODE_REJECTED, + hci::ErrorCode::INVALID_LMP_OR_LL_PARAMETERS, + hci::ErrorCode::UNSPECIFIED_ERROR, + hci::ErrorCode::UNSUPPORTED_LMP_OR_LL_PARAMETER, + hci::ErrorCode::ROLE_CHANGE_NOT_ALLOWED, + hci::ErrorCode::TRANSACTION_RESPONSE_TIMEOUT, + hci::ErrorCode::LINK_LAYER_COLLISION, + hci::ErrorCode::ENCRYPTION_MODE_NOT_ACCEPTABLE, + hci::ErrorCode::ROLE_SWITCH_FAILED, + hci::ErrorCode::CONTROLLER_BUSY, + hci::ErrorCode::ADVERTISING_TIMEOUT, + hci::ErrorCode::CONNECTION_FAILED_ESTABLISHMENT, + hci::ErrorCode::LIMIT_REACHED, + hci::ErrorCode::STATUS_UNKNOWN, +}; + +// Generic template for all commands +template <typename T, typename U> +T CreateCommand(U u) { + T command; + return command; +} + +template <> +hci::DisconnectView CreateCommand(std::shared_ptr<std::vector<uint8_t>> bytes) { + return hci::DisconnectView::Create( + hci::AclCommandView::Create(hci::CommandView::Create(hci::PacketView<hci::kLittleEndian>(bytes)))); +} + +} // namespace + +class TestAclConnectionInterface : public hci::AclConnectionInterface { + private: + void EnqueueCommand( + std::unique_ptr<hci::AclCommandBuilder> command, + common::ContextualOnceCallback<void(hci::CommandStatusView)> on_status) override { + const std::lock_guard<std::mutex> lock(command_queue_mutex_); + command_queue_.push(std::move(command)); + command_status_callbacks.push_back(std::move(on_status)); + if (command_promise_ != nullptr) { + command_promise_->set_value(); + command_promise_.reset(); + } + } + + void EnqueueCommand( + std::unique_ptr<hci::AclCommandBuilder> command, + common::ContextualOnceCallback<void(hci::CommandCompleteView)> on_complete) override { + const std::lock_guard<std::mutex> lock(command_queue_mutex_); + command_queue_.push(std::move(command)); + command_complete_callbacks.push_back(std::move(on_complete)); + if (command_promise_ != nullptr) { + command_promise_->set_value(); + command_promise_.reset(); + } + } + + public: + virtual ~TestAclConnectionInterface() = default; + + std::unique_ptr<hci::CommandBuilder> DequeueCommand() { + const std::lock_guard<std::mutex> lock(command_queue_mutex_); + auto packet = std::move(command_queue_.front()); + command_queue_.pop(); + return std::move(packet); + } + + std::shared_ptr<std::vector<uint8_t>> DequeueCommandBytes() { + auto command = DequeueCommand(); + auto bytes = std::make_shared<std::vector<uint8_t>>(); + packet::BitInserter bi(*bytes); + command->Serialize(bi); + return bytes; + } + + bool IsPacketQueueEmpty() const { + const std::lock_guard<std::mutex> lock(command_queue_mutex_); + return command_queue_.empty(); + } + + size_t NumberOfQueuedCommands() const { + const std::lock_guard<std::mutex> lock(command_queue_mutex_); + return command_queue_.size(); + } + + private: + std::list<common::ContextualOnceCallback<void(hci::CommandCompleteView)>> command_complete_callbacks; + std::list<common::ContextualOnceCallback<void(hci::CommandStatusView)>> command_status_callbacks; + std::queue<std::unique_ptr<hci::CommandBuilder>> command_queue_; + mutable std::mutex command_queue_mutex_; + std::unique_ptr<std::promise<void>> command_promise_; + std::unique_ptr<std::future<void>> command_future_; +}; + +class TestConnectionManagementCallbacks : public hci::acl_manager::ConnectionManagementCallbacks { + public: + ~TestConnectionManagementCallbacks() = default; + void OnConnectionPacketTypeChanged(uint16_t packet_type) override {} + void OnAuthenticationComplete(hci::ErrorCode hci_status) override {} + void OnEncryptionChange(hci::EncryptionEnabled enabled) override {} + void OnChangeConnectionLinkKeyComplete() override {} + void OnReadClockOffsetComplete(uint16_t clock_offset) override {} + void OnModeChange(hci::ErrorCode status, hci::Mode current_mode, uint16_t interval) override {} + void OnSniffSubrating( + hci::ErrorCode hci_status, + uint16_t maximum_transmit_latency, + uint16_t maximum_receive_latency, + uint16_t minimum_remote_timeout, + uint16_t minimum_local_timeout) override {} + void OnQosSetupComplete( + hci::ServiceType service_type, + uint32_t token_rate, + uint32_t peak_bandwidth, + uint32_t latency, + uint32_t delay_variation) override {} + void OnFlowSpecificationComplete( + hci::FlowDirection flow_direction, + hci::ServiceType service_type, + uint32_t token_rate, + uint32_t token_bucket_size, + uint32_t peak_bandwidth, + uint32_t access_latency) override {} + void OnFlushOccurred() override {} + void OnRoleDiscoveryComplete(hci::Role current_role) override {} + void OnReadLinkPolicySettingsComplete(uint16_t link_policy_settings) override {} + void OnReadAutomaticFlushTimeoutComplete(uint16_t flush_timeout) override {} + void OnReadTransmitPowerLevelComplete(uint8_t transmit_power_level) override {} + void OnReadLinkSupervisionTimeoutComplete(uint16_t link_supervision_timeout) override {} + void OnReadFailedContactCounterComplete(uint16_t failed_contact_counter) override {} + void OnReadLinkQualityComplete(uint8_t link_quality) override {} + void OnReadAfhChannelMapComplete(hci::AfhMode afh_mode, std::array<uint8_t, 10> afh_channel_map) override {} + void OnReadRssiComplete(uint8_t rssi) override {} + void OnReadClockComplete(uint32_t clock, uint16_t accuracy) override {} + void OnCentralLinkKeyComplete(hci::KeyFlag key_flag) override {} + void OnRoleChange(hci::ErrorCode hci_status, hci::Role new_role) override {} + void OnDisconnection(hci::ErrorCode reason) override { + on_disconnection_error_code_queue_.push(reason); + } + void OnReadRemoteVersionInformationComplete( + hci::ErrorCode hci_status, uint8_t lmp_version, uint16_t manufacturer_name, uint16_t sub_version) override {} + void OnReadRemoteSupportedFeaturesComplete(uint64_t features) override {} + void OnReadRemoteExtendedFeaturesComplete(uint8_t page_number, uint8_t max_page_number, uint64_t features) override {} + + std::queue<hci::ErrorCode> on_disconnection_error_code_queue_; +}; + +namespace bluetooth { +namespace hci { +namespace acl_manager { + +class ClassicAclConnectionTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_TRUE(hci::Address::FromString(kAddress, address_)); + thread_ = new os::Thread("thread", os::Thread::Priority::NORMAL); + handler_ = new os::Handler(thread_); + queue_ = std::make_shared<hci::acl_manager::AclConnection::Queue>(kQueueSize); + sync_handler(); + } + + void TearDown() override { + handler_->Clear(); + delete handler_; + delete thread_; + } + + void sync_handler() { + ASSERT(handler_ != nullptr); + + auto promise = std::promise<void>(); + auto future = promise.get_future(); + handler_->BindOnceOn(&promise, &std::promise<void>::set_value).Invoke(); + auto status = future.wait_for(2s); + ASSERT_EQ(status, std::future_status::ready); + } + + Address address_; + os::Handler* handler_{nullptr}; + os::Thread* thread_{nullptr}; + std::shared_ptr<hci::acl_manager::AclConnection::Queue> queue_; + + TestAclConnectionInterface acl_connection_interface_; + TestConnectionManagementCallbacks callbacks_; +}; + +TEST_F(ClassicAclConnectionTest, simple) { + AclConnectionInterface* acl_connection_interface = nullptr; + ClassicAclConnection* connection = + new ClassicAclConnection(queue_, acl_connection_interface, kConnectionHandle, address_); + connection->RegisterCallbacks(&callbacks_, handler_); + + delete connection; +} + +class ClassicAclConnectionWithCallbacksTest : public ClassicAclConnectionTest { + protected: + void SetUp() override { + ClassicAclConnectionTest::SetUp(); + connection_ = + std::make_unique<ClassicAclConnection>(queue_, &acl_connection_interface_, kConnectionHandle, address_); + connection_->RegisterCallbacks(&callbacks_, handler_); + is_callbacks_registered_ = true; + connection_management_callbacks_ = + connection_->GetEventCallbacks([this](uint16_t hci_handle) { is_callbacks_invalidated_ = true; }); + is_callbacks_invalidated_ = false; + } + + void TearDown() override { + connection_.reset(); + ASSERT_TRUE(is_callbacks_invalidated_); + ClassicAclConnectionTest::TearDown(); + } + + protected: + std::unique_ptr<ClassicAclConnection> connection_; + ConnectionManagementCallbacks* connection_management_callbacks_; + bool is_callbacks_registered_{false}; + bool is_callbacks_invalidated_{false}; +}; + +TEST_F(ClassicAclConnectionWithCallbacksTest, Disconnect) { + for (const auto& reason : disconnect_reason_vector) { + ASSERT_TRUE(connection_->Disconnect(reason)); + } + + for (const auto& reason : disconnect_reason_vector) { + ASSERT_FALSE(acl_connection_interface_.IsPacketQueueEmpty()); + auto command = CreateCommand<DisconnectView>(acl_connection_interface_.DequeueCommandBytes()); + ASSERT_TRUE(command.IsValid()); + ASSERT_EQ(reason, command.GetReason()); + ASSERT_EQ(kConnectionHandle, command.GetConnectionHandle()); + } + ASSERT_TRUE(acl_connection_interface_.IsPacketQueueEmpty()); +} + +TEST_F(ClassicAclConnectionWithCallbacksTest, OnDisconnection) { + for (const auto& error_code : error_code_vector) { + connection_management_callbacks_->OnDisconnection(error_code); + } + + sync_handler(); + ASSERT_TRUE(!callbacks_.on_disconnection_error_code_queue_.empty()); + + for (const auto& error_code : error_code_vector) { + ASSERT_EQ(error_code, callbacks_.on_disconnection_error_code_queue_.front()); + callbacks_.on_disconnection_error_code_queue_.pop(); + } +} + +} // namespace acl_manager +} // namespace hci +} // namespace bluetooth diff --git a/system/gd/hci/acl_manager/le_impl_test.cc b/system/gd/hci/acl_manager/le_impl_test.cc index 02129e8dae..e1238d49d6 100644 --- a/system/gd/hci/acl_manager/le_impl_test.cc +++ b/system/gd/hci/acl_manager/le_impl_test.cc @@ -47,17 +47,31 @@ using ::bluetooth::packet::BitInserter; using ::bluetooth::packet::RawBuilder; using ::bluetooth::testing::LogCapture; +using ::testing::_; +using ::testing::DoAll; +using ::testing::SaveArg; + namespace { +constexpr bool kCrashOnUnknownHandle = true; constexpr char kFixedAddress[] = "c0:aa:bb:cc:dd:ee"; +constexpr char kLocalRandomAddress[] = "04:c0:aa:bb:cc:dd:ee"; +constexpr char kRemoteRandomAddress[] = "04:11:22:33:44:55"; constexpr char kRemoteAddress[] = "00:11:22:33:44:55"; +constexpr uint16_t kHciHandle = 123; [[maybe_unused]] constexpr bool kAddToFilterAcceptList = true; [[maybe_unused]] constexpr bool kSkipFilterAcceptList = !kAddToFilterAcceptList; [[maybe_unused]] constexpr bool kIsDirectConnection = true; [[maybe_unused]] constexpr bool kIsBackgroundConnection = !kIsDirectConnection; -[[maybe_unused]] constexpr ::bluetooth::crypto_toolbox::Octet16 kRotationIrk = {}; -[[maybe_unused]] constexpr std::chrono::milliseconds kMinimumRotationTime(14 * 1000); -[[maybe_unused]] constexpr std::chrono::milliseconds kMaximumRotationTime(16 * 1000); -[[maybe_unused]] constexpr std::array<uint8_t, 16> kPeerIdentityResolvingKey({ +constexpr crypto_toolbox::Octet16 kRotationIrk = {}; +constexpr std::chrono::milliseconds kMinimumRotationTime(14 * 1000); +constexpr std::chrono::milliseconds kMaximumRotationTime(16 * 1000); +constexpr uint16_t kIntervalMax = 0x40; +constexpr uint16_t kIntervalMin = 0x20; +constexpr uint16_t kLatency = 0x60; +constexpr uint16_t kLength = 0x5678; +constexpr uint16_t kTime = 0x1234; +constexpr uint16_t kTimeout = 0x80; +constexpr std::array<uint8_t, 16> kPeerIdentityResolvingKey({ 0x00, 0x01, 0x02, @@ -75,7 +89,7 @@ constexpr char kRemoteAddress[] = "00:11:22:33:44:55"; 0x0e, 0x0f, }); -[[maybe_unused]] constexpr std::array<uint8_t, 16> kLocalIdentityResolvingKey({ +constexpr std::array<uint8_t, 16> kLocalIdentityResolvingKey({ 0x80, 0x81, 0x82, @@ -94,47 +108,51 @@ constexpr char kRemoteAddress[] = "00:11:22:33:44:55"; 0x8f, }); -// Generic template for all commands -template <typename T, typename U> -T CreateCommand(U u) { - T command; - return command; +template <typename B> +std::shared_ptr<std::vector<uint8_t>> Serialize(std::unique_ptr<B> build) { + auto bytes = std::make_shared<std::vector<uint8_t>>(); + BitInserter bi(*bytes); + build->Serialize(bi); + return bytes; +} + +template <typename T> +T CreateCommandView(std::shared_ptr<std::vector<uint8_t>> bytes) { + return T::Create(hci::CommandView::Create(hci::PacketView<hci::kLittleEndian>(bytes))); } -template <> -[[maybe_unused]] hci::LeSetAddressResolutionEnableView CreateCommand(std::shared_ptr<std::vector<uint8_t>> bytes) { - return hci::LeSetAddressResolutionEnableView::Create( - hci::LeSecurityCommandView::Create(hci::CommandView::Create(hci::PacketView<hci::kLittleEndian>(bytes)))); +template <typename T> +T CreateAclCommandView(std::shared_ptr<std::vector<uint8_t>> bytes) { + return T::Create(CreateCommandView<hci::AclCommandView>(bytes)); } -template <> -[[maybe_unused]] hci::LeAddDeviceToResolvingListView CreateCommand(std::shared_ptr<std::vector<uint8_t>> bytes) { - return hci::LeAddDeviceToResolvingListView::Create( - hci::LeSecurityCommandView::Create(hci::CommandView::Create(hci::PacketView<hci::kLittleEndian>(bytes)))); +template <typename T> +T CreateLeConnectionManagementCommandView(std::shared_ptr<std::vector<uint8_t>> bytes) { + return T::Create(CreateAclCommandView<hci::LeConnectionManagementCommandView>(bytes)); } -template <> -[[maybe_unused]] hci::LeSetPrivacyModeView CreateCommand(std::shared_ptr<std::vector<uint8_t>> bytes) { - return hci::LeSetPrivacyModeView::Create( - hci::LeSecurityCommandView::Create(hci::CommandView::Create(hci::PacketView<hci::kLittleEndian>(bytes)))); +template <typename T> +T CreateLeSecurityCommandView(std::shared_ptr<std::vector<uint8_t>> bytes) { + return T::Create(CreateCommandView<hci::LeSecurityCommandView>(bytes)); +} + +template <typename T> +T CreateLeEventView(std::shared_ptr<std::vector<uint8_t>> bytes) { + return T::Create(hci::LeMetaEventView::Create(hci::EventView::Create(hci::PacketView<hci::kLittleEndian>(bytes)))); } [[maybe_unused]] hci::CommandCompleteView ReturnCommandComplete(hci::OpCode op_code, hci::ErrorCode error_code) { std::vector<uint8_t> success_vector{static_cast<uint8_t>(error_code)}; - auto bytes = std::make_shared<std::vector<uint8_t>>(); - BitInserter bi(*bytes); auto builder = hci::CommandCompleteBuilder::Create(uint8_t{1}, op_code, std::make_unique<RawBuilder>(success_vector)); - builder->Serialize(bi); + auto bytes = Serialize<hci::CommandCompleteBuilder>(std::move(builder)); return hci::CommandCompleteView::Create(hci::EventView::Create(hci::PacketView<hci::kLittleEndian>(bytes))); } [[maybe_unused]] hci::CommandStatusView ReturnCommandStatus(hci::OpCode op_code, hci::ErrorCode error_code) { std::vector<uint8_t> success_vector{static_cast<uint8_t>(error_code)}; - auto bytes = std::make_shared<std::vector<uint8_t>>(); - BitInserter bi(*bytes); auto builder = hci::CommandStatusBuilder::Create( hci::ErrorCode::SUCCESS, uint8_t{1}, op_code, std::make_unique<RawBuilder>(success_vector)); - builder->Serialize(bi); + auto bytes = Serialize<hci::CommandStatusBuilder>(std::move(builder)); return hci::CommandStatusView::Create(hci::EventView::Create(hci::PacketView<hci::kLittleEndian>(bytes))); } @@ -364,17 +382,46 @@ class TestHciLayer : public HciLayer { CommandInterfaceImpl<AclCommandBuilder> le_acl_connection_manager_interface_{*this}; }; -class LeConnectionCallbacksTest : public LeConnectionCallbacks { +class MockLeConnectionCallbacks : public LeConnectionCallbacks { public: - virtual ~LeConnectionCallbacksTest() = default; - virtual void OnLeConnectSuccess( - AddressWithType address_with_type, std::unique_ptr<LeAclConnection> connection) override {} - virtual void OnLeConnectFail(AddressWithType address_with_type, ErrorCode reason) override {} + MOCK_METHOD( + void, + OnLeConnectSuccess, + (AddressWithType address_with_type, std::unique_ptr<LeAclConnection> connection), + (override)); + MOCK_METHOD(void, OnLeConnectFail, (AddressWithType address_with_type, ErrorCode reason), (override)); +}; + +class MockLeConnectionManagementCallbacks : public LeConnectionManagementCallbacks { + public: + MOCK_METHOD( + void, + OnConnectionUpdate, + (hci::ErrorCode hci_status, + uint16_t connection_interval, + uint16_t connection_latency, + uint16_t supervision_timeout), + (override)); + MOCK_METHOD( + void, + OnDataLengthChange, + (uint16_t tx_octets, uint16_t tx_time, uint16_t rx_octets, uint16_t rx_time), + (override)); + MOCK_METHOD(void, OnDisconnection, (ErrorCode reason), (override)); + MOCK_METHOD( + void, + OnReadRemoteVersionInformationComplete, + (hci::ErrorCode hci_status, uint8_t lmp_version, uint16_t manufacturer_name, uint16_t sub_version), + (override)); + MOCK_METHOD(void, OnLeReadRemoteFeaturesComplete, (hci::ErrorCode hci_status, uint64_t features), (override)); + MOCK_METHOD(void, OnPhyUpdate, (hci::ErrorCode hci_status, uint8_t tx_phy, uint8_t rx_phy), (override)); + MOCK_METHOD(void, OnLocalAddressUpdate, (AddressWithType address_with_type), (override)); }; class LeImplTest : public ::testing::Test { protected: void SetUp() override { + bluetooth::common::InitFlags::SetAllForTesting(); thread_ = new Thread("thread", Thread::Priority::NORMAL); handler_ = new Handler(thread_); controller_ = new TestController(); @@ -383,15 +430,18 @@ class LeImplTest : public ::testing::Test { round_robin_scheduler_ = new RoundRobinScheduler(handler_, controller_, hci_queue_.GetUpEnd()); hci_queue_.GetDownEnd()->RegisterDequeue( handler_, common::Bind(&LeImplTest::HciDownEndDequeue, common::Unretained(this))); - le_impl_ = new le_impl(hci_layer_, controller_, handler_, round_robin_scheduler_, true); + le_impl_ = new le_impl(hci_layer_, controller_, handler_, round_robin_scheduler_, kCrashOnUnknownHandle); le_impl_->handle_register_le_callbacks(&mock_le_connection_callbacks_, handler_); Address address; Address::FromString(kFixedAddress, address); fixed_address_ = AddressWithType(address, AddressType::PUBLIC_DEVICE_ADDRESS); - Address::FromString(kRemoteAddress, address); - remote_public_address_ = AddressWithType(address, AddressType::PUBLIC_DEVICE_ADDRESS); + Address::FromString(kRemoteAddress, remote_address_); + remote_public_address_with_type_ = AddressWithType(remote_address_, AddressType::PUBLIC_DEVICE_ADDRESS); + + Address::FromString(kLocalRandomAddress, local_rpa_); + Address::FromString(kRemoteRandomAddress, remote_rpa_); } void set_random_device_address_policy() { @@ -464,16 +514,6 @@ class LeImplTest : public ::testing::Test { } } - class MockLeConnectionCallbacks : public LeConnectionCallbacks { - public: - MOCK_METHOD( - void, - OnLeConnectSuccess, - (AddressWithType address_with_type, std::unique_ptr<LeAclConnection> connection), - (override)); - MOCK_METHOD(void, OnLeConnectFail, (AddressWithType, ErrorCode reason), (override)); - } mock_le_connection_callbacks_; - protected: void set_privacy_policy_for_initiator_address( const AddressWithType& address, const LeAddressManager::AddressPolicy& policy) { @@ -481,8 +521,12 @@ class LeImplTest : public ::testing::Test { policy, address, kRotationIrk, kMinimumRotationTime, kMaximumRotationTime); } + Address remote_address_; AddressWithType fixed_address_; AddressWithType remote_public_address_; + Address local_rpa_; + Address remote_rpa_; + AddressWithType remote_public_address_with_type_; uint16_t packet_count_; std::unique_ptr<std::promise<void>> packet_promise_; @@ -497,15 +541,22 @@ class LeImplTest : public ::testing::Test { TestController* controller_; RoundRobinScheduler* round_robin_scheduler_{nullptr}; - LeConnectionCallbacksTest connection_callbacks_; + MockLeConnectionCallbacks mock_le_connection_callbacks_; + MockLeConnectionManagementCallbacks connection_management_callbacks_; + struct le_impl* le_impl_; }; -class LeImplWithCallbacksTest : public LeImplTest { +class LeImplRegisteredWithAddressManagerTest : public LeImplTest { protected: void SetUp() override { LeImplTest::SetUp(); - le_impl_->handle_register_le_callbacks(&connection_callbacks_, handler_); + set_privacy_policy_for_initiator_address(fixed_address_, LeAddressManager::AddressPolicy::USE_PUBLIC_ADDRESS); + + le_impl_->register_with_address_manager(); + sync_handler(); // Let |LeAddressManager::register_client| execute on handler + ASSERT_TRUE(le_impl_->address_manager_registered); + ASSERT_TRUE(le_impl_->pause_connection); } void TearDown() override { @@ -513,7 +564,47 @@ class LeImplWithCallbacksTest : public LeImplTest { } }; -TEST_F(LeImplTest, nop) {} +class LeImplWithConnectionTest : public LeImplTest { + protected: + void SetUp() override { + LeImplTest::SetUp(); + + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(_, _)) + .WillOnce([&](AddressWithType addr, std::unique_ptr<LeAclConnection> conn) { + remote_address_with_type_ = addr; + connection_ = std::move(conn); + connection_->RegisterCallbacks(&connection_management_callbacks_, handler_); + }); + + auto command = LeEnhancedConnectionCompleteBuilder::Create( + ErrorCode::SUCCESS, + kHciHandle, + Role::PERIPHERAL, + AddressType::PUBLIC_DEVICE_ADDRESS, + remote_address_, + local_rpa_, + remote_rpa_, + 0x0024, + 0x0000, + 0x0011, + ClockAccuracy::PPM_30); + auto bytes = Serialize<LeEnhancedConnectionCompleteBuilder>(std::move(command)); + auto view = CreateLeEventView<hci::LeEnhancedConnectionCompleteView>(bytes); + ASSERT_TRUE(view.IsValid()); + le_impl_->on_le_event(view); + + sync_handler(); + ASSERT_EQ(remote_public_address_with_type_, remote_address_with_type_); + } + + void TearDown() override { + connection_.reset(); + LeImplTest::TearDown(); + } + + AddressWithType remote_address_with_type_; + std::unique_ptr<LeAclConnection> connection_; +}; TEST_F(LeImplTest, add_device_to_connect_list) { le_impl_->add_device_to_connect_list({{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, AddressType::PUBLIC_DEVICE_ADDRESS}); @@ -574,7 +665,7 @@ TEST_F(LeImplTest, connection_complete_with_periperal_role) { hci::Address remote_address; Address::FromString("D0:05:04:03:02:01", remote_address); hci::AddressWithType address_with_type(remote_address, hci::AddressType::PUBLIC_DEVICE_ADDRESS); - EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(address_with_type, ::testing::_)); + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(address_with_type, _)); hci_layer_->IncomingLeMetaEvent(LeConnectionCompleteBuilder::Create( ErrorCode::SUCCESS, 0x0041, @@ -613,7 +704,7 @@ TEST_F(LeImplTest, enhanced_connection_complete_with_periperal_role) { hci::Address remote_address; Address::FromString("D0:05:04:03:02:01", remote_address); hci::AddressWithType address_with_type(remote_address, hci::AddressType::PUBLIC_DEVICE_ADDRESS); - EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(address_with_type, ::testing::_)); + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(address_with_type, _)); hci_layer_->IncomingLeMetaEvent(LeEnhancedConnectionCompleteBuilder::Create( ErrorCode::SUCCESS, 0x0041, @@ -652,7 +743,7 @@ TEST_F(LeImplTest, connection_complete_with_central_role) { ASSERT_EQ(ConnectabilityState::ARMED, le_impl_->connectability_state_); // Receive connection complete of outgoing connection (Role::CENTRAL) - EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(address_with_type, ::testing::_)); + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(address_with_type, _)); hci_layer_->IncomingLeMetaEvent(LeConnectionCompleteBuilder::Create( ErrorCode::SUCCESS, 0x0041, @@ -690,7 +781,7 @@ TEST_F(LeImplTest, enhanced_connection_complete_with_central_role) { ASSERT_EQ(ConnectabilityState::ARMED, le_impl_->connectability_state_); // Receive connection complete of outgoing connection (Role::CENTRAL) - EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(address_with_type, ::testing::_)); + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(address_with_type, _)); hci_layer_->IncomingLeMetaEvent(LeEnhancedConnectionCompleteBuilder::Create( ErrorCode::SUCCESS, 0x0041, @@ -710,7 +801,6 @@ TEST_F(LeImplTest, enhanced_connection_complete_with_central_role) { } TEST_F(LeImplTest, register_with_address_manager__AddressPolicyNotSet) { - bluetooth::common::InitFlags::SetAllForTesting(); auto log_capture = std::make_unique<LogCapture>(); std::promise<void> promise; @@ -763,7 +853,6 @@ TEST_F(LeImplTest, register_with_address_manager__AddressPolicyNotSet) { } TEST_F(LeImplTest, disarm_connectability_DISARMED) { - bluetooth::common::InitFlags::SetAllForTesting(); std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); le_impl_->connectability_state_ = ConnectabilityState::DISARMED; @@ -777,7 +866,6 @@ TEST_F(LeImplTest, disarm_connectability_DISARMED) { } TEST_F(LeImplTest, disarm_connectability_DISARMED_extended) { - bluetooth::common::InitFlags::SetAllForTesting(); std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); le_impl_->connectability_state_ = ConnectabilityState::DISARMED; @@ -792,7 +880,6 @@ TEST_F(LeImplTest, disarm_connectability_DISARMED_extended) { } TEST_F(LeImplTest, disarm_connectability_ARMING) { - bluetooth::common::InitFlags::SetAllForTesting(); std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); le_impl_->connectability_state_ = ConnectabilityState::ARMING; @@ -805,7 +892,6 @@ TEST_F(LeImplTest, disarm_connectability_ARMING) { } TEST_F(LeImplTest, disarm_connectability_ARMING_extended) { - bluetooth::common::InitFlags::SetAllForTesting(); std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); le_impl_->connectability_state_ = ConnectabilityState::ARMING; @@ -820,7 +906,6 @@ TEST_F(LeImplTest, disarm_connectability_ARMING_extended) { } TEST_F(LeImplTest, disarm_connectability_ARMED) { - bluetooth::common::InitFlags::SetAllForTesting(); std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); le_impl_->connectability_state_ = ConnectabilityState::ARMED; @@ -834,7 +919,6 @@ TEST_F(LeImplTest, disarm_connectability_ARMED) { } TEST_F(LeImplTest, disarm_connectability_ARMED_extended) { - bluetooth::common::InitFlags::SetAllForTesting(); std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); le_impl_->connectability_state_ = ConnectabilityState::ARMED; @@ -849,7 +933,6 @@ TEST_F(LeImplTest, disarm_connectability_ARMED_extended) { } TEST_F(LeImplTest, disarm_connectability_DISARMING) { - bluetooth::common::InitFlags::SetAllForTesting(); std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); le_impl_->connectability_state_ = ConnectabilityState::DISARMING; @@ -863,7 +946,6 @@ TEST_F(LeImplTest, disarm_connectability_DISARMING) { } TEST_F(LeImplTest, disarm_connectability_DISARMING_extended) { - bluetooth::common::InitFlags::SetAllForTesting(); std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); le_impl_->connectability_state_ = ConnectabilityState::DISARMING; @@ -877,6 +959,503 @@ TEST_F(LeImplTest, disarm_connectability_DISARMING_extended) { ASSERT_TRUE(log_capture->Rewind()->Find("in unexpected state:ConnectabilityState::DISARMING")); } +TEST_F(LeImplTest, register_with_address_manager__AddressPolicyPublicAddress) { + std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); + + set_privacy_policy_for_initiator_address(fixed_address_, LeAddressManager::AddressPolicy::USE_PUBLIC_ADDRESS); + + le_impl_->register_with_address_manager(); + sync_handler(); // Let |eAddressManager::register_client| execute on handler + ASSERT_TRUE(le_impl_->address_manager_registered); + ASSERT_TRUE(le_impl_->pause_connection); + + le_impl_->ready_to_unregister = true; + + le_impl_->check_for_unregister(); + sync_handler(); // Let |LeAddressManager::unregister_client| execute on handler + ASSERT_FALSE(le_impl_->address_manager_registered); + ASSERT_FALSE(le_impl_->pause_connection); + + ASSERT_TRUE(log_capture->Rewind()->Find("SetPrivacyPolicyForInitiatorAddress with policy 1")); + ASSERT_TRUE(log_capture->Rewind()->Find("Client unregistered")); +} + +TEST_F(LeImplTest, register_with_address_manager__AddressPolicyStaticAddress) { + std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); + + set_privacy_policy_for_initiator_address(fixed_address_, LeAddressManager::AddressPolicy::USE_STATIC_ADDRESS); + + le_impl_->register_with_address_manager(); + sync_handler(); // Let |LeAddressManager::register_client| execute on handler + ASSERT_TRUE(le_impl_->address_manager_registered); + ASSERT_TRUE(le_impl_->pause_connection); + + le_impl_->ready_to_unregister = true; + + le_impl_->check_for_unregister(); + sync_handler(); // Let |LeAddressManager::unregister_client| execute on handler + ASSERT_FALSE(le_impl_->address_manager_registered); + ASSERT_FALSE(le_impl_->pause_connection); + + ASSERT_TRUE(log_capture->Rewind()->Find("SetPrivacyPolicyForInitiatorAddress with policy 2")); + ASSERT_TRUE(log_capture->Rewind()->Find("Client unregistered")); +} + +TEST_F(LeImplTest, register_with_address_manager__AddressPolicyNonResolvableAddress) { + std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); + + set_privacy_policy_for_initiator_address(fixed_address_, LeAddressManager::AddressPolicy::USE_NON_RESOLVABLE_ADDRESS); + + le_impl_->register_with_address_manager(); + sync_handler(); // Let |LeAddressManager::register_client| execute on handler + ASSERT_TRUE(le_impl_->address_manager_registered); + ASSERT_TRUE(le_impl_->pause_connection); + + le_impl_->ready_to_unregister = true; + + le_impl_->check_for_unregister(); + sync_handler(); // Let |LeAddressManager::unregister_client| execute on handler + ASSERT_FALSE(le_impl_->address_manager_registered); + ASSERT_FALSE(le_impl_->pause_connection); + + ASSERT_TRUE(log_capture->Rewind()->Find("SetPrivacyPolicyForInitiatorAddress with policy 3")); + ASSERT_TRUE(log_capture->Rewind()->Find("Client unregistered")); +} + +TEST_F(LeImplTest, register_with_address_manager__AddressPolicyResolvableAddress) { + std::unique_ptr<LogCapture> log_capture = std::make_unique<LogCapture>(); + + set_privacy_policy_for_initiator_address(fixed_address_, LeAddressManager::AddressPolicy::USE_RESOLVABLE_ADDRESS); + + le_impl_->register_with_address_manager(); + sync_handler(); // Let |LeAddressManager::register_client| execute on handler + ASSERT_TRUE(le_impl_->address_manager_registered); + ASSERT_TRUE(le_impl_->pause_connection); + + le_impl_->ready_to_unregister = true; + + le_impl_->check_for_unregister(); + sync_handler(); // Let |LeAddressManager::unregister_client| execute on handler + ASSERT_FALSE(le_impl_->address_manager_registered); + ASSERT_FALSE(le_impl_->pause_connection); + + ASSERT_TRUE(log_capture->Rewind()->Find("SetPrivacyPolicyForInitiatorAddress with policy 4")); + ASSERT_TRUE(log_capture->Rewind()->Find("Client unregistered")); +} + +TEST_F(LeImplTest, add_device_to_resolving_list) { + // Some kind of privacy policy must be set for LeAddressManager to operate properly + set_privacy_policy_for_initiator_address(fixed_address_, LeAddressManager::AddressPolicy::USE_PUBLIC_ADDRESS); + // Let LeAddressManager::resume_registered_clients execute + sync_handler(); + + ASSERT_EQ(0UL, hci_layer_->NumberOfQueuedCommands()); + + // le_impl should not be registered with address manager + ASSERT_FALSE(le_impl_->address_manager_registered); + ASSERT_FALSE(le_impl_->pause_connection); + + ASSERT_EQ(0UL, le_impl_->le_address_manager_->NumberCachedCommands()); + // Acknowledge that the le_impl has quiesced all relevant controller state + le_impl_->add_device_to_resolving_list( + remote_public_address_with_type_, kPeerIdentityResolvingKey, kLocalIdentityResolvingKey); + ASSERT_EQ(3UL, le_impl_->le_address_manager_->NumberCachedCommands()); + + sync_handler(); // Let |LeAddressManager::register_client| execute on handler + ASSERT_TRUE(le_impl_->address_manager_registered); + ASSERT_TRUE(le_impl_->pause_connection); + + le_impl_->le_address_manager_->AckPause(le_impl_); + sync_handler(); // Allow |LeAddressManager::ack_pause| to complete + + ASSERT_FALSE(hci_layer_->IsPacketQueueEmpty()); + { + // Inform controller to disable address resolution + auto command = CreateLeSecurityCommandView<LeSetAddressResolutionEnableView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(command.IsValid()); + ASSERT_EQ(Enable::DISABLED, command.GetAddressResolutionEnable()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_SET_ADDRESS_RESOLUTION_ENABLE, ErrorCode::SUCCESS)); + } + sync_handler(); // |LeAddressManager::check_cached_commands| + + ASSERT_FALSE(hci_layer_->IsPacketQueueEmpty()); + { + auto command = CreateLeSecurityCommandView<LeAddDeviceToResolvingListView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(command.IsValid()); + ASSERT_EQ(PeerAddressType::PUBLIC_DEVICE_OR_IDENTITY_ADDRESS, command.GetPeerIdentityAddressType()); + ASSERT_EQ(remote_public_address_with_type_.GetAddress(), command.GetPeerIdentityAddress()); + ASSERT_EQ(kPeerIdentityResolvingKey, command.GetPeerIrk()); + ASSERT_EQ(kLocalIdentityResolvingKey, command.GetLocalIrk()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_ADD_DEVICE_TO_RESOLVING_LIST, ErrorCode::SUCCESS)); + } + sync_handler(); // |LeAddressManager::check_cached_commands| + + ASSERT_FALSE(hci_layer_->IsPacketQueueEmpty()); + { + auto command = CreateLeSecurityCommandView<LeSetAddressResolutionEnableView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(command.IsValid()); + ASSERT_EQ(Enable::ENABLED, command.GetAddressResolutionEnable()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_SET_ADDRESS_RESOLUTION_ENABLE, ErrorCode::SUCCESS)); + } + sync_handler(); // |LeAddressManager::check_cached_commands| + + ASSERT_TRUE(hci_layer_->IsPacketQueueEmpty()); + ASSERT_TRUE(le_impl_->address_manager_registered); + + le_impl_->ready_to_unregister = true; + + le_impl_->check_for_unregister(); + sync_handler(); + ASSERT_FALSE(le_impl_->address_manager_registered); + ASSERT_FALSE(le_impl_->pause_connection); +} + +TEST_F(LeImplTest, add_device_to_resolving_list__SupportsBlePrivacy) { + controller_->supports_ble_privacy_ = true; + + // Some kind of privacy policy must be set for LeAddressManager to operate properly + set_privacy_policy_for_initiator_address(fixed_address_, LeAddressManager::AddressPolicy::USE_PUBLIC_ADDRESS); + // Let LeAddressManager::resume_registered_clients execute + sync_handler(); + + ASSERT_EQ(0UL, hci_layer_->NumberOfQueuedCommands()); + + // le_impl should not be registered with address manager + ASSERT_FALSE(le_impl_->address_manager_registered); + ASSERT_FALSE(le_impl_->pause_connection); + + ASSERT_EQ(0UL, le_impl_->le_address_manager_->NumberCachedCommands()); + // Acknowledge that the le_impl has quiesced all relevant controller state + le_impl_->add_device_to_resolving_list( + remote_public_address_with_type_, kPeerIdentityResolvingKey, kLocalIdentityResolvingKey); + ASSERT_EQ(4UL, le_impl_->le_address_manager_->NumberCachedCommands()); + + sync_handler(); // Let |LeAddressManager::register_client| execute on handler + ASSERT_TRUE(le_impl_->address_manager_registered); + ASSERT_TRUE(le_impl_->pause_connection); + + le_impl_->le_address_manager_->AckPause(le_impl_); + sync_handler(); // Allow |LeAddressManager::ack_pause| to complete + + ASSERT_FALSE(hci_layer_->IsPacketQueueEmpty()); + { + // Inform controller to disable address resolution + auto command = CreateLeSecurityCommandView<LeSetAddressResolutionEnableView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(command.IsValid()); + ASSERT_EQ(Enable::DISABLED, command.GetAddressResolutionEnable()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_SET_ADDRESS_RESOLUTION_ENABLE, ErrorCode::SUCCESS)); + } + sync_handler(); // |LeAddressManager::check_cached_commands| + + ASSERT_FALSE(hci_layer_->IsPacketQueueEmpty()); + { + auto command = CreateLeSecurityCommandView<LeAddDeviceToResolvingListView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(command.IsValid()); + ASSERT_EQ(PeerAddressType::PUBLIC_DEVICE_OR_IDENTITY_ADDRESS, command.GetPeerIdentityAddressType()); + ASSERT_EQ(remote_public_address_with_type_.GetAddress(), command.GetPeerIdentityAddress()); + ASSERT_EQ(kPeerIdentityResolvingKey, command.GetPeerIrk()); + ASSERT_EQ(kLocalIdentityResolvingKey, command.GetLocalIrk()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_ADD_DEVICE_TO_RESOLVING_LIST, ErrorCode::SUCCESS)); + } + sync_handler(); // |LeAddressManager::check_cached_commands| + + ASSERT_FALSE(hci_layer_->IsPacketQueueEmpty()); + { + auto command = CreateLeSecurityCommandView<LeSetPrivacyModeView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(command.IsValid()); + ASSERT_EQ(PrivacyMode::DEVICE, command.GetPrivacyMode()); + ASSERT_EQ(remote_public_address_with_type_.GetAddress(), command.GetPeerIdentityAddress()); + ASSERT_EQ(PeerAddressType::PUBLIC_DEVICE_OR_IDENTITY_ADDRESS, command.GetPeerIdentityAddressType()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_SET_PRIVACY_MODE, ErrorCode::SUCCESS)); + } + sync_handler(); // |LeAddressManager::check_cached_commands| + + ASSERT_FALSE(hci_layer_->IsPacketQueueEmpty()); + { + auto command = CreateLeSecurityCommandView<LeSetAddressResolutionEnableView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(command.IsValid()); + ASSERT_EQ(Enable::ENABLED, command.GetAddressResolutionEnable()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_SET_ADDRESS_RESOLUTION_ENABLE, ErrorCode::SUCCESS)); + } + sync_handler(); // |LeAddressManager::check_cached_commands| + + ASSERT_TRUE(hci_layer_->IsPacketQueueEmpty()); + ASSERT_TRUE(le_impl_->address_manager_registered); + + le_impl_->ready_to_unregister = true; + + le_impl_->check_for_unregister(); + sync_handler(); + ASSERT_FALSE(le_impl_->address_manager_registered); + ASSERT_FALSE(le_impl_->pause_connection); +} + +TEST_F(LeImplTest, connectability_state_machine_text) { + ASSERT_STREQ( + "ConnectabilityState::DISARMED", connectability_state_machine_text(ConnectabilityState::DISARMED).c_str()); + ASSERT_STREQ("ConnectabilityState::ARMING", connectability_state_machine_text(ConnectabilityState::ARMING).c_str()); + ASSERT_STREQ("ConnectabilityState::ARMED", connectability_state_machine_text(ConnectabilityState::ARMED).c_str()); + ASSERT_STREQ( + "ConnectabilityState::DISARMING", connectability_state_machine_text(ConnectabilityState::DISARMING).c_str()); +} + +TEST_F(LeImplTest, on_le_event__CONNECTION_COMPLETE_CENTRAL) { + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(_, _)).Times(1); + set_random_device_address_policy(); + auto command = LeConnectionCompleteBuilder::Create( + ErrorCode::SUCCESS, + kHciHandle, + Role::CENTRAL, + AddressType::PUBLIC_DEVICE_ADDRESS, + remote_address_, + 0x0024, + 0x0000, + 0x0011, + ClockAccuracy::PPM_30); + auto bytes = Serialize<LeConnectionCompleteBuilder>(std::move(command)); + auto view = CreateLeEventView<hci::LeConnectionCompleteView>(bytes); + ASSERT_TRUE(view.IsValid()); + le_impl_->on_le_event(view); +} + +TEST_F(LeImplTest, on_le_event__CONNECTION_COMPLETE_PERIPHERAL) { + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(_, _)).Times(1); + set_random_device_address_policy(); + auto command = LeConnectionCompleteBuilder::Create( + ErrorCode::SUCCESS, + kHciHandle, + Role::PERIPHERAL, + AddressType::PUBLIC_DEVICE_ADDRESS, + remote_address_, + 0x0024, + 0x0000, + 0x0011, + ClockAccuracy::PPM_30); + auto bytes = Serialize<LeConnectionCompleteBuilder>(std::move(command)); + auto view = CreateLeEventView<hci::LeConnectionCompleteView>(bytes); + ASSERT_TRUE(view.IsValid()); + le_impl_->on_le_event(view); +} + +TEST_F(LeImplTest, on_le_event__ENHANCED_CONNECTION_COMPLETE_CENTRAL) { + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(_, _)).Times(1); + set_random_device_address_policy(); + auto command = LeEnhancedConnectionCompleteBuilder::Create( + ErrorCode::SUCCESS, + kHciHandle, + Role::CENTRAL, + AddressType::PUBLIC_DEVICE_ADDRESS, + remote_address_, + local_rpa_, + remote_rpa_, + 0x0024, + 0x0000, + 0x0011, + ClockAccuracy::PPM_30); + auto bytes = Serialize<LeEnhancedConnectionCompleteBuilder>(std::move(command)); + auto view = CreateLeEventView<hci::LeEnhancedConnectionCompleteView>(bytes); + ASSERT_TRUE(view.IsValid()); + le_impl_->on_le_event(view); +} + +TEST_F(LeImplTest, on_le_event__ENHANCED_CONNECTION_COMPLETE_PERIPHERAL) { + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectSuccess(_, _)).Times(1); + auto command = LeEnhancedConnectionCompleteBuilder::Create( + ErrorCode::SUCCESS, + kHciHandle, + Role::PERIPHERAL, + AddressType::PUBLIC_DEVICE_ADDRESS, + remote_address_, + local_rpa_, + remote_rpa_, + 0x0024, + 0x0000, + 0x0011, + ClockAccuracy::PPM_30); + auto bytes = Serialize<LeEnhancedConnectionCompleteBuilder>(std::move(command)); + auto view = CreateLeEventView<hci::LeEnhancedConnectionCompleteView>(bytes); + ASSERT_TRUE(view.IsValid()); + le_impl_->on_le_event(view); +} + +TEST_F(LeImplWithConnectionTest, on_le_event__PHY_UPDATE_COMPLETE) { + hci::ErrorCode hci_status{ErrorCode::STATUS_UNKNOWN}; + hci::PhyType tx_phy{0}; + hci::PhyType rx_phy{0}; + + // Send a phy update + { + EXPECT_CALL(connection_management_callbacks_, OnPhyUpdate(_, _, _)) + .WillOnce([&](hci::ErrorCode _hci_status, uint8_t _tx_phy, uint8_t _rx_phy) { + hci_status = _hci_status; + tx_phy = static_cast<PhyType>(_tx_phy); + rx_phy = static_cast<PhyType>(_rx_phy); + }); + auto command = LePhyUpdateCompleteBuilder::Create(ErrorCode::SUCCESS, kHciHandle, 0x01, 0x02); + auto bytes = Serialize<LePhyUpdateCompleteBuilder>(std::move(command)); + auto view = CreateLeEventView<hci::LePhyUpdateCompleteView>(bytes); + ASSERT_TRUE(view.IsValid()); + le_impl_->on_le_event(view); + } + + sync_handler(); + ASSERT_EQ(ErrorCode::SUCCESS, hci_status); + ASSERT_EQ(PhyType::LE_1M, tx_phy); + ASSERT_EQ(PhyType::LE_2M, rx_phy); +} + +TEST_F(LeImplWithConnectionTest, on_le_event__DATA_LENGTH_CHANGE) { + uint16_t tx_octets{0}; + uint16_t tx_time{0}; + uint16_t rx_octets{0}; + uint16_t rx_time{0}; + + // Send a data length event + { + EXPECT_CALL(connection_management_callbacks_, OnDataLengthChange(_, _, _, _)) + .WillOnce([&](uint16_t _tx_octets, uint16_t _tx_time, uint16_t _rx_octets, uint16_t _rx_time) { + tx_octets = _tx_octets; + tx_time = _tx_time; + rx_octets = _rx_octets; + rx_time = _rx_time; + }); + auto command = LeDataLengthChangeBuilder::Create(kHciHandle, 0x1234, 0x5678, 0x9abc, 0xdef0); + auto bytes = Serialize<LeDataLengthChangeBuilder>(std::move(command)); + auto view = CreateLeEventView<hci::LeDataLengthChangeView>(bytes); + ASSERT_TRUE(view.IsValid()); + le_impl_->on_le_event(view); + } + + sync_handler(); + ASSERT_EQ(0x1234, tx_octets); + ASSERT_EQ(0x5678, tx_time); + ASSERT_EQ(0x9abc, rx_octets); + ASSERT_EQ(0xdef0, rx_time); +} + +TEST_F(LeImplWithConnectionTest, on_le_event__REMOTE_CONNECTION_PARAMETER_REQUEST) { + // Send a remote connection parameter request + auto command = hci::LeRemoteConnectionParameterRequestBuilder::Create( + kHciHandle, kIntervalMin, kIntervalMax, kLatency, kTimeout); + auto bytes = Serialize<LeRemoteConnectionParameterRequestBuilder>(std::move(command)); + { + auto view = CreateLeEventView<hci::LeRemoteConnectionParameterRequestView>(bytes); + ASSERT_TRUE(view.IsValid()); + le_impl_->on_le_event(view); + } + + sync_handler(); + + ASSERT_FALSE(hci_layer_->IsPacketQueueEmpty()); + + auto view = CreateLeConnectionManagementCommandView<LeRemoteConnectionParameterRequestReplyView>( + hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(view.IsValid()); + + ASSERT_EQ(kIntervalMin, view.GetIntervalMin()); + ASSERT_EQ(kIntervalMax, view.GetIntervalMax()); + ASSERT_EQ(kLatency, view.GetLatency()); + ASSERT_EQ(kTimeout, view.GetTimeout()); +} + +TEST_F(LeImplRegisteredWithAddressManagerTest, clear_resolving_list) { + le_impl_->clear_resolving_list(); + ASSERT_EQ(3UL, le_impl_->le_address_manager_->NumberCachedCommands()); + + sync_handler(); // Allow |LeAddressManager::pause_registered_clients| to complete + sync_handler(); // Allow |LeAddressManager::handle_next_command| to complete + + ASSERT_EQ(1UL, hci_layer_->NumberOfQueuedCommands()); + { + auto view = CreateLeSecurityCommandView<LeSetAddressResolutionEnableView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(view.IsValid()); + ASSERT_EQ(Enable::DISABLED, view.GetAddressResolutionEnable()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_SET_ADDRESS_RESOLUTION_ENABLE, ErrorCode::SUCCESS)); + } + + sync_handler(); // Allow |LeAddressManager::check_cached_commands| to complete + ASSERT_EQ(1UL, hci_layer_->NumberOfQueuedCommands()); + { + auto view = CreateLeSecurityCommandView<LeClearResolvingListView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(view.IsValid()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_CLEAR_RESOLVING_LIST, ErrorCode::SUCCESS)); + } + + sync_handler(); // Allow |LeAddressManager::handle_next_command| to complete + ASSERT_EQ(1UL, hci_layer_->NumberOfQueuedCommands()); + { + auto view = CreateLeSecurityCommandView<LeSetAddressResolutionEnableView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(view.IsValid()); + ASSERT_EQ(Enable::ENABLED, view.GetAddressResolutionEnable()); + le_impl_->le_address_manager_->OnCommandComplete( + ReturnCommandComplete(OpCode::LE_SET_ADDRESS_RESOLUTION_ENABLE, ErrorCode::SUCCESS)); + } + ASSERT_TRUE(hci_layer_->IsPacketQueueEmpty()); +} + +TEST_F(LeImplWithConnectionTest, HACK_get_handle) { + sync_handler(); + + ASSERT_EQ(kHciHandle, le_impl_->HACK_get_handle(remote_address_)); +} + +TEST_F(LeImplTest, on_le_connection_canceled_on_pause) { + set_random_device_address_policy(); + le_impl_->pause_connection = true; + le_impl_->on_le_connection_canceled_on_pause(); + ASSERT_TRUE(le_impl_->arm_on_resume_); + ASSERT_EQ(ConnectabilityState::DISARMED, le_impl_->connectability_state_); +} + +TEST_F(LeImplTest, on_create_connection_timeout) { + EXPECT_CALL(mock_le_connection_callbacks_, OnLeConnectFail(_, ErrorCode::CONNECTION_ACCEPT_TIMEOUT)).Times(1); + le_impl_->create_connection_timeout_alarms_.emplace( + std::piecewise_construct, + std::forward_as_tuple( + remote_public_address_with_type_.GetAddress(), remote_public_address_with_type_.GetAddressType()), + std::forward_as_tuple(handler_)); + le_impl_->on_create_connection_timeout(remote_public_address_with_type_); + sync_handler(); + ASSERT_TRUE(le_impl_->create_connection_timeout_alarms_.empty()); +} + +TEST_F(LeImplTest, on_common_le_connection_complete__NoPriorConnection) { + auto log_capture = std::make_unique<LogCapture>(); + le_impl_->on_common_le_connection_complete(remote_public_address_with_type_); + ASSERT_TRUE(le_impl_->connecting_le_.empty()); + ASSERT_TRUE(log_capture->Rewind()->Find("No prior connection request for")); +} + +TEST_F(LeImplTest, cancel_connect) { + le_impl_->create_connection_timeout_alarms_.emplace( + std::piecewise_construct, + std::forward_as_tuple( + remote_public_address_with_type_.GetAddress(), remote_public_address_with_type_.GetAddressType()), + std::forward_as_tuple(handler_)); + le_impl_->cancel_connect(remote_public_address_with_type_); + sync_handler(); + ASSERT_TRUE(le_impl_->create_connection_timeout_alarms_.empty()); +} + +TEST_F(LeImplTest, set_le_suggested_default_data_parameters) { + le_impl_->set_le_suggested_default_data_parameters(kLength, kTime); + sync_handler(); + auto view = + CreateLeConnectionManagementCommandView<LeWriteSuggestedDefaultDataLengthView>(hci_layer_->DequeueCommandBytes()); + ASSERT_TRUE(view.IsValid()); + ASSERT_EQ(kLength, view.GetTxOctets()); + ASSERT_EQ(kTime, view.GetTxTime()); +} + } // namespace acl_manager } // namespace hci } // namespace bluetooth diff --git a/system/gd/hci/hci_layer.cc b/system/gd/hci/hci_layer.cc index 48f05c4146..595c7b6564 100644 --- a/system/gd/hci/hci_layer.cc +++ b/system/gd/hci/hci_layer.cc @@ -49,6 +49,7 @@ using std::unique_ptr; static void fail_if_reset_complete_not_success(CommandCompleteView complete) { auto reset_complete = ResetCompleteView::Create(complete); ASSERT(reset_complete.IsValid()); + LOG_DEBUG("Reset completed with status: %s", ErrorCodeText(ErrorCode::SUCCESS).c_str()); ASSERT(reset_complete.GetStatus() == ErrorCode::SUCCESS); } diff --git a/system/gd/hci/hci_layer_unittest.cc b/system/gd/hci/hci_layer_unittest.cc index 3d88f06b41..d8ac9de2b1 100644 --- a/system/gd/hci/hci_layer_unittest.cc +++ b/system/gd/hci/hci_layer_unittest.cc @@ -37,6 +37,25 @@ using namespace std::chrono_literals; +namespace { +constexpr size_t kBufSize = 512; +constexpr char kOurAclEventHandlerWasInvoked[] = "Our ACL event handler was invoked."; +constexpr char kOurCommandCompleteHandlerWasInvoked[] = "Our command complete handler was invoked."; +constexpr char kOurCommandStatusHandlerWasInvoked[] = "Our command status handler was invoked."; +constexpr char kOurDisconnectHandlerWasInvoked[] = "Our disconnect handler was invoked."; +constexpr char kOurEventHandlerWasInvoked[] = "Our event handler was invoked."; +constexpr char kOurLeAclEventHandlerWasInvoked[] = "Our LE ACL event handler was invoked."; +constexpr char kOurLeAdvertisementEventHandlerWasInvoked[] = "Our LE advertisement event handler was invoked."; +constexpr char kOurLeDisconnectHandlerWasInvoked[] = "Our LE disconnect handler was invoked."; +constexpr char kOurLeEventHandlerWasInvoked[] = "Our LE event handler was invoked."; +constexpr char kOurLeIsoEventHandlerWasInvoked[] = "Our LE ISO event handler was invoked."; +constexpr char kOurLeReadRemoteVersionHandlerWasInvoked[] = "Our Read Remote Version complete handler was invoked."; +constexpr char kOurLeScanningEventHandlerWasInvoked[] = "Our LE scanning event handler was invoked."; +constexpr char kOurReadRemoteVersionHandlerWasInvoked[] = "Our Read Remote Version complete handler was invoked."; +constexpr char kOurLeSecurityEventHandlerWasInvoked[] = "Our LE security event handler was invoked."; +constexpr char kOurSecurityEventHandlerWasInvoked[] = "Our security event handler was invoked."; +} // namespace + namespace bluetooth { namespace hci { @@ -68,7 +87,7 @@ class TestHciHal : public hal::HciHal { TestHciHal() : hal::HciHal() {} ~TestHciHal() { - ASSERT_LOG(callbacks == nullptr, "unregisterIncomingPacketCallback() must be called"); + ASSERT(callbacks == nullptr); } void registerIncomingPacketCallback(hal::HciHalCallbacks* callback) override { @@ -81,7 +100,8 @@ class TestHciHal : public hal::HciHal { void sendHciCommand(hal::HciPacket command) override { outgoing_commands_.push_back(std::move(command)); - LOG_DEBUG("Enqueued HCI command in HAL."); + sent_commands_++; + LOG_DEBUG("Enqueued HCI command %d in HAL.", sent_commands_); } void sendScoData(hal::HciPacket data) override {} @@ -111,19 +131,24 @@ class TestHciHal : public hal::HciHal { return outgoing_commands_.size(); } - void InjectEvent(std::unique_ptr<packet::BasePacketBuilder> packet) { - callbacks->hciEventReceived(GetPacketBytes(std::move(packet))); - } - std::string ToString() const override { return std::string("TestHciHal"); } + void InjectResetCompleteEventWithCode(ErrorCode code) { + auto reset_complete = ResetCompleteBuilder::Create(0x01, code); + InjectEvent(std::move(reset_complete)); + } + + void InjectEvent(std::unique_ptr<packet::BasePacketBuilder> packet) { + callbacks->hciEventReceived(GetPacketBytes(std::move(packet))); + } static const ModuleFactory Factory; private: std::list<hal::HciPacket> outgoing_commands_; std::unique_ptr<std::promise<void>> sent_command_promise_; + int sent_commands_{0}; }; const ModuleFactory TestHciHal::Factory = ModuleFactory([]() { return new TestHciHal(); }); @@ -152,8 +177,12 @@ class HciLayerTest : public ::testing::Test { } void FailIfResetNotSent() { + hci_handler_->BindOnceOn(this, &HciLayerTest::fail_if_reset_not_sent).Invoke(); + } + + void fail_if_reset_not_sent() { std::promise<void> promise; - log_capture_->WaitUntilLogContains(&promise, "Enqueued HCI command in HAL."); + log_capture_->WaitUntilLogContains(&promise, "Enqueued HCI command 1 in HAL."); auto sent_command = hal_->GetSentCommand(); auto reset_view = ResetView::Create(CommandView::Create(sent_command)); ASSERT_TRUE(reset_view.IsValid()); @@ -177,7 +206,7 @@ TEST_F(HciLayerTest, controller_debug_info_requested_on_hci_timeout) { FakeTimerAdvance(HciLayer::kHciTimeoutMs.count()); std::promise<void> promise; - log_capture_->WaitUntilLogContains(&promise, "Enqueued HCI command in HAL."); + log_capture_->WaitUntilLogContains(&promise, "Enqueued HCI command 2 in HAL."); auto sent_command = hal_->GetSentCommand(); auto debug_info_view = ControllerDebugInfoView::Create(VendorCommandView::Create(sent_command)); ASSERT_TRUE(debug_info_view.IsValid()); @@ -188,7 +217,7 @@ TEST_F(HciLayerTest, abort_after_hci_restart_timeout) { FakeTimerAdvance(HciLayer::kHciTimeoutMs.count()); std::promise<void> promise; - log_capture_->WaitUntilLogContains(&promise, "Enqueued HCI command in HAL."); + log_capture_->WaitUntilLogContains(&promise, "Enqueued HCI command 2 in HAL."); auto sent_command = hal_->GetSentCommand(); auto debug_info_view = ControllerDebugInfoView::Create(VendorCommandView::Create(sent_command)); ASSERT_TRUE(debug_info_view.IsValid()); @@ -219,5 +248,315 @@ TEST_F(HciLayerTest, abort_on_root_inflammation_event) { ""); } +TEST_F(HciLayerTest, successful_reset) { + FailIfResetNotSent(); + auto error_code = ErrorCode::SUCCESS; + hal_->InjectResetCompleteEventWithCode(error_code); + std::promise<void> promise; + auto buf = std::make_unique<char[]>(kBufSize); + std::snprintf(buf.get(), kBufSize, "Reset completed with status: %s", ErrorCodeText(error_code).c_str()); + log_capture_->WaitUntilLogContains(&promise, buf.get()); +} + +TEST_F(HciLayerTest, abort_if_reset_complete_returns_error) { + ASSERT_DEATH( + { + FailIfResetNotSent(); + auto error_code = ErrorCode::UNSPECIFIED_ERROR; + hal_->InjectResetCompleteEventWithCode(error_code); + auto buf = std::make_unique<char[]>(kBufSize); + std::snprintf(buf.get(), kBufSize, "Reset completed with status: %s", ErrorCodeText(error_code).c_str()); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, buf.get()); + }, + ""); +} + +TEST_F(HciLayerTest, event_handler_is_invoked) { + FailIfResetNotSent(); + hci_->UnregisterEventHandler(EventCode::COMMAND_COMPLETE); + hci_->RegisterEventHandler(EventCode::COMMAND_COMPLETE, hci_handler_->Bind([](EventView view) { + LOG_DEBUG("%s", kOurEventHandlerWasInvoked); + })); + auto error_code = ErrorCode::SUCCESS; + hal_->InjectResetCompleteEventWithCode(error_code); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurEventHandlerWasInvoked); +} + +TEST_F(HciLayerTest, le_event_handler_is_invoked) { + FailIfResetNotSent(); + hci_->RegisterLeEventHandler(SubeventCode::ENHANCED_CONNECTION_COMPLETE, hci_handler_->Bind([](LeMetaEventView view) { + LOG_DEBUG("%s", kOurLeEventHandlerWasInvoked); + })); + hci::Address remote_address; + Address::FromString("D0:05:04:03:02:01", remote_address); + hal_->InjectEvent(LeEnhancedConnectionCompleteBuilder::Create( + ErrorCode::SUCCESS, + 0x0041, + Role::PERIPHERAL, + AddressType::PUBLIC_DEVICE_ADDRESS, + remote_address, + Address::kEmpty, + Address::kEmpty, + 0x0024, + 0x0000, + 0x0011, + ClockAccuracy::PPM_30)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurLeEventHandlerWasInvoked); +} + +TEST_F(HciLayerTest, abort_on_second_register_event_handler) { + ASSERT_DEATH( + { + FailIfResetNotSent(); + hci_->RegisterEventHandler(EventCode::COMMAND_COMPLETE, hci_handler_->Bind([](EventView view) {})); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, "Can not register a second handler for"); + }, + ""); +} + +TEST_F(HciLayerTest, abort_on_second_register_le_event_handler) { + ASSERT_DEATH( + { + FailIfResetNotSent(); + hci_->RegisterLeEventHandler( + SubeventCode::ENHANCED_CONNECTION_COMPLETE, hci_handler_->Bind([](LeMetaEventView view) {})); + hci_->RegisterLeEventHandler( + SubeventCode::ENHANCED_CONNECTION_COMPLETE, hci_handler_->Bind([](LeMetaEventView view) {})); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, "Can not register a second handler for"); + }, + ""); +} + +TEST_F(HciLayerTest, our_acl_event_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetAclConnectionInterface( + hci_handler_->Bind([](EventView view) { LOG_DEBUG("%s", kOurAclEventHandlerWasInvoked); }), + hci_handler_->Bind([](uint16_t handle, ErrorCode reason) {}), + hci_handler_->Bind([](hci::ErrorCode hci_status, + uint16_t handle, + uint8_t version, + uint16_t manufacturer_name, + uint16_t sub_version) {})); + hal_->InjectEvent(ReadClockOffsetCompleteBuilder::Create(ErrorCode::SUCCESS, 0x0001, 0x0123)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurAclEventHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_disconnect_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetAclConnectionInterface( + hci_handler_->Bind([](EventView view) {}), + hci_handler_->Bind([](uint16_t handle, ErrorCode reason) { LOG_DEBUG("%s", kOurDisconnectHandlerWasInvoked); }), + hci_handler_->Bind([](hci::ErrorCode hci_status, + uint16_t handle, + uint8_t version, + uint16_t manufacturer_name, + uint16_t sub_version) {})); + hal_->InjectEvent( + DisconnectionCompleteBuilder::Create(ErrorCode::SUCCESS, 0x0001, ErrorCode::REMOTE_USER_TERMINATED_CONNECTION)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurDisconnectHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_read_remote_version_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetAclConnectionInterface( + hci_handler_->Bind([](EventView view) {}), + hci_handler_->Bind([](uint16_t handle, ErrorCode reason) {}), + hci_handler_->Bind([](hci::ErrorCode hci_status, + uint16_t handle, + uint8_t version, + uint16_t manufacturer_name, + uint16_t sub_version) { LOG_DEBUG("%s", kOurReadRemoteVersionHandlerWasInvoked); })); + hal_->InjectEvent( + ReadRemoteVersionInformationCompleteBuilder::Create(ErrorCode::SUCCESS, 0x0001, 0x0b, 0x000f, 0x0000)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurReadRemoteVersionHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_le_acl_event_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetLeAclConnectionInterface( + hci_handler_->Bind([](LeMetaEventView view) { LOG_DEBUG("%s", kOurLeAclEventHandlerWasInvoked); }), + hci_handler_->Bind([](uint16_t handle, ErrorCode reason) {}), + hci_handler_->Bind([](hci::ErrorCode hci_status, + uint16_t handle, + uint8_t version, + uint16_t manufacturer_name, + uint16_t sub_version) {})); + hal_->InjectEvent(LeDataLengthChangeBuilder::Create(0x0001, 0x001B, 0x0148, 0x001B, 0x0148)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurLeAclEventHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_le_disconnect_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetLeAclConnectionInterface( + hci_handler_->Bind([](LeMetaEventView view) {}), + hci_handler_->Bind([](uint16_t handle, ErrorCode reason) { LOG_DEBUG("%s", kOurLeDisconnectHandlerWasInvoked); }), + hci_handler_->Bind([](hci::ErrorCode hci_status, + uint16_t handle, + uint8_t version, + uint16_t manufacturer_name, + uint16_t sub_version) {})); + hal_->InjectEvent( + DisconnectionCompleteBuilder::Create(ErrorCode::SUCCESS, 0x0001, ErrorCode::REMOTE_USER_TERMINATED_CONNECTION)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurLeDisconnectHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_le_read_remote_version_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetLeAclConnectionInterface( + hci_handler_->Bind([](LeMetaEventView view) {}), + hci_handler_->Bind([](uint16_t handle, ErrorCode reason) {}), + hci_handler_->Bind([](hci::ErrorCode hci_status, + uint16_t handle, + uint8_t version, + uint16_t manufacturer_name, + uint16_t sub_version) { LOG_DEBUG("%s", kOurLeReadRemoteVersionHandlerWasInvoked); })); + hal_->InjectEvent( + ReadRemoteVersionInformationCompleteBuilder::Create(ErrorCode::SUCCESS, 0x0001, 0x0b, 0x000f, 0x0000)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurLeReadRemoteVersionHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_security_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetSecurityInterface( + hci_handler_->Bind([](EventView view) { LOG_DEBUG("%s", kOurSecurityEventHandlerWasInvoked); })); + hal_->InjectEvent(EncryptionChangeBuilder::Create(ErrorCode::SUCCESS, 0x0001, bluetooth::hci::EncryptionEnabled::ON)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurSecurityEventHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_le_security_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetLeSecurityInterface( + hci_handler_->Bind([](LeMetaEventView view) { LOG_DEBUG("%s", kOurLeSecurityEventHandlerWasInvoked); })); + hal_->InjectEvent(LeLongTermKeyRequestBuilder::Create(0x0001, {0, 0, 0, 0, 0, 0, 0, 0}, 0)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurLeSecurityEventHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_le_advertising_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetLeAdvertisingInterface( + hci_handler_->Bind([](LeMetaEventView view) { LOG_DEBUG("%s", kOurLeAdvertisementEventHandlerWasInvoked); })); + hal_->InjectEvent(LeAdvertisingSetTerminatedBuilder::Create(ErrorCode::SUCCESS, 0x01, 0x001, 0x01)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurLeAdvertisementEventHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_le_scanning_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetLeScanningInterface( + hci_handler_->Bind([](LeMetaEventView view) { LOG_DEBUG("%s", kOurLeScanningEventHandlerWasInvoked); })); + hal_->InjectEvent(LeScanTimeoutBuilder::Create()); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurLeScanningEventHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_le_iso_callback_is_invoked) { + FailIfResetNotSent(); + hci_->GetLeIsoInterface( + hci_handler_->Bind([](LeMetaEventView view) { LOG_DEBUG("%s", kOurLeIsoEventHandlerWasInvoked); })); + hal_->InjectEvent(LeCisRequestBuilder::Create(0x0001, 0x0001, 0x01, 0x01)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurLeIsoEventHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_command_complete_callback_is_invoked) { + FailIfResetNotSent(); + auto error_code = ErrorCode::SUCCESS; + hal_->InjectResetCompleteEventWithCode(error_code); + hci_->EnqueueCommand(ResetBuilder::Create(), hci_handler_->BindOnce([](CommandCompleteView view) { + LOG_DEBUG("%s", kOurCommandCompleteHandlerWasInvoked); + })); + hal_->InjectResetCompleteEventWithCode(error_code); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurCommandCompleteHandlerWasInvoked); +} + +TEST_F(HciLayerTest, our_command_status_callback_is_invoked) { + FailIfResetNotSent(); + auto error_code = ErrorCode::SUCCESS; + hal_->InjectResetCompleteEventWithCode(error_code); + hci_->EnqueueCommand(ReadClockOffsetBuilder::Create(0x001), hci_handler_->BindOnce([](CommandStatusView view) { + LOG_DEBUG("%s", kOurCommandStatusHandlerWasInvoked); + })); + hal_->InjectEvent(ReadClockOffsetStatusBuilder::Create(ErrorCode::SUCCESS, 1)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, kOurCommandStatusHandlerWasInvoked); +} + +TEST_F(HciLayerTest, command_complete_callback_is_invoked_with_an_opcode_that_does_not_match_command_queue) { + ASSERT_DEATH( + { + FailIfResetNotSent(); + hci_->EnqueueCommand( + ReadClockOffsetBuilder::Create(0x001), hci_handler_->BindOnce([](CommandCompleteView view) {})); + hal_->InjectEvent(ReadClockOffsetStatusBuilder::Create(ErrorCode::SUCCESS, 1)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, "Waiting for 0x0c03 (RESET)"); + }, + ""); +} + +TEST_F(HciLayerTest, command_status_callback_is_invoked_with_an_opcode_that_does_not_match_command_queue) { + ASSERT_DEATH( + { + FailIfResetNotSent(); + hci_->EnqueueCommand( + ReadClockOffsetBuilder::Create(0x001), hci_handler_->BindOnce([](CommandStatusView view) {})); + hal_->InjectEvent(ReadClockOffsetStatusBuilder::Create(ErrorCode::SUCCESS, 1)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, "Waiting for 0x0c03 (RESET)"); + }, + ""); +} + +TEST_F(HciLayerTest, command_complete_callback_is_invoked_but_command_queue_empty) { + ASSERT_DEATH( + { + FailIfResetNotSent(); + auto error_code = ErrorCode::SUCCESS; + hal_->InjectResetCompleteEventWithCode(error_code); + hal_->InjectResetCompleteEventWithCode(error_code); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, "Unexpected event complete with opcode:0x0c3"); + }, + ""); +} + +TEST_F(HciLayerTest, command_status_callback_is_invoked_but_command_queue_empty) { + ASSERT_DEATH( + { + FailIfResetNotSent(); + auto error_code = ErrorCode::SUCCESS; + hal_->InjectResetCompleteEventWithCode(error_code); + hal_->InjectEvent(ReadClockOffsetStatusBuilder::Create(ErrorCode::SUCCESS, 1)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains(&promise, "Unexpected event status with opcode:0x41f"); + }, + ""); +} + +TEST_F(HciLayerTest, command_status_callback_is_invoked_with_failure_status) { + FailIfResetNotSent(); + auto error_code = ErrorCode::SUCCESS; + hal_->InjectResetCompleteEventWithCode(error_code); + hci_->EnqueueCommand(ReadClockOffsetBuilder::Create(0x001), hci_handler_->BindOnce([](CommandStatusView view) {})); + hal_->InjectEvent(ReadClockOffsetStatusBuilder::Create(ErrorCode::HARDWARE_FAILURE, 1)); + std::promise<void> promise; + log_capture_->WaitUntilLogContains( + &promise, "Received UNEXPECTED command status:HARDWARE_FAILURE opcode:0x41f (READ_CLOCK_OFFSET)"); +} + } // namespace hci } // namespace bluetooth diff --git a/system/gd/hci/le_address_manager.cc b/system/gd/hci/le_address_manager.cc index ece39f53de..703851fc3f 100644 --- a/system/gd/hci/le_address_manager.cc +++ b/system/gd/hci/le_address_manager.cc @@ -51,6 +51,18 @@ void LeAddressManager::SetPrivacyPolicyForInitiatorAddress( bool supports_ble_privacy, std::chrono::milliseconds minimum_rotation_time, std::chrono::milliseconds maximum_rotation_time) { + // Handle repeated calls to the function + if (address_policy_ != AddressPolicy::POLICY_NOT_SET) { + // Need to update some parameteres like IRK if privacy is supported + if (supports_ble_privacy) { + LOG_INFO("Updating rotation parameters."); + rotation_irk_ = rotation_irk; + minimum_rotation_time_ = minimum_rotation_time; + maximum_rotation_time_ = maximum_rotation_time; + set_random_address(); + } + return; + } ASSERT(address_policy_ == AddressPolicy::POLICY_NOT_SET); ASSERT(address_policy != AddressPolicy::POLICY_NOT_SET); ASSERT_LOG(registered_clients_.empty(), "Policy must be set before clients are registered."); diff --git a/system/gd/metrics/chromeos/metrics.cc b/system/gd/metrics/chromeos/metrics.cc index 5e313f69d8..735cbade95 100644 --- a/system/gd/metrics/chromeos/metrics.cc +++ b/system/gd/metrics/chromeos/metrics.cc @@ -54,6 +54,7 @@ void LogMetricsAdapterStateChanged(uint32_t state) { } void LogMetricsBondCreateAttempt(RawAddress* addr, uint32_t device_type) { + ConnectionType connection_type; int64_t boot_time; std::string addr_string; std::string boot_id; @@ -62,26 +63,28 @@ void LogMetricsBondCreateAttempt(RawAddress* addr, uint32_t device_type) { addr_string = addr->ToString(); boot_time = bluetooth::common::time_get_os_boottime_us(); + connection_type = ToPairingDeviceType(addr_string, device_type); LOG_DEBUG( "PairingStateChanged: %s, %d, %s, %d, %d", boot_id.c_str(), boot_time, addr_string.c_str(), - device_type, + connection_type, PairingState::PAIR_STARTING); ::metrics::structured::events::bluetooth::BluetoothPairingStateChanged() .SetBootId(boot_id) .SetSystemTime(boot_time) .SetDeviceId(addr_string) - .SetDeviceType(device_type) + .SetDeviceType((int64_t)connection_type) .SetPairingState((int64_t)PairingState::PAIR_STARTING) .Record(); } void LogMetricsBondStateChanged( RawAddress* addr, uint32_t device_type, uint32_t status, uint32_t bond_state, int32_t fail_reason) { + ConnectionType connection_type; int64_t boot_time; PairingState pairing_state; std::string addr_string; @@ -91,6 +94,7 @@ void LogMetricsBondStateChanged( addr_string = addr->ToString(); boot_time = bluetooth::common::time_get_os_boottime_us(); + connection_type = ToPairingDeviceType(addr_string, device_type); pairing_state = ToPairingState(status, bond_state, fail_reason); // Ignore the start of pairing event as its logged separated above. @@ -101,14 +105,14 @@ void LogMetricsBondStateChanged( boot_id.c_str(), boot_time, addr_string.c_str(), - device_type, + connection_type, pairing_state); ::metrics::structured::events::bluetooth::BluetoothPairingStateChanged() .SetBootId(boot_id) .SetSystemTime(boot_time) .SetDeviceId(addr_string) - .SetDeviceType(device_type) + .SetDeviceType((int64_t)connection_type) .SetPairingState((int64_t)pairing_state) .Record(); } diff --git a/system/gd/metrics/chromeos/metrics_event.cc b/system/gd/metrics/chromeos/metrics_event.cc index 2a4a5ef2a9..e8b0462dfe 100644 --- a/system/gd/metrics/chromeos/metrics_event.cc +++ b/system/gd/metrics/chromeos/metrics_event.cc @@ -20,6 +20,7 @@ #include "hci/hci_packets.h" #include "include/hardware/bluetooth.h" +#include "include/hardware/bt_av.h" #include "include/hardware/bt_hh.h" namespace bluetooth { @@ -29,9 +30,19 @@ namespace metrics { typedef bt_bond_state_t BtBondState; // topshim::btif::BtStatus is a copy of hardware/bluetooth.h:bt_status_t typedef bt_status_t BtStatus; -// topshim::profile::hid_host::BthhConnectionState is a copy of hardware/bluetooth.h:bthh_connection_state_t +// topshim::profile::a2dp::BtavConnectionState is a copy of hardware/bt_av.h:btav_connection_state_t +typedef btav_connection_state_t BtavConnectionState; +// topshim::profile::hid_host::BthhConnectionState is a copy of hardware/bt_hh.h:bthh_connection_state_t typedef bthh_connection_state_t BthhConnectionState; +// A copy of topshim::btif::BtDeviceType +enum class BtDeviceType { + Unknown = 0, + Bredr, + Ble, + Dual, +}; + // A normalized connection state ENUM definition all profiles enum class ProfilesConnectionState { DISCONNECTED = 0, @@ -173,6 +184,8 @@ static PairingState FailReasonToPairingState(int32_t fail_reason) { return PairingState::PAIR_FAIL_AUTH_FAILED; case hci::ErrorCode::ROLE_SWITCH_FAILED: return PairingState::PAIR_FAIL_FAILED; + case hci::ErrorCode::HOST_BUSY: + return PairingState::PAIR_FAIL_BUSY; case hci::ErrorCode::CONTROLLER_BUSY: return PairingState::PAIR_FAIL_BUSY; case hci::ErrorCode::CONNECTION_FAILED_ESTABLISHMENT: @@ -188,6 +201,8 @@ static PairingState FailReasonToPairingState(int32_t fail_reason) { case hci::ErrorCode::UNKNOWN_ADVERTISING_IDENTIFIER: case hci::ErrorCode::STATUS_UNKNOWN: return PairingState::PAIR_FAIL_UNKNOWN; + default: + return PairingState::PAIR_FAIL_UNKNOWN; } } @@ -195,6 +210,28 @@ AdapterState ToAdapterState(uint32_t state) { return state == 1 ? AdapterState::ON : AdapterState::OFF; } +ConnectionType ToPairingDeviceType(std::string addr, uint32_t device_type) { + // A map stores the pending ConnectionType used to match a pairing event with unknown type. + // map<address, type> + static std::map<std::string, ConnectionType> pending_type; + + switch ((BtDeviceType)device_type) { + case BtDeviceType::Ble: + pending_type[addr] = ConnectionType::CONN_TYPE_LE; + return ConnectionType::CONN_TYPE_LE; + case BtDeviceType::Bredr: + pending_type[addr] = ConnectionType::CONN_TYPE_BREDR; + return ConnectionType::CONN_TYPE_BREDR; + case BtDeviceType::Dual: + case BtDeviceType::Unknown: + if (pending_type.find(addr) != pending_type.end()) { + return pending_type[addr]; + } else { + return ConnectionType::CONN_TYPE_UNKNOWN; + } + } +} + PairingState ToPairingState(uint32_t status, uint32_t bond_state, int32_t fail_reason) { PairingState pairing_state = PairingState::PAIR_FAIL_UNKNOWN; @@ -294,7 +331,26 @@ static std::pair<uint32_t, uint32_t> ToProfileConnectionState(uint32_t profile, std::pair<uint32_t, uint32_t> output; switch ((ProfilesFloss)profile) { - // case ProfilesFloss::A2dpSink: + case ProfilesFloss::A2dpSink: + output.first = (uint32_t)Profile::A2DP; + switch ((BtavConnectionState)state) { + case BtavConnectionState::BTAV_CONNECTION_STATE_CONNECTED: + output.second = (uint32_t)ProfilesConnectionState::CONNECTED; + break; + case BtavConnectionState::BTAV_CONNECTION_STATE_CONNECTING: + output.second = (uint32_t)ProfilesConnectionState::CONNECTING; + break; + case BtavConnectionState::BTAV_CONNECTION_STATE_DISCONNECTED: + output.second = (uint32_t)ProfilesConnectionState::DISCONNECTED; + break; + case BtavConnectionState::BTAV_CONNECTION_STATE_DISCONNECTING: + output.second = (uint32_t)ProfilesConnectionState::DISCONNECTING; + break; + default: + output.second = (uint32_t)ProfilesConnectionState::UNKNOWN; + break; + } + break; // case ProfilesFloss::A2dpSource: // case ProfilesFloss::AdvAudioDist: // case ProfilesFloss::Hsp: diff --git a/system/gd/metrics/chromeos/metrics_event.h b/system/gd/metrics/chromeos/metrics_event.h index a88c270197..769ecee238 100644 --- a/system/gd/metrics/chromeos/metrics_event.h +++ b/system/gd/metrics/chromeos/metrics_event.h @@ -25,6 +25,17 @@ namespace metrics { // BluetoothAdapterStateChanged/AdapterState. enum class AdapterState : int64_t { OFF = 0, ON = 1 }; +// ENUM definition for device/connection type that in sync with ChromeOS structured metrics +// BluetoothPairingStateChanged/DeviceType and BlueZ metrics_conn_type. Note this is a non-optimal ENUM design that +// mixed the connection transport type with the device type. The connection can only be LE or Classic, but the device +// type can also be Dual. +enum class ConnectionType : int64_t { + CONN_TYPE_UNKNOWN = 0, + CONN_TYPE_BREDR = 1, + CONN_TYPE_LE = 2, + CONN_TYPE_END = 3, +}; + // ENUM definition for pairing state that in sync with ChromeOS structured metrics // BluetoothPairingStateChanged/PairingState and BlueZ metrics_pair_result. enum class PairingState : int64_t { @@ -131,6 +142,9 @@ struct ProfileConnectionEvent { // Convert topshim::btif::BtState to AdapterState. AdapterState ToAdapterState(uint32_t state); +// Convert topshim::btif::BtDeviceType to ConnectionType +ConnectionType ToPairingDeviceType(std::string addr, uint32_t device_type); + // Convert topshim::btif::bond_state info (status, addr, bond_state, and fail_reason) to PairingState PairingState ToPairingState(uint32_t status, uint32_t bond_state, int32_t fail_reason); diff --git a/system/gd/rust/common/src/init_flags.rs b/system/gd/rust/common/src/init_flags.rs index 4fabae0e22..029283d6d6 100644 --- a/system/gd/rust/common/src/init_flags.rs +++ b/system/gd/rust/common/src/init_flags.rs @@ -74,10 +74,12 @@ macro_rules! init_flags { init_flags!( flags: { + sdp_serialization, gd_core, gd_security, gd_l2cap, - gatt_robust_caching, + gatt_robust_caching_client, + gatt_robust_caching_server, btaa_hci, gd_rust, gd_link_policy diff --git a/system/gd/rust/linux/client/src/command_handler.rs b/system/gd/rust/linux/client/src/command_handler.rs index 94ad8e0813..af6cb16331 100644 --- a/system/gd/rust/linux/client/src/command_handler.rs +++ b/system/gd/rust/linux/client/src/command_handler.rs @@ -595,7 +595,16 @@ impl CommandHandler { name: String::from("Classic Device"), }; - let (name, alias, device_type, class, bonded, connection_state, uuids) = { + let ( + name, + alias, + device_type, + class, + appearance, + bonded, + connection_state, + uuids, + ) = { let ctx = self.context.lock().unwrap(); let adapter = ctx.adapter_dbus.as_ref().unwrap(); @@ -603,6 +612,7 @@ impl CommandHandler { let device_type = adapter.get_remote_type(device.clone()); let alias = adapter.get_remote_alias(device.clone()); let class = adapter.get_remote_class(device.clone()); + let appearance = adapter.get_remote_appearance(device.clone()); let bonded = adapter.get_bond_state(device.clone()); let connection_state = match adapter.get_connection_state(device.clone()) { BtConnectionState::NotConnected => "Not Connected", @@ -611,7 +621,16 @@ impl CommandHandler { }; let uuids = adapter.get_remote_uuids(device.clone()); - (name, alias, device_type, class, bonded, connection_state, uuids) + ( + name, + alias, + device_type, + class, + appearance, + bonded, + connection_state, + uuids, + ) }; let uuid_helper = UuidHelper::new(); @@ -620,6 +639,7 @@ impl CommandHandler { print_info!("Alias: {}", alias); print_info!("Type: {:?}", device_type); print_info!("Class: {}", class); + print_info!("Appearance: {}", appearance); print_info!("Bond State: {:?}", bonded); print_info!("Connection State: {}", connection_state); print_info!( @@ -707,7 +727,7 @@ impl CommandHandler { ); } "client-connect" => { - if args.len() < 3 { + if args.len() < 2 { println!("usage: gatt client-connect <addr>"); return; } @@ -729,7 +749,7 @@ impl CommandHandler { ); } "client-disconnect" => { - if args.len() < 3 { + if args.len() < 2 { println!("usage: gatt client-disconnect <addr>"); return; } @@ -750,7 +770,7 @@ impl CommandHandler { .client_disconnect(client_id.unwrap(), addr); } "client-read-phy" => { - if args.len() < 3 { + if args.len() < 2 { println!("usage: gatt client-read-phy <addr>"); return; } @@ -771,7 +791,7 @@ impl CommandHandler { .client_read_phy(client_id.unwrap(), addr); } "client-discover-services" => { - if args.len() < 3 { + if args.len() < 2 { println!("usage: gatt client-discover-services <addr>"); return; } @@ -792,7 +812,7 @@ impl CommandHandler { .discover_services(client_id.unwrap(), addr); } "configure-mtu" => { - if args.len() < 4 { + if args.len() < 3 { println!("usage: gatt configure-mtu <addr> <mtu>"); return; } diff --git a/system/gd/rust/linux/client/src/dbus_iface.rs b/system/gd/rust/linux/client/src/dbus_iface.rs index 6937455095..83c837f193 100644 --- a/system/gd/rust/linux/client/src/dbus_iface.rs +++ b/system/gd/rust/linux/client/src/dbus_iface.rs @@ -538,6 +538,11 @@ impl IBluetooth for BluetoothDBus { dbus_generated!() } + #[dbus_method("GetRemoteAppearance")] + fn get_remote_appearance(&self, device: BluetoothDevice) -> u16 { + dbus_generated!() + } + #[dbus_method("GetRemoteConnected")] fn get_remote_connected(&self, device: BluetoothDevice) -> bool { dbus_generated!() diff --git a/system/gd/rust/linux/service/Cargo.toml b/system/gd/rust/linux/service/Cargo.toml index 0e0b9e3fb7..36dc6ce9ae 100644 --- a/system/gd/rust/linux/service/Cargo.toml +++ b/system/gd/rust/linux/service/Cargo.toml @@ -16,7 +16,9 @@ dbus-crossroads = "0.4.0" dbus-tokio = "0.7.3" env_logger = "0.8.3" futures = "0.3.13" +lazy_static = "1.4" log = "0.4.14" +nix = "0.19" num-traits = "*" tokio = { version = "1", features = ['bytes', 'fs', 'io-util', 'libc', 'macros', 'memchr', 'mio', 'net', 'num_cpus', 'rt', 'rt-multi-thread', 'sync', 'time', 'tokio-macros'] } syslog = "4.0" diff --git a/system/gd/rust/linux/service/src/iface_battery_manager.rs b/system/gd/rust/linux/service/src/iface_battery_manager.rs index 88f4fe37cf..7ab0b0cb58 100644 --- a/system/gd/rust/linux/service/src/iface_battery_manager.rs +++ b/system/gd/rust/linux/service/src/iface_battery_manager.rs @@ -9,7 +9,7 @@ use crate::dbus_arg::{DBusArg, DBusArgError, RefArgToRust}; #[dbus_propmap(Battery)] pub struct BatteryDBus { - percentage: i32, + percentage: u32, source_info: String, variant: String, } @@ -28,22 +28,26 @@ struct IBatteryManagerDBus {} #[generate_dbus_exporter(export_battery_manager_dbus_intf, "org.chromium.bluetooth.BatteryManager")] impl IBatteryManager for IBatteryManagerDBus { - #[dbus_method("GetBatteryInformation")] - fn get_battery_information(&self, remote_address: String) -> Battery { - dbus_generated!() - } - #[dbus_method("RegisterBatteryCallback")] fn register_battery_callback( &mut self, - remote_address: String, battery_manager_callback: Box<dyn IBatteryManagerCallback + Send>, - ) -> i32 { + ) -> u32 { dbus_generated!() } #[dbus_method("UnregisterBatteryCallback")] - fn unregister_battery_callback(&mut self, callback_id: i32) { + fn unregister_battery_callback(&mut self, callback_id: u32) { + dbus_generated!() + } + + #[dbus_method("EnableNotifications")] + fn enable_notifications(&mut self, callback_id: u32, enable: bool) { + dbus_generated!() + } + + #[dbus_method("GetBatteryInformation")] + fn get_battery_information(&self, remote_address: String) -> Option<Battery> { dbus_generated!() } } diff --git a/system/gd/rust/linux/service/src/iface_bluetooth.rs b/system/gd/rust/linux/service/src/iface_bluetooth.rs index 00beb6c81b..7977ceab27 100644 --- a/system/gd/rust/linux/service/src/iface_bluetooth.rs +++ b/system/gd/rust/linux/service/src/iface_bluetooth.rs @@ -316,6 +316,11 @@ impl IBluetooth for IBluetoothDBus { dbus_generated!() } + #[dbus_method("GetRemoteAppearance")] + fn get_remote_appearance(&self, _device: BluetoothDevice) -> u16 { + dbus_generated!() + } + #[dbus_method("GetRemoteConnected")] fn get_remote_connected(&self, _device: BluetoothDevice) -> bool { dbus_generated!() diff --git a/system/gd/rust/linux/service/src/main.rs b/system/gd/rust/linux/service/src/main.rs index 5c2c2694bf..4cf0dde7f6 100644 --- a/system/gd/rust/linux/service/src/main.rs +++ b/system/gd/rust/linux/service/src/main.rs @@ -1,4 +1,6 @@ extern crate clap; +#[macro_use] +extern crate lazy_static; use clap::{App, AppSettings, Arg}; use dbus::{channel::MatchingReceiver, message::MatchRule}; @@ -6,22 +8,27 @@ use dbus_crossroads::Crossroads; use dbus_tokio::connection; use futures::future; use log::LevelFilter; +use nix::sys::signal; use std::error::Error; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::Duration; use syslog::{BasicLogger, Facility, Formatter3164}; +use tokio::time; use bt_topshim::{btif::get_btinterface, topstack}; use btstack::{ battery_manager::BatteryManager, battery_provider_manager::BatteryProviderManager, + battery_service::BatteryService, bluetooth::{get_bt_dispatcher, Bluetooth, IBluetooth}, bluetooth_gatt::BluetoothGatt, bluetooth_media::BluetoothMedia, socket_manager::BluetoothSocketManager, suspend::Suspend, - Stack, + Message, Stack, }; use dbus_projection::DisconnectWatcher; +use tokio::sync::mpsc::Sender; mod dbus_arg; mod iface_battery_manager; @@ -100,6 +107,7 @@ fn main() -> Result<(), Box<dyn Error>> { } let (tx, rx) = Stack::create_channel(); + let sig_notifier = Arc::new((Mutex::new(false), Condvar::new())); let intf = Arc::new(Mutex::new(get_btinterface().unwrap())); let bluetooth_gatt = @@ -107,11 +115,15 @@ fn main() -> Result<(), Box<dyn Error>> { let bluetooth_media = Arc::new(Mutex::new(Box::new(BluetoothMedia::new(tx.clone(), intf.clone())))); let battery_provider_manager = Arc::new(Mutex::new(Box::new(BatteryProviderManager::new()))); - let battery_manager = Arc::new(Mutex::new(Box::new(BatteryManager::new()))); + let battery_service = + Arc::new(Mutex::new(Box::new(BatteryService::new(bluetooth_gatt.clone(), tx.clone())))); + let battery_manager = + Arc::new(Mutex::new(Box::new(BatteryManager::new(battery_service.clone(), tx.clone())))); let bluetooth = Arc::new(Mutex::new(Box::new(Bluetooth::new( tx.clone(), intf.clone(), bluetooth_media.clone(), + sig_notifier.clone(), )))); let suspend = Arc::new(Mutex::new(Box::new(Suspend::new( bluetooth.clone(), @@ -156,6 +168,8 @@ fn main() -> Result<(), Box<dyn Error>> { rx, bluetooth.clone(), bluetooth_gatt.clone(), + battery_service.clone(), + battery_manager.clone(), bluetooth_media.clone(), suspend.clone(), bt_sock_mgr.clone(), @@ -255,7 +269,7 @@ fn main() -> Result<(), Box<dyn Error>> { battery_provider_manager.clone(), ); cr.lock().unwrap().insert( - make_object_name(adapter_index, "battery__manager"), + make_object_name(adapter_index, "battery_manager"), &[battery_manager_iface], battery_manager.clone(), ); @@ -273,7 +287,28 @@ fn main() -> Result<(), Box<dyn Error>> { bluetooth.enable(); bluetooth_gatt.lock().unwrap().init_profiles(tx.clone(), adapter.clone()); + // TODO(b/247093293): Gatt topshim api is only usable some + // time after init. Investigate why this delay is needed + // and make it a blocking part of init before removing + // this. + tokio::spawn(async move { + time::sleep(Duration::from_millis(500)).await; + battery_service.lock().unwrap().init(); + }); bt_sock_mgr.lock().unwrap().initialize(intf.clone()); + + // Install SIGTERM handler so that we can properly shutdown + *SIG_DATA.lock().unwrap() = Some((tx.clone(), sig_notifier.clone())); + + let sig_action = signal::SigAction::new( + signal::SigHandler::Handler(handle_sigterm), + signal::SaFlags::empty(), + signal::SigSet::empty(), + ); + + unsafe { + signal::sigaction(signal::SIGTERM, &sig_action).unwrap(); + } } // Serve clients forever. @@ -281,3 +316,29 @@ fn main() -> Result<(), Box<dyn Error>> { unreachable!() }) } + +lazy_static! { + /// Data needed for signal handling. + static ref SIG_DATA: Mutex<Option<(Sender<Message>, Arc<(Mutex<bool>, Condvar)>)>> = Mutex::new(None); +} + +extern "C" fn handle_sigterm(_signum: i32) { + let guard = SIG_DATA.lock().unwrap(); + if let Some((tx, notifier)) = guard.as_ref() { + log::debug!("Handling SIGTERM by disabling the adapter!"); + let txl = tx.clone(); + tokio::spawn(async move { + // Send the shutdown message here. + let _ = txl.send(Message::Shutdown).await; + }); + + let guard = notifier.0.lock().unwrap(); + if *guard { + log::debug!("Waiting for stack to turn off for 2s"); + let _ = notifier.1.wait_timeout(guard, std::time::Duration::from_millis(2000)); + } + } + + log::debug!("Sigterm completed"); + std::process::exit(0); +} diff --git a/system/gd/rust/linux/stack/src/battery_manager.rs b/system/gd/rust/linux/stack/src/battery_manager.rs index 9bb39d41a3..4d16cbf58a 100644 --- a/system/gd/rust/linux/stack/src/battery_manager.rs +++ b/system/gd/rust/linux/stack/src/battery_manager.rs @@ -1,20 +1,24 @@ +use crate::battery_service::{ + BatteryService, BatteryServiceStatus, IBatteryService, IBatteryServiceCallback, +}; +use crate::callbacks::Callbacks; +use crate::Message; +use crate::RPCProxy; +use std::collections::HashSet; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc::Sender; + +/// The primary representation of battery information for internal +/// passing and external calls. #[derive(Debug, Clone)] pub struct Battery { - pub percentage: i32, + pub percentage: u32, pub source_info: String, pub variant: String, } -pub struct BatteryManager {} - -impl BatteryManager { - pub fn new() -> BatteryManager { - BatteryManager {} - } -} - /// Callback for interacting with the BatteryManager. -pub trait IBatteryManagerCallback { +pub trait IBatteryManagerCallback: RPCProxy { /// Invoked whenever battery information associated with the given remote changes. fn on_battery_info_updated(&self, remote_address: String, battery: Battery); } @@ -25,31 +29,108 @@ pub trait IBatteryManager { /// callback_id for future calls. fn register_battery_callback( &mut self, - remote_address: String, battery_manager_callback: Box<dyn IBatteryManagerCallback + Send>, - ) -> i32; + ) -> u32; /// Unregister a callback. - fn unregister_battery_callback(&mut self, callback_id: i32); + fn unregister_battery_callback(&mut self, callback_id: u32); + + /// Enables notifications for a given callback. + fn enable_notifications(&mut self, callback_id: u32, enable: bool); /// Returns battery information for the remote, sourced from the highest priority origin. - fn get_battery_information(&self, remote_address: String) -> Battery; + fn get_battery_information(&self, remote_address: String) -> Option<Battery>; } -impl IBatteryManager for BatteryManager { - fn register_battery_callback( - &mut self, +/// Repesentation of the BatteryManager. +pub struct BatteryManager { + bas: Arc<Mutex<Box<BatteryService>>>, + callbacks: Callbacks<dyn IBatteryManagerCallback + Send>, + /// List of callback IDs that have enabled notifications. + notifications_enabled: HashSet<u32>, +} + +impl BatteryManager { + /// Construct a new BatteryManager with callbacks communicating on tx. + pub fn new(bas: Arc<Mutex<Box<BatteryService>>>, tx: Sender<Message>) -> BatteryManager { + let callbacks = Callbacks::new(tx.clone(), Message::BatteryManagerCallbackDisconnected); + let notifications_enabled = HashSet::new(); + Self { bas, callbacks, notifications_enabled } + } + + /// Invoked after BAS has been initialized. + pub fn init(&self) { + self.bas.lock().unwrap().register_callback(Box::new(BasCallback::new())); + } + + /// Remove a callback due to disconnection or unregistration. + pub fn remove_callback(&mut self, callback_id: u32) { + self.callbacks.remove_callback(callback_id); + } +} + +struct BasCallback {} + +impl BasCallback { + pub fn new() -> BasCallback { + Self {} + } +} + +impl IBatteryServiceCallback for BasCallback { + fn on_battery_service_status_updated( + &self, _remote_address: String, - _battery_manager_callback: Box<dyn IBatteryManagerCallback + Send>, - ) -> i32 { + _status: BatteryServiceStatus, + ) { todo!() } - fn unregister_battery_callback(&mut self, _callback_id: i32) { + fn on_battery_level_updated(&self, _remote_address: String, _battery_level: u32) { todo!() } - fn get_battery_information(&self, _remote_address: String) -> Battery { + fn on_battery_level_read(&self, _remote_address: String, _battery_level: u32) { todo!() } } + +impl RPCProxy for BasCallback { + fn get_object_id(&self) -> String { + "BAS Callback".to_string() + } +} + +impl IBatteryManager for BatteryManager { + fn register_battery_callback( + &mut self, + battery_manager_callback: Box<dyn IBatteryManagerCallback + Send>, + ) -> u32 { + self.callbacks.add_callback(battery_manager_callback) + } + + fn unregister_battery_callback(&mut self, callback_id: u32) { + self.remove_callback(callback_id); + } + + fn enable_notifications(&mut self, callback_id: u32, enable: bool) { + if self.callbacks.get_by_id(callback_id).is_none() { + return; + } + self.notifications_enabled.remove(&callback_id); + if enable { + self.notifications_enabled.insert(callback_id); + } + } + + // TODO(b/233101174): update to use all available sources once + // BatteryProviderManager is implemented. + fn get_battery_information(&self, remote_address: String) -> Option<Battery> { + let battery_level = self.bas.lock().unwrap().get_battery_level(remote_address)?; + Some(Battery { + percentage: battery_level, + source_info: "BAS".to_string(), + variant: "".to_string(), + }) + } +} diff --git a/system/gd/rust/linux/stack/src/battery_service.rs b/system/gd/rust/linux/stack/src/battery_service.rs new file mode 100644 index 0000000000..3fe9a9bb2e --- /dev/null +++ b/system/gd/rust/linux/stack/src/battery_service.rs @@ -0,0 +1,397 @@ +use crate::bluetooth_gatt::{ + BluetoothGatt, BluetoothGattService, IBluetoothGatt, IBluetoothGattCallback, +}; +use crate::callbacks::Callbacks; +use crate::uuid; +use crate::uuid::parse_uuid_string; +use crate::Message; +use crate::RPCProxy; +use bt_topshim::btif::BtTransport; +use bt_topshim::profiles::gatt::{GattStatus, LePhy}; +use log::debug; +use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; +use std::iter; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc::Sender; + +/// The UUID corresponding to the BatteryLevel characteristic defined +/// by the BatteryService specification. +pub const CHARACTERISTIC_BATTERY_LEVEL: &str = "00002A1900001000800000805F9B34FB"; + +/// Represents the Floss BatteryService implementation. +pub struct BatteryService { + gatt: Arc<Mutex<Box<BluetoothGatt>>>, + /// Sender for callback communication with the main thread. + tx: Sender<Message>, + callbacks: Callbacks<dyn IBatteryServiceCallback + Send>, + /// The GATT client ID needed for GATT calls. + client_id: Option<i32>, + /// Cached battery levels keyed by remote device. + battery_levels: HashMap<String, u32>, + /// Callback IDs that have enabled notifications. + notifications_enabled: HashSet<u32>, + /// Found handles for battery levels. Required for faster + /// refreshes than initiating another search. + handles: HashMap<String, i32>, +} + +/// Enum for GATT callbacks to relay messages to the main processing +/// thread. Newly supported callbacks should add a corresponding entry +/// here. +pub enum GattBatteryCallbacks { + /// Params: status, client_id + OnClientRegistered(GattStatus, i32), + /// Params: status, client_id, connected, addr + OnClientConnectionState(GattStatus, i32, bool, String), + /// Params: addr, services, status + OnSearchComplete(String, Vec<BluetoothGattService>, GattStatus), + /// Params: addr, status, handle, value + OnCharacteristicRead(String, GattStatus, i32, Vec<u8>), + /// Params: addr, handle, value + OnNotify(String, i32, Vec<u8>), +} + +/// API for Floss implementation of the Bluetooth Battery Service +/// (BAS). BAS is built on GATT and this implementation wraps all of +/// the GATT calls and handles tracking battery information for the +/// client. +pub trait IBatteryService { + /// Registers a callback for interacting with BatteryService. + fn register_callback(&mut self, callback: Box<dyn IBatteryServiceCallback + Send>) -> u32; + + /// Unregisters a callback. + fn unregister_callback(&mut self, callback_id: u32); + + /// Enables notifications for a given callback. + fn enable_notifications(&mut self, callback_id: u32, enable: bool); + + /// Returns the battery level of the remove device if available in + /// BatteryService's cache. Call refresh_battery_level at least + /// once to ensure that BatteryService is tracking the device's + /// battery information. + fn get_battery_level(&self, remote_address: String) -> Option<u32>; + + /// Forces an explicit read of the device's battery level, + /// including initiating battery level tracking if not yet + /// performed. + fn refresh_battery_level(&self, remote_address: String) -> bool; +} + +/// Callback for interacting with BAS. +pub trait IBatteryServiceCallback: RPCProxy { + /// Called when the status of BatteryService has changed. Trying + /// to read from devices that do not support BAS will result in + /// this method being called with BatteryServiceNotSupported. + fn on_battery_service_status_updated( + &self, + remote_address: String, + status: BatteryServiceStatus, + ); + + /// Invoked when battery level for a device has been changed due to notification. + fn on_battery_level_updated(&self, remote_address: String, battery_level: u32); + + /// Invoked whenever an explicit read of a devices battery level completes. + fn on_battery_level_read(&self, remote_address: String, battery_level: u32); +} + +impl BatteryService { + /// Construct a new BatteryService with callbacks relaying messages through tx. + pub fn new(gatt: Arc<Mutex<Box<BluetoothGatt>>>, tx: Sender<Message>) -> BatteryService { + let tx = tx.clone(); + let callbacks = Callbacks::new(tx.clone(), Message::BatteryServiceCallbackDisconnected); + let client_id = None; + let battery_levels = HashMap::new(); + let notifications_enabled = HashSet::new(); + let handles = HashMap::new(); + Self { gatt, tx, callbacks, client_id, battery_levels, notifications_enabled, handles } + } + + /// Must be called after BluetoothGatt's init_profiles method has completed. + pub fn init(&self) { + self.gatt.lock().unwrap().register_client( + // TODO(b/233101174): make dynamic or decide on a static UUID + String::from("e4d2acffcfaa42198f494606b7412117"), + Box::new(GattCallback::new(self.tx.clone())), + false, + ); + } + + /// Handles all callback messages in a central location to avoid deadlocks. + pub fn handle_callback(&mut self, callback: GattBatteryCallbacks) { + match callback { + GattBatteryCallbacks::OnClientRegistered(_status, client_id) => { + self.client_id = Some(client_id); + } + + GattBatteryCallbacks::OnClientConnectionState(_status, _client_id, connected, addr) => { + if !connected { + return; + } + let client_id = match self.client_id { + Some(id) => id, + None => { + return; + } + }; + self.gatt.lock().unwrap().discover_services(client_id, addr); + } + + GattBatteryCallbacks::OnSearchComplete(addr, services, status) => { + if status != GattStatus::Success { + debug!("GATT service discovery for {} failed with status {:?}", addr, status); + return; + } + let (bas_uuid, battery_level_uuid) = match ( + parse_uuid_string(uuid::BAS), + parse_uuid_string(CHARACTERISTIC_BATTERY_LEVEL), + ) { + (Some(bas_uuid), Some(battery_level_uuid)) => (bas_uuid, battery_level_uuid), + _ => return, + }; + // TODO(b/233101174): handle multiple instances of BAS + let bas = match services.iter().find(|service| service.uuid == bas_uuid.uu) { + Some(bas) => bas, + None => { + self.callbacks.for_all_callbacks(|callback| { + callback.on_battery_service_status_updated( + addr.clone(), + BatteryServiceStatus::BatteryServiceNotSupported, + ) + }); + return; + } + }; + let battery_level = match bas + .characteristics + .iter() + .find(|characteristic| characteristic.uuid == battery_level_uuid.uu) + { + Some(battery_level) => battery_level, + None => { + debug!("Device {} has no BatteryLevel characteristic", addr); + return; + } + }; + let client_id = match self.client_id { + Some(id) => id, + None => return, + }; + let handle = battery_level.instance_id; + self.handles.insert(addr.clone(), handle.clone()); + self.gatt.lock().unwrap().register_for_notification( + client_id, + addr.clone(), + handle, + true, + ); + if let None = self.battery_levels.get(&addr) { + self.gatt.lock().unwrap().read_characteristic( + client_id, + addr, + battery_level.instance_id, + 0, + ); + } + } + + GattBatteryCallbacks::OnCharacteristicRead(addr, status, _handle, value) => { + if status != GattStatus::Success { + return; + } + let level = self.set_battery_level(addr.clone(), value.clone()); + self.callbacks.for_all_callbacks(|callback| { + callback.on_battery_level_read(addr.clone(), level); + }); + } + + GattBatteryCallbacks::OnNotify(addr, _handle, value) => { + let level = self.set_battery_level(addr.clone(), value); + // TODO(b/247551256): expand Callbacks to allow direct + // filtering/exposing the underlying iter + let to_notify = self.notifications_enabled.clone(); + to_notify.iter().for_each(|id| match self.callbacks.get_by_id(*id) { + Some(callback) => callback.on_battery_level_updated(addr.clone(), level), + None => (), + }); + } + } + } + + fn set_battery_level(&mut self, remote_address: String, value: Vec<u8>) -> u32 { + let level: Vec<_> = value.iter().cloned().chain(iter::repeat(0 as u8)).take(4).collect(); + let level = u32::from_le_bytes(level.try_into().unwrap()); + self.battery_levels.insert(remote_address, level); + level + } + + fn init_device(&self, remote_address: String) { + let client_id = match self.client_id { + Some(id) => id, + None => return, + }; + self.gatt.lock().unwrap().client_connect( + client_id, + remote_address, + false, + BtTransport::Le, + false, + LePhy::Phy1m, + ); + } + + /// Remove a callback due to disconnection or unregistration. + pub fn remove_callback(&mut self, callback_id: u32) { + self.callbacks.remove_callback(callback_id); + } +} + +/// Status enum for relaying the state of BAS or a particular device. +pub enum BatteryServiceStatus { + /// Device does not report support for BAS. + BatteryServiceNotSupported, +} + +impl IBatteryService for BatteryService { + fn register_callback(&mut self, callback: Box<dyn IBatteryServiceCallback + Send>) -> u32 { + self.callbacks.add_callback(callback) + } + + fn unregister_callback(&mut self, callback_id: u32) { + self.remove_callback(callback_id); + } + + fn enable_notifications(&mut self, callback_id: u32, enable: bool) { + if self.callbacks.get_by_id(callback_id).is_none() { + return; + } + self.notifications_enabled.remove(&callback_id); + if enable { + self.notifications_enabled.insert(callback_id); + } + } + + fn get_battery_level(&self, remote_address: String) -> Option<u32> { + self.battery_levels.get(&remote_address).cloned() + } + + fn refresh_battery_level(&self, remote_address: String) -> bool { + let client_id = match self.client_id { + Some(id) => id, + None => return false, + }; + let handle = match self.handles.get(&remote_address) { + Some(id) => *id, + None => { + self.init_device(remote_address); + return true; + } + }; + self.gatt.lock().unwrap().read_characteristic(client_id, remote_address.clone(), handle, 0); + self.gatt.lock().unwrap().register_for_notification( + client_id, + remote_address, + handle, + true, + ); + true + } +} + +struct GattCallback { + tx: Sender<Message>, +} + +impl GattCallback { + fn new(tx: Sender<Message>) -> Self { + Self { tx } + } +} + +impl IBluetoothGattCallback for GattCallback { + // All callback methods relay messages through the stack receiver + // to allow BAS to operate on requests serially. This reduces + // overall complexity including removing the need to share state + // data with callbacks. + + fn on_client_registered(&self, status: GattStatus, client_id: i32) { + let tx = self.tx.clone(); + tokio::spawn(async move { + let _ = tx + .send(Message::BatteryServiceCallbacks(GattBatteryCallbacks::OnClientRegistered( + status, client_id, + ))) + .await; + }); + } + + fn on_client_connection_state( + &self, + status: GattStatus, + client_id: i32, + connected: bool, + addr: String, + ) { + let tx = self.tx.clone(); + tokio::spawn(async move { + let _ = tx + .send(Message::BatteryServiceCallbacks( + GattBatteryCallbacks::OnClientConnectionState( + status, client_id, connected, addr, + ), + )) + .await; + }); + } + + fn on_search_complete( + &self, + addr: String, + services: Vec<BluetoothGattService>, + status: GattStatus, + ) { + let tx = self.tx.clone(); + tokio::spawn(async move { + let _ = tx + .send(Message::BatteryServiceCallbacks(GattBatteryCallbacks::OnSearchComplete( + addr, services, status, + ))) + .await; + }); + } + + fn on_characteristic_read( + &self, + addr: String, + status: GattStatus, + handle: i32, + value: Vec<u8>, + ) { + let tx = self.tx.clone(); + tokio::spawn(async move { + let _ = tx + .send(Message::BatteryServiceCallbacks(GattBatteryCallbacks::OnCharacteristicRead( + addr, status, handle, value, + ))) + .await; + }); + } + + fn on_notify(&self, addr: String, handle: i32, value: Vec<u8>) { + let tx = self.tx.clone(); + tokio::spawn(async move { + let _ = tx + .send(Message::BatteryServiceCallbacks(GattBatteryCallbacks::OnNotify( + addr, handle, value, + ))) + .await; + }); + } +} + +impl RPCProxy for GattCallback { + fn get_object_id(&self) -> String { + "BAS Gatt Callback".to_string() + } +} diff --git a/system/gd/rust/linux/stack/src/bluetooth.rs b/system/gd/rust/linux/stack/src/bluetooth.rs index 8dea15126f..67a70cc7ab 100644 --- a/system/gd/rust/linux/stack/src/bluetooth.rs +++ b/system/gd/rust/linux/stack/src/bluetooth.rs @@ -22,8 +22,7 @@ use log::{debug, error, warn}; use num_traits::cast::ToPrimitive; use std::collections::HashMap; use std::collections::VecDeque; -use std::sync::Arc; -use std::sync::Mutex; +use std::sync::{Arc, Condvar, Mutex}; use std::time::Duration; use std::time::Instant; use tokio::sync::mpsc::Sender; @@ -157,6 +156,9 @@ pub trait IBluetooth { /// Gets the class of the remote device. fn get_remote_class(&self, device: BluetoothDevice) -> u32; + /// Gets the appearance of the remote device. + fn get_remote_appearance(&self, device: BluetoothDevice) -> u16; + /// Gets whether the remote device is connected. fn get_remote_connected(&self, device: BluetoothDevice) -> bool; @@ -352,6 +354,9 @@ pub struct Bluetooth { // Internal API members internal_le_rand_queue: VecDeque<OneShotSender<u64>>, discoverable_timeout: Option<JoinHandle<()>>, + + /// Used to notify signal handler that we have turned off the stack. + sig_notifier: Arc<(Mutex<bool>, Condvar)>, } impl Bluetooth { @@ -360,6 +365,7 @@ impl Bluetooth { tx: Sender<Message>, intf: Arc<Mutex<BluetoothInterface>>, bluetooth_media: Arc<Mutex<Box<BluetoothMedia>>>, + sig_notifier: Arc<(Mutex<bool>, Condvar)>, ) -> Bluetooth { Bluetooth { bonded_devices: HashMap::new(), @@ -387,6 +393,7 @@ impl Bluetooth { // Internal API members internal_le_rand_queue: VecDeque::<OneShotSender<u64>>::new(), discoverable_timeout: None, + sig_notifier, } } @@ -697,6 +704,10 @@ impl BtifBluetoothCallbacks for Bluetooth { if self.state == BtState::Off { self.properties.clear(); + + // Let the signal notifier know we are turned off. + *self.sig_notifier.0.lock().unwrap() = false; + self.sig_notifier.1.notify_all(); } else { // Trigger properties update self.intf.lock().unwrap().get_adapter_properties(); @@ -706,6 +717,10 @@ impl BtifBluetoothCallbacks for Bluetooth { // Ensure device is connectable so that disconnected device can reconnect self.set_connectable(true); + + // Notify the signal notifier that we are turned on. + *self.sig_notifier.0.lock().unwrap() = true; + self.sig_notifier.1.notify_all(); } } @@ -1215,7 +1230,11 @@ impl IBluetooth for Bluetooth { } let address = addr.unwrap(); - let device_type = self.get_remote_type(device); + let device_type = match transport { + BtTransport::Bredr => BtDeviceType::Bredr, + BtTransport::Le => BtDeviceType::Ble, + _ => self.get_remote_type(device), + }; // We explicitly log the attempt to start the bonding separate from logging the bond state. // The start of the attempt is critical to help identify a bonding/pairing session. @@ -1401,6 +1420,13 @@ impl IBluetooth for Bluetooth { } } + fn get_remote_appearance(&self, device: BluetoothDevice) -> u16 { + match self.get_remote_device_property(&device, &BtPropertyType::Appearance) { + Some(BluetoothProperty::Appearance(appearance)) => appearance, + _ => 0, + } + } + fn get_remote_connected(&self, device: BluetoothDevice) -> bool { self.get_connection_state(device) != BtConnectionState::NotConnected } diff --git a/system/gd/rust/linux/stack/src/bluetooth_gatt.rs b/system/gd/rust/linux/stack/src/bluetooth_gatt.rs index 00ffd41973..f0fb53dc56 100644 --- a/system/gd/rust/linux/stack/src/bluetooth_gatt.rs +++ b/system/gd/rust/linux/stack/src/bluetooth_gatt.rs @@ -499,67 +499,84 @@ impl BluetoothGattService { /// Callback for GATT Client API. pub trait IBluetoothGattCallback: RPCProxy { /// When the `register_client` request is done. - fn on_client_registered(&self, status: GattStatus, client_id: i32); + fn on_client_registered(&self, _status: GattStatus, _client_id: i32) {} /// When there is a change in the state of a GATT client connection. fn on_client_connection_state( &self, - status: GattStatus, - client_id: i32, - connected: bool, - addr: String, - ); + _status: GattStatus, + _client_id: i32, + _connected: bool, + _addr: String, + ) { + } /// When there is a change of PHY. - fn on_phy_update(&self, addr: String, tx_phy: LePhy, rx_phy: LePhy, status: GattStatus); + fn on_phy_update(&self, _addr: String, _tx_phy: LePhy, _rx_phy: LePhy, _status: GattStatus) {} /// The completion of IBluetoothGatt::read_phy. - fn on_phy_read(&self, addr: String, tx_phy: LePhy, rx_phy: LePhy, status: GattStatus); + fn on_phy_read(&self, _addr: String, _tx_phy: LePhy, _rx_phy: LePhy, _status: GattStatus) {} /// When GATT db is available. fn on_search_complete( &self, - addr: String, - services: Vec<BluetoothGattService>, - status: GattStatus, - ); + _addr: String, + _services: Vec<BluetoothGattService>, + _status: GattStatus, + ) { + } /// The completion of IBluetoothGatt::read_characteristic. - fn on_characteristic_read(&self, addr: String, status: GattStatus, handle: i32, value: Vec<u8>); + fn on_characteristic_read( + &self, + _addr: String, + _status: GattStatus, + _handle: i32, + _value: Vec<u8>, + ) { + } /// The completion of IBluetoothGatt::write_characteristic. - fn on_characteristic_write(&self, addr: String, status: GattStatus, handle: i32); + fn on_characteristic_write(&self, _addr: String, _status: GattStatus, _handle: i32) {} /// When a reliable write is completed. - fn on_execute_write(&self, addr: String, status: GattStatus); + fn on_execute_write(&self, _addr: String, _status: GattStatus) {} /// The completion of IBluetoothGatt::read_descriptor. - fn on_descriptor_read(&self, addr: String, status: GattStatus, handle: i32, value: Vec<u8>); + fn on_descriptor_read( + &self, + _addr: String, + _status: GattStatus, + _handle: i32, + _value: Vec<u8>, + ) { + } /// The completion of IBluetoothGatt::write_descriptor. - fn on_descriptor_write(&self, addr: String, status: GattStatus, handle: i32); + fn on_descriptor_write(&self, _addr: String, _status: GattStatus, _handle: i32) {} /// When notification or indication is received. - fn on_notify(&self, addr: String, handle: i32, value: Vec<u8>); + fn on_notify(&self, _addr: String, _handle: i32, _value: Vec<u8>) {} /// The completion of IBluetoothGatt::read_remote_rssi. - fn on_read_remote_rssi(&self, addr: String, rssi: i32, status: GattStatus); + fn on_read_remote_rssi(&self, _addr: String, _rssi: i32, _status: GattStatus) {} /// The completion of IBluetoothGatt::configure_mtu. - fn on_configure_mtu(&self, addr: String, mtu: i32, status: GattStatus); + fn on_configure_mtu(&self, _addr: String, _mtu: i32, _status: GattStatus) {} /// When a connection parameter changes. fn on_connection_updated( &self, - addr: String, - interval: i32, - latency: i32, - timeout: i32, - status: GattStatus, - ); + _addr: String, + _interval: i32, + _latency: i32, + _timeout: i32, + _status: GattStatus, + ) { + } /// When there is an addition, removal, or change of a GATT service. - fn on_service_changed(&self, addr: String); + fn on_service_changed(&self, _addr: String) {} } /// Interface for scanner callbacks to clients, passed to @@ -1160,9 +1177,19 @@ impl IBluetoothGatt for BluetoothGatt { callback: Box<dyn IBluetoothGattCallback + Send>, eatt_support: bool, ) { - let uuid = parse_uuid_string(app_uuid).unwrap(); + let uuid = match parse_uuid_string(&app_uuid) { + Some(id) => id, + None => { + log::info!("Uuid is malformed: {}", app_uuid); + return; + } + }; self.context_map.add(&uuid.uu, callback); - self.gatt.as_ref().unwrap().client.register_client(&uuid, eatt_support); + self.gatt + .as_ref() + .expect("GATT has not been initialized") + .client + .register_client(&uuid, eatt_support); } fn unregister_client(&mut self, client_id: i32) { diff --git a/system/gd/rust/linux/stack/src/bluetooth_media.rs b/system/gd/rust/linux/stack/src/bluetooth_media.rs index b065fa0bc7..0672126516 100644 --- a/system/gd/rust/linux/stack/src/bluetooth_media.rs +++ b/system/gd/rust/linux/stack/src/bluetooth_media.rs @@ -1,6 +1,6 @@ //! Anything related to audio and media API. -use bt_topshim::btif::{BluetoothInterface, RawAddress}; +use bt_topshim::btif::{BluetoothInterface, BtStatus, RawAddress}; use bt_topshim::profiles::a2dp::{ A2dp, A2dpCallbacks, A2dpCallbacksDispatcher, A2dpCodecBitsPerSample, A2dpCodecChannelMode, A2dpCodecConfig, A2dpCodecSampleRate, BtavAudioState, BtavConnectionState, @@ -11,7 +11,7 @@ use bt_topshim::profiles::hfp::{ BthfAudioState, BthfConnectionState, Hfp, HfpCallbacks, HfpCallbacksDispatcher, HfpCodecCapability, }; -use bt_topshim::topstack; +use bt_topshim::{metrics, topstack}; use bt_utils::uinput::UInput; use log::{info, warn}; @@ -28,6 +28,7 @@ use tokio::time::{sleep, Duration, Instant}; use crate::bluetooth::{Bluetooth, BluetoothDevice, IBluetooth}; use crate::callbacks::Callbacks; use crate::uuid; +use crate::uuid::Profile; use crate::{Message, RPCProxy}; // The timeout we have to wait for all supported profiles to connect after we @@ -192,48 +193,75 @@ impl BluetoothMedia { } } + fn is_profile_connected(&self, addr: RawAddress, profile: uuid::Profile) -> bool { + if let Some(connected_profiles) = self.connected_profiles.get(&addr) { + return connected_profiles.contains(&profile); + } + return false; + } + + fn add_connected_profile(&mut self, addr: RawAddress, profile: uuid::Profile) { + if self.is_profile_connected(addr, profile) { + warn!("[{}]: profile is already connected", addr.to_string()); + return; + } + + self.connected_profiles.entry(addr).or_insert_with(HashSet::new).insert(profile); + + self.notify_media_capability_updated(addr); + } + + fn rm_connected_profile( + &mut self, + addr: RawAddress, + profile: uuid::Profile, + is_profile_critical: bool, + ) { + if !self.is_profile_connected(addr, profile) { + warn!("[{}]: profile is already disconnected", addr.to_string()); + return; + } + + self.connected_profiles.entry(addr).or_insert_with(HashSet::new).remove(&profile); + + if is_profile_critical { + self.notify_critical_profile_disconnected(addr); + } + + self.notify_media_capability_updated(addr); + } + pub fn set_adapter(&mut self, adapter: Arc<Mutex<Box<Bluetooth>>>) { self.adapter = Some(adapter); } pub fn dispatch_a2dp_callbacks(&mut self, cb: A2dpCallbacks) { match cb { - A2dpCallbacks::ConnectionState(addr, state) => { + A2dpCallbacks::ConnectionState(addr, state, error) => { if !self.a2dp_states.get(&addr).is_none() && state == *self.a2dp_states.get(&addr).unwrap() { return; } + metrics::profile_connection_state_changed( + addr, + Profile::A2dpSink as u32, + error.status, + state.clone() as u32, + ); match state { BtavConnectionState::Connected => { info!("[{}]: a2dp connected.", addr.to_string()); self.a2dp_states.insert(addr, state); - - self.connected_profiles - .entry(addr) - .or_insert_with(HashSet::new) - .insert(uuid::Profile::A2dpSink); - - self.notify_media_capability_updated(addr); + self.add_connected_profile(addr, uuid::Profile::A2dpSink); + } + BtavConnectionState::Disconnected => { + info!("[{}]: a2dp disconnected.", addr.to_string()); + self.a2dp_states.remove(&addr); + self.a2dp_caps.remove(&addr); + self.a2dp_audio_state.remove(&addr); + self.rm_connected_profile(addr, uuid::Profile::A2dpSink, true); } - BtavConnectionState::Disconnected => match self.a2dp_states.remove(&addr) { - Some(_) => { - info!("[{}]: a2dp disconnected.", addr.to_string()); - self.a2dp_caps.remove(&addr); - self.a2dp_audio_state.remove(&addr); - - self.connected_profiles - .entry(addr) - .or_insert_with(HashSet::new) - .remove(&uuid::Profile::A2dpSink); - - self.notify_critical_profile_disconnected(addr); - self.notify_media_capability_updated(addr); - } - None => { - warn!("[{}]: Unknown address a2dp disconnected.", addr.to_string()); - } - }, _ => { self.a2dp_states.insert(addr, state); } @@ -273,12 +301,14 @@ impl BluetoothMedia { self.absolute_volume = supported; - self.connected_profiles - .entry(addr) - .or_insert_with(HashSet::new) - .insert(uuid::Profile::AvrcpController); + metrics::profile_connection_state_changed( + addr, + Profile::AvrcpController as u32, + BtStatus::Success, + BtavConnectionState::Connected as u32, + ); - self.notify_media_capability_updated(addr); + self.add_connected_profile(addr, uuid::Profile::AvrcpController); } AvrcpCallbacks::AvrcpDeviceDisconnected(addr) => { info!("[{}]: avrcp disconnected.", addr.to_string()); @@ -288,18 +318,25 @@ impl BluetoothMedia { // TODO: better support for multi-device self.absolute_volume = false; - self.connected_profiles - .entry(addr) - .or_insert_with(HashSet::new) - .remove(&uuid::Profile::AvrcpController); - // This may be considered a critical profile in the extreme case // where only AVRCP was connected. - if self.connected_profiles.is_empty() { - self.notify_critical_profile_disconnected(addr); - } + let is_profile_critical = match self.connected_profiles.get(&addr) { + Some(profiles) => *profiles == HashSet::from([uuid::Profile::AvrcpController]), + None => false, + }; - self.notify_media_capability_updated(addr); + metrics::profile_connection_state_changed( + addr, + Profile::AvrcpController as u32, + BtStatus::Success, + BtavConnectionState::Disconnected as u32, + ); + + self.rm_connected_profile( + addr, + uuid::Profile::AvrcpController, + is_profile_critical, + ); } AvrcpCallbacks::AvrcpAbsoluteVolumeUpdate(volume) => { self.callbacks.lock().unwrap().for_all_callbacks(|callback| { @@ -333,6 +370,12 @@ impl BluetoothMedia { { return; } + metrics::profile_connection_state_changed( + addr, + Profile::Hfp as u32, + BtStatus::Success, + state.clone() as u32, + ); match state { BthfConnectionState::Connected => { info!("[{}]: hfp connected.", addr.to_string()); @@ -344,34 +387,14 @@ impl BluetoothMedia { if !self.hfp_cap.contains_key(&addr) { self.hfp_cap.insert(addr, HfpCodecCapability::CVSD); } - - self.connected_profiles - .entry(addr) - .or_insert_with(HashSet::new) - .insert(uuid::Profile::Hfp); - - self.notify_media_capability_updated(addr); + self.add_connected_profile(addr, uuid::Profile::Hfp); } BthfConnectionState::Disconnected => { info!("[{}]: hfp disconnected.", addr.to_string()); - match self.hfp_states.remove(&addr) { - Some(_) => { - self.hfp_cap.remove(&addr); - self.hfp_audio_state.remove(&addr); - - self.connected_profiles - .entry(addr) - .or_insert_with(HashSet::new) - .remove(&uuid::Profile::Hfp); - - self.notify_critical_profile_disconnected(addr); - self.notify_media_capability_updated(addr); - } - None => { - warn!("[{}] Unknown address hfp disconnected.", addr.to_string()) - } - } - return; + self.hfp_states.remove(&addr); + self.hfp_cap.remove(&addr); + self.hfp_audio_state.remove(&addr); + self.rm_connected_profile(addr, uuid::Profile::Hfp, true); } BthfConnectionState::Connecting => { info!("[{}]: hfp connecting.", addr.to_string()); @@ -725,24 +748,97 @@ impl IBluetoothMedia for BluetoothMedia { for profile in missing_profiles { match profile { uuid::Profile::A2dpSink => { + metrics::profile_connection_state_changed( + addr, + Profile::A2dpSink as u32, + BtStatus::Success, + BtavConnectionState::Connecting as u32, + ); match self.a2dp.as_mut() { - Some(a2dp) => a2dp.connect(addr), - None => warn!("Uninitialized A2DP to connect {}", address), + Some(a2dp) => { + let status: BtStatus = a2dp.connect(addr); + if BtStatus::Success != status { + metrics::profile_connection_state_changed( + addr, + Profile::A2dpSink as u32, + status, + BtavConnectionState::Disconnected as u32, + ); + } + } + None => { + warn!("Uninitialized A2DP to connect {}", address); + metrics::profile_connection_state_changed( + addr, + Profile::A2dpSink as u32, + BtStatus::NotReady, + BtavConnectionState::Disconnected as u32, + ); + } }; } uuid::Profile::Hfp => { + metrics::profile_connection_state_changed( + addr, + Profile::Hfp as u32, + BtStatus::Success, + BtavConnectionState::Connecting as u32, + ); match self.hfp.as_mut() { - Some(hfp) => hfp.connect(addr), - None => warn!("Uninitialized HFP to connect {}", address), + Some(hfp) => { + let status: BtStatus = hfp.connect(addr); + if BtStatus::Success != status { + metrics::profile_connection_state_changed( + addr, + Profile::Hfp as u32, + status, + BthfConnectionState::Disconnected as u32, + ); + } + } + None => { + warn!("Uninitialized HFP to connect {}", address); + metrics::profile_connection_state_changed( + addr, + Profile::Hfp as u32, + BtStatus::NotReady, + BthfConnectionState::Disconnected as u32, + ); + } }; } uuid::Profile::AvrcpController => { + metrics::profile_connection_state_changed( + addr, + Profile::AvrcpController as u32, + BtStatus::Success, + BtavConnectionState::Connecting as u32, + ); match self.avrcp.as_mut() { - Some(avrcp) => avrcp.connect(addr), - None => warn!("Uninitialized AVRCP to connect {}", address), + Some(avrcp) => { + let status: BtStatus = avrcp.connect(addr); + if BtStatus::Success != status { + metrics::profile_connection_state_changed( + addr, + Profile::AvrcpController as u32, + status, + BtavConnectionState::Disconnected as u32, + ); + } + } + + None => { + warn!("Uninitialized AVRCP to connect {}", address); + metrics::profile_connection_state_changed( + addr, + Profile::AvrcpController as u32, + BtStatus::NotReady, + BtavConnectionState::Disconnected as u32, + ); + } }; } - _ => warn!("Unknown profile."), + _ => warn!("Unknown profile: {:?}", profile), } } } @@ -774,24 +870,97 @@ impl IBluetoothMedia for BluetoothMedia { for profile in connected_profiles { match profile { uuid::Profile::A2dpSink => { + metrics::profile_connection_state_changed( + addr, + Profile::A2dpSink as u32, + BtStatus::Success, + BtavConnectionState::Disconnecting as u32, + ); match self.a2dp.as_mut() { - Some(a2dp) => a2dp.disconnect(addr), - None => warn!("Uninitialized A2DP to disconnect {}", address), + Some(a2dp) => { + let status: BtStatus = a2dp.disconnect(addr); + if BtStatus::Success != status { + metrics::profile_connection_state_changed( + addr, + Profile::A2dpSource as u32, + status, + BtavConnectionState::Disconnected as u32, + ); + } + } + None => { + warn!("Uninitialized A2DP to disconnect {}", address); + metrics::profile_connection_state_changed( + addr, + Profile::A2dpSource as u32, + BtStatus::NotReady, + BtavConnectionState::Disconnected as u32, + ); + } }; } uuid::Profile::Hfp => { + metrics::profile_connection_state_changed( + addr, + Profile::Hfp as u32, + BtStatus::Success, + BthfConnectionState::Disconnecting as u32, + ); match self.hfp.as_mut() { - Some(hfp) => hfp.disconnect(addr), - None => warn!("Uninitialized HFP to disconnect {}", address), + Some(hfp) => { + let status: BtStatus = hfp.disconnect(addr); + if BtStatus::Success != status { + metrics::profile_connection_state_changed( + addr, + Profile::Hfp as u32, + status, + BthfConnectionState::Disconnected as u32, + ); + } + } + None => { + warn!("Uninitialized HFP to disconnect {}", address); + metrics::profile_connection_state_changed( + addr, + Profile::Hfp as u32, + BtStatus::NotReady, + BthfConnectionState::Disconnected as u32, + ); + } }; } uuid::Profile::AvrcpController => { + metrics::profile_connection_state_changed( + addr, + Profile::AvrcpController as u32, + BtStatus::Success, + BtavConnectionState::Disconnecting as u32, + ); match self.avrcp.as_mut() { - Some(avrcp) => avrcp.disconnect(addr), - None => warn!("Uninitialized AVRCP to disconnect {}", address), + Some(avrcp) => { + let status: BtStatus = avrcp.disconnect(addr); + if BtStatus::Success != status { + metrics::profile_connection_state_changed( + addr, + Profile::AvrcpController as u32, + status, + BtavConnectionState::Disconnected as u32, + ); + } + } + + None => { + warn!("Uninitialized AVRCP to disconnect {}", address); + metrics::profile_connection_state_changed( + addr, + Profile::AvrcpController as u32, + BtStatus::NotReady, + BtavConnectionState::Disconnected as u32, + ); + } }; } - _ => warn!("Unknown profile."), + _ => warn!("Unknown profile: {:?}", profile), } } } diff --git a/system/gd/rust/linux/stack/src/lib.rs b/system/gd/rust/linux/stack/src/lib.rs index 31b3975ef1..606eb99206 100644 --- a/system/gd/rust/linux/stack/src/lib.rs +++ b/system/gd/rust/linux/stack/src/lib.rs @@ -8,6 +8,7 @@ extern crate num_derive; pub mod battery_manager; pub mod battery_provider_manager; +pub mod battery_service; pub mod bluetooth; pub mod bluetooth_adv; pub mod bluetooth_gatt; @@ -22,7 +23,9 @@ use std::sync::{Arc, Mutex}; use tokio::sync::mpsc::channel; use tokio::sync::mpsc::{Receiver, Sender}; -use crate::bluetooth::Bluetooth; +use crate::battery_manager::BatteryManager; +use crate::battery_service::{BatteryService, GattBatteryCallbacks}; +use crate::bluetooth::{Bluetooth, IBluetooth}; use crate::bluetooth_gatt::BluetoothGatt; use crate::bluetooth_media::{BluetoothMedia, MediaActions}; use crate::socket_manager::{BluetoothSocketManager, SocketActions}; @@ -39,6 +42,9 @@ use bt_topshim::{ /// Message types that are sent to the stack main dispatch loop. pub enum Message { + // Shuts down the stack. + Shutdown, + // Callbacks from libbluetooth A2dp(A2dpCallbacks), Avrcp(AvrcpCallbacks), @@ -77,6 +83,11 @@ pub enum Message { SocketManagerActions(SocketActions), SocketManagerCallbackDisconnected(u32), + // Battery related + BatteryServiceCallbackDisconnected(u32), + BatteryServiceCallbacks(GattBatteryCallbacks), + BatteryManagerCallbackDisconnected(u32), + GattClientCallbackDisconnected(u32), } @@ -106,6 +117,8 @@ impl Stack { mut rx: Receiver<Message>, bluetooth: Arc<Mutex<Box<Bluetooth>>>, bluetooth_gatt: Arc<Mutex<Box<BluetoothGatt>>>, + battery_service: Arc<Mutex<Box<BatteryService>>>, + battery_manager: Arc<Mutex<Box<BatteryManager>>>, bluetooth_media: Arc<Mutex<Box<BluetoothMedia>>>, suspend: Arc<Mutex<Box<Suspend>>>, bluetooth_socketmgr: Arc<Mutex<Box<BluetoothSocketManager>>>, @@ -119,6 +132,10 @@ impl Stack { } match m.unwrap() { + Message::Shutdown => { + bluetooth.lock().unwrap().disable(); + } + Message::A2dp(a) => { bluetooth_media.lock().unwrap().dispatch_a2dp_callbacks(a); } @@ -210,6 +227,15 @@ impl Stack { Message::SocketManagerCallbackDisconnected(id) => { bluetooth_socketmgr.lock().unwrap().remove_callback(id); } + Message::BatteryServiceCallbackDisconnected(id) => { + battery_service.lock().unwrap().remove_callback(id); + } + Message::BatteryServiceCallbacks(callback) => { + battery_service.lock().unwrap().handle_callback(callback); + } + Message::BatteryManagerCallbackDisconnected(id) => { + battery_manager.lock().unwrap().remove_callback(id); + } Message::GattClientCallbackDisconnected(id) => { bluetooth_gatt.lock().unwrap().remove_client_callback(id); } diff --git a/system/gd/rust/linux/stack/src/uuid.rs b/system/gd/rust/linux/stack/src/uuid.rs index 6068405e1f..5c99d5292f 100644 --- a/system/gd/rust/linux/stack/src/uuid.rs +++ b/system/gd/rust/linux/stack/src/uuid.rs @@ -9,6 +9,7 @@ use bt_topshim::btif::{Uuid, Uuid128Bit}; pub const A2DP_SINK: &str = "0000110B-0000-1000-8000-00805F9B34FB"; pub const A2DP_SOURCE: &str = "0000110A-0000-1000-8000-00805F9B34FB"; pub const ADV_AUDIO_DIST: &str = "0000110D-0000-1000-8000-00805F9B34FB"; +pub const BAS: &str = "0000180F-0000-1000-8000-00805F9B34FB"; pub const HSP: &str = "00001108-0000-1000-8000-00805F9B34FB"; pub const HSP_AG: &str = "00001112-0000-1000-8000-00805F9B34FB"; pub const HFP: &str = "0000111E-0000-1000-8000-00805F9B34FB"; @@ -43,6 +44,7 @@ pub enum Profile { A2dpSink, A2dpSource, AdvAudioDist, + Bas, Hsp, HspAg, Hfp, @@ -219,6 +221,11 @@ impl UuidHelper { pub fn parse_uuid_string<T: Into<String>>(uuid: T) -> Option<Uuid> { let uuid = uuid.into(); + // Strip un-needed characters before parsing to handle the common + // case of including dashes in UUID strings. UUID expects only + // 0-9, a-f, A-F with no other characters. |is_digit| with radix + // 16 (hex) supports that exact behavior. + let uuid = uuid.chars().filter(|char| char.is_digit(16)).collect::<String>(); if uuid.len() != 32 { return None; } @@ -226,11 +233,7 @@ pub fn parse_uuid_string<T: Into<String>>(uuid: T) -> Option<Uuid> { let mut raw = [0; 16]; for i in 0..16 { - let byte = u8::from_str_radix(&uuid[i * 2..i * 2 + 2], 16); - if byte.is_err() { - return None; - } - raw[i] = byte.unwrap(); + raw[i] = u8::from_str_radix(&uuid[i * 2..i * 2 + 2], 16).ok()?; } Some(Uuid { uu: raw }) diff --git a/system/gd/rust/linux/utils/src/adv_parser.rs b/system/gd/rust/linux/utils/src/adv_parser.rs index a319e71b2f..8f89fc0cfe 100644 --- a/system/gd/rust/linux/utils/src/adv_parser.rs +++ b/system/gd/rust/linux/utils/src/adv_parser.rs @@ -7,9 +7,13 @@ use bt_topshim::btif::Uuid128Bit; // Advertising data types. const FLAGS: u8 = 0x01; +const COMPLETE_LIST_16_BIT_SERVICE_UUIDS: u8 = 0x03; +const COMPLETE_LIST_32_BIT_SERVICE_UUIDS: u8 = 0x05; const COMPLETE_LIST_128_BIT_SERVICE_UUIDS: u8 = 0x07; const SHORTENED_LOCAL_NAME: u8 = 0x08; const COMPLETE_LOCAL_NAME: u8 = 0x09; +const SERVICE_DATA_16_BIT_UUID: u8 = 0x16; +const SERVICE_DATA_32_BIT_UUID: u8 = 0x20; const SERVICE_DATA_128_BIT_UUID: u8 = 0x21; const MANUFACTURER_SPECIFIC_DATA: u8 = 0xff; @@ -52,9 +56,19 @@ pub fn extract_flags(bytes: &[u8]) -> u8 { // Helper function to extract service uuids (128bit) from advertising data pub fn extract_service_uuids(bytes: &[u8]) -> Vec<Uuid128Bit> { - iterate_adv_data(bytes, COMPLETE_LIST_128_BIT_SERVICE_UUIDS) - .flat_map(|slice| slice.chunks(16)) + iterate_adv_data(bytes, COMPLETE_LIST_16_BIT_SERVICE_UUIDS) + .flat_map(|slice| slice.chunks(2)) .filter_map(|chunk| Uuid::try_from(chunk.to_vec()).ok().map(|uuid| uuid.uu)) + .chain( + iterate_adv_data(bytes, COMPLETE_LIST_32_BIT_SERVICE_UUIDS) + .flat_map(|slice| slice.chunks(4)) + .filter_map(|chunk| Uuid::try_from(chunk.to_vec()).ok().map(|uuid| uuid.uu)), + ) + .chain( + iterate_adv_data(bytes, COMPLETE_LIST_128_BIT_SERVICE_UUIDS) + .flat_map(|slice| slice.chunks(16)) + .filter_map(|chunk| Uuid::try_from(chunk.to_vec()).ok().map(|uuid| uuid.uu)), + ) .collect() } @@ -68,12 +82,22 @@ pub fn extract_name(bytes: &[u8]) -> String { // Helper function to extract service data from advertising data pub fn extract_service_data(bytes: &[u8]) -> HashMap<String, Vec<u8>> { - iterate_adv_data(bytes, SERVICE_DATA_128_BIT_UUID) + iterate_adv_data(bytes, SERVICE_DATA_16_BIT_UUID) .filter_map(|slice| { + Uuid::try_from(slice.get(0..2)?.to_vec()) + .ok() + .map(|uuid| (uuid.to_string(), slice[2..].to_vec())) + }) + .chain(iterate_adv_data(bytes, SERVICE_DATA_32_BIT_UUID).filter_map(|slice| { + Uuid::try_from(slice.get(0..4)?.to_vec()) + .ok() + .map(|uuid| (uuid.to_string(), slice[4..].to_vec())) + })) + .chain(iterate_adv_data(bytes, SERVICE_DATA_128_BIT_UUID).filter_map(|slice| { Uuid::try_from(slice.get(0..16)?.to_vec()) .ok() .map(|uuid| (uuid.to_string(), slice[16..].to_vec())) - }) + })) .collect() } @@ -129,6 +153,16 @@ mod tests { 2, FLAGS, 3, + 3, + COMPLETE_LIST_16_BIT_SERVICE_UUIDS, + 0xFE, + 0x2C, + 5, + COMPLETE_LIST_32_BIT_SERVICE_UUIDS, + 2, + 3, + 4, + 5, 17, COMPLETE_LIST_128_BIT_SERVICE_UUIDS, 0, @@ -149,9 +183,27 @@ mod tests { 15, ]; let uuids = extract_service_uuids(payload.as_slice()); - assert_eq!(uuids.len(), 1); + assert_eq!(uuids.len(), 3); assert_eq!( uuids[0], + Uuid::try_from(vec![ + 0x0, 0x0, 0xFE, 0x2C, 0x0, 0x0, 0x10, 0x0, 0x80, 0x0, 0x0, 0x80, 0x5f, 0x9b, 0x34, + 0xfb + ]) + .unwrap() + .uu + ); + assert_eq!( + uuids[1], + Uuid::try_from(vec![ + 0x2, 0x3, 0x4, 0x5, 0x0, 0x0, 0x10, 0x0, 0x80, 0x0, 0x0, 0x80, 0x5f, 0x9b, 0x34, + 0xfb + ]) + .unwrap() + .uu + ); + assert_eq!( + uuids[2], Uuid::try_from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]).unwrap().uu ); } @@ -178,6 +230,18 @@ mod tests { assert_eq!(service_data.len(), 0); let payload: Vec<u8> = vec![ + 4, + SERVICE_DATA_16_BIT_UUID, + 0xFE, + 0x2C, + 0xFF, + 6, + SERVICE_DATA_32_BIT_UUID, + 2, + 3, + 4, + 5, + 0xFE, 18, SERVICE_DATA_128_BIT_UUID, 0, @@ -217,7 +281,19 @@ mod tests { 16, ]; let service_data = extract_service_data(payload.as_slice()); - assert_eq!(service_data.len(), 2); + assert_eq!(service_data.len(), 4); + let expected_uuid = Uuid::try_from(vec![ + 0x0, 0x0, 0xFE, 0x2C, 0x0, 0x0, 0x10, 0x0, 0x80, 0x0, 0x0, 0x80, 0x5f, 0x9b, 0x34, 0xfb, + ]) + .unwrap() + .to_string(); + assert_eq!(service_data.get(&expected_uuid), Some(&vec![0xFF])); + let expected_uuid = Uuid::try_from(vec![ + 0x2, 0x3, 0x4, 0x5, 0x0, 0x0, 0x10, 0x0, 0x80, 0x0, 0x0, 0x80, 0x5f, 0x9b, 0x34, 0xfb, + ]) + .unwrap() + .to_string(); + assert_eq!(service_data.get(&expected_uuid), Some(&vec![0xFE])); let expected_uuid = Uuid::try_from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) .unwrap() diff --git a/system/gd/rust/packets/build.rs b/system/gd/rust/packets/build.rs index be5d804b78..030fd67fa3 100644 --- a/system/gd/rust/packets/build.rs +++ b/system/gd/rust/packets/build.rs @@ -61,6 +61,7 @@ fn generate_packets() { } for i in 0..input_files.len() { + println!("cargo:rerun-if-changed={}", input_files[i].display()); let output = Command::new(packetgen.as_os_str().to_str().unwrap()) .arg("--source_root=".to_owned() + gd_root.as_os_str().to_str().unwrap()) .arg("--out=".to_owned() + out_dir.as_os_str().to_str().unwrap()) diff --git a/system/gd/rust/shim/src/init_flags.rs b/system/gd/rust/shim/src/init_flags.rs index fd3015cc34..57152c5775 100644 --- a/system/gd/rust/shim/src/init_flags.rs +++ b/system/gd/rust/shim/src/init_flags.rs @@ -4,10 +4,12 @@ mod ffi { fn load(flags: Vec<String>); fn set_all_for_testing(); + fn sdp_serialization_is_enabled() -> bool; fn gd_core_is_enabled() -> bool; fn gd_security_is_enabled() -> bool; fn gd_l2cap_is_enabled() -> bool; - fn gatt_robust_caching_is_enabled() -> bool; + fn gatt_robust_caching_client_is_enabled() -> bool; + fn gatt_robust_caching_server_is_enabled() -> bool; fn btaa_hci_is_enabled() -> bool; fn gd_rust_is_enabled() -> bool; fn gd_link_policy_is_enabled() -> bool; diff --git a/system/gd/rust/topshim/btav/btav_shim.cc b/system/gd/rust/topshim/btav/btav_shim.cc index 7e4560ec22..157843dc10 100644 --- a/system/gd/rust/topshim/btav/btav_shim.cc +++ b/system/gd/rust/topshim/btav/btav_shim.cc @@ -152,10 +152,19 @@ static ::rust::Vec<A2dpCodecConfig> to_rust_codec_config_vec(const std::vector<b return rconfigs; } -static void connection_state_cb( - const RawAddress& bd_addr, btav_connection_state_t state, [[maybe_unused]] const btav_error_t& error) { +static A2dpError to_rust_error(const btav_error_t& error) { + A2dpError a2dp_error = { + .status = error.status, + .error_code = error.error_code, + .error_msg = error.error_msg.value_or(""), + }; + return a2dp_error; +} + +static void connection_state_cb(const RawAddress& bd_addr, btav_connection_state_t state, const btav_error_t& error) { RustRawAddress addr = rusty::CopyToRustAddress(bd_addr); - rusty::connection_state_callback(addr, state); + A2dpError a2dp_error = to_rust_error(error); + rusty::connection_state_callback(addr, state, a2dp_error); } static void audio_state_cb(const RawAddress& bd_addr, btav_audio_state_t state) { RustRawAddress addr = rusty::CopyToRustAddress(bd_addr); @@ -208,11 +217,11 @@ int A2dpIntf::init() const { return intf_->init(&internal::g_callbacks, 1, a, b); } -int A2dpIntf::connect(RustRawAddress bt_addr) const { +uint32_t A2dpIntf::connect(RustRawAddress bt_addr) const { RawAddress addr = rusty::CopyFromRustAddress(bt_addr); return intf_->connect(addr); } -int A2dpIntf::disconnect(RustRawAddress bt_addr) const { +uint32_t A2dpIntf::disconnect(RustRawAddress bt_addr) const { RawAddress addr = rusty::CopyFromRustAddress(bt_addr); return intf_->disconnect(addr); } @@ -286,11 +295,11 @@ void AvrcpIntf::cleanup() { intf_->Cleanup(); } -int AvrcpIntf::connect(RustRawAddress bt_addr) { +uint32_t AvrcpIntf::connect(RustRawAddress bt_addr) { RawAddress addr = rusty::CopyFromRustAddress(bt_addr); return intf_->ConnectDevice(addr); } -int AvrcpIntf::disconnect(RustRawAddress bt_addr) { +uint32_t AvrcpIntf::disconnect(RustRawAddress bt_addr) { RawAddress addr = rusty::CopyFromRustAddress(bt_addr); return intf_->DisconnectDevice(addr); } diff --git a/system/gd/rust/topshim/btav/btav_shim.h b/system/gd/rust/topshim/btav/btav_shim.h index 1cabd9c15f..e6c1ce2096 100644 --- a/system/gd/rust/topshim/btav/btav_shim.h +++ b/system/gd/rust/topshim/btav/btav_shim.h @@ -39,8 +39,8 @@ class A2dpIntf { // interface for Settings int init() const; - int connect(RustRawAddress bt_addr) const; - int disconnect(RustRawAddress bt_addr) const; + uint32_t connect(RustRawAddress bt_addr) const; + uint32_t disconnect(RustRawAddress bt_addr) const; int set_silence_device(RustRawAddress bt_addr, bool silent) const; int set_active_device(RustRawAddress bt_addr) const; int config_codec(RustRawAddress bt_addr, ::rust::Vec<A2dpCodecConfig> codec_preferences) const; @@ -65,8 +65,8 @@ class AvrcpIntf { void init(); void cleanup(); - int connect(RustRawAddress bt_addr); - int disconnect(RustRawAddress bt_addr); + uint32_t connect(RustRawAddress bt_addr); + uint32_t disconnect(RustRawAddress bt_addr); // interface for Audio server void set_volume(int8_t volume); diff --git a/system/gd/rust/topshim/facade/src/adapter_service.rs b/system/gd/rust/topshim/facade/src/adapter_service.rs index 06123508db..11c5d5585c 100644 --- a/system/gd/rust/topshim/facade/src/adapter_service.rs +++ b/system/gd/rust/topshim/facade/src/adapter_service.rs @@ -1,11 +1,11 @@ //! Adapter service facade use bt_topshim::btif; -use bt_topshim::btif::{BaseCallbacks, BaseCallbacksDispatcher, BluetoothInterface, RawAddress}; +use bt_topshim::btif::{BaseCallbacks, BaseCallbacksDispatcher, BluetoothInterface}; use bt_topshim_facade_protobuf::empty::Empty; use bt_topshim_facade_protobuf::facade::{ - EventType, FetchEventsRequest, FetchEventsResponse, RemoveBondRequest, SetDiscoveryModeRequest, + EventType, FetchEventsRequest, FetchEventsResponse, SetDiscoveryModeRequest, SetDiscoveryModeResponse, ToggleStackRequest, ToggleStackResponse, }; use bt_topshim_facade_protobuf::facade_grpc::{create_adapter_service, AdapterService}; @@ -173,14 +173,6 @@ impl AdapterService for AdapterServiceImpl { }) } - fn remove_bond(&mut self, ctx: RpcContext<'_>, req: RemoveBondRequest, sink: UnarySink<Empty>) { - let raw_address = RawAddress::from_string(req.address).unwrap(); - self.btif_intf.lock().unwrap().remove_bond(&raw_address); - ctx.spawn(async move { - sink.success(Empty::default()).await.unwrap(); - }) - } - fn restore_filter_accept_list( &mut self, ctx: RpcContext<'_>, diff --git a/system/gd/rust/topshim/facade/src/main.rs b/system/gd/rust/topshim/facade/src/main.rs index 7610a46653..e971893f8c 100644 --- a/system/gd/rust/topshim/facade/src/main.rs +++ b/system/gd/rust/topshim/facade/src/main.rs @@ -21,6 +21,7 @@ use tokio::runtime::Runtime; mod adapter_service; mod gatt_service; mod media_service; +mod security_service; // This is needed for linking, libbt_shim_bridge needs symbols defined by // bt_shim, however bt_shim depends on rust crates (future, tokio) that @@ -79,6 +80,9 @@ async fn async_main(rt: Arc<Runtime>, mut sigint: mpsc::UnboundedReceiver<()>) { let adapter_service_impl = adapter_service::AdapterServiceImpl::create(rt.clone(), btif_intf.clone()); + let security_service_impl = + security_service::SecurityServiceImpl::create(rt.clone(), btif_intf.clone()); + let gatt_service_impl = gatt_service::GattServiceImpl::create(rt.clone(), btif_intf.clone()); let media_service_impl = media_service::MediaServiceImpl::create(rt.clone(), btif_intf.clone()); @@ -91,6 +95,7 @@ async fn async_main(rt: Arc<Runtime>, mut sigint: mpsc::UnboundedReceiver<()>) { let mut server = ServerBuilder::new(env) .register_service(adapter_service_impl) + .register_service(security_service_impl) .register_service(gatt_service_impl) .register_service(media_service_impl) .bind("0.0.0.0", grpc_port) diff --git a/system/gd/rust/topshim/facade/src/security_service.rs b/system/gd/rust/topshim/facade/src/security_service.rs new file mode 100644 index 0000000000..1e59658c94 --- /dev/null +++ b/system/gd/rust/topshim/facade/src/security_service.rs @@ -0,0 +1,41 @@ +//! Security service facade + +use bt_topshim::btif::BluetoothInterface; + +use bt_topshim_facade_protobuf::empty::Empty; +use bt_topshim_facade_protobuf::facade::RemoveBondRequest; +use bt_topshim_facade_protobuf::facade_grpc::{create_security_service, SecurityService}; +use grpcio::*; + +use std::sync::{Arc, Mutex}; +use tokio::runtime::Runtime; + +/// Main object for Adapter facade service +#[derive(Clone)] +pub struct SecurityServiceImpl { + #[allow(dead_code)] + rt: Arc<Runtime>, + #[allow(dead_code)] + btif_intf: Arc<Mutex<BluetoothInterface>>, +} + +#[allow(dead_code)] +impl SecurityServiceImpl { + /// Create a new instance of the root facade service + pub fn create(rt: Arc<Runtime>, btif_intf: Arc<Mutex<BluetoothInterface>>) -> grpcio::Service { + create_security_service(Self { rt, btif_intf }) + } +} + +impl SecurityService for SecurityServiceImpl { + fn remove_bond( + &mut self, + ctx: RpcContext<'_>, + _req: RemoveBondRequest, + sink: UnarySink<Empty>, + ) { + ctx.spawn(async move { + sink.success(Empty::default()).await.unwrap(); + }) + } +} diff --git a/system/gd/rust/topshim/hfp/hfp_shim.cc b/system/gd/rust/topshim/hfp/hfp_shim.cc index 1d180356b6..9e124ad902 100644 --- a/system/gd/rust/topshim/hfp/hfp_shim.cc +++ b/system/gd/rust/topshim/hfp/hfp_shim.cc @@ -210,7 +210,7 @@ int HfpIntf::init() { return intf_->Init(DBusHeadsetCallbacks::GetInstance(intf_), 1, false); } -int HfpIntf::connect(RustRawAddress bt_addr) { +uint32_t HfpIntf::connect(RustRawAddress bt_addr) { RawAddress addr = rusty::CopyFromRustAddress(bt_addr); return intf_->Connect(&addr); } @@ -231,7 +231,7 @@ int HfpIntf::set_volume(int8_t volume, RustRawAddress bt_addr) { return intf_->VolumeControl(headset::bthf_volume_type_t::BTHF_VOLUME_TYPE_SPK, volume, &addr); } -int HfpIntf::disconnect(RustRawAddress bt_addr) { +uint32_t HfpIntf::disconnect(RustRawAddress bt_addr) { RawAddress addr = rusty::CopyFromRustAddress(bt_addr); return intf_->Disconnect(&addr); } diff --git a/system/gd/rust/topshim/hfp/hfp_shim.h b/system/gd/rust/topshim/hfp/hfp_shim.h index 43d8b99b1d..99d78cd33f 100644 --- a/system/gd/rust/topshim/hfp/hfp_shim.h +++ b/system/gd/rust/topshim/hfp/hfp_shim.h @@ -33,11 +33,11 @@ class HfpIntf { HfpIntf(headset::Interface* intf) : intf_(intf){}; int init(); - int connect(RustRawAddress bt_addr); + uint32_t connect(RustRawAddress bt_addr); int connect_audio(RustRawAddress bt_addr, bool sco_offload, bool force_cvsd); int set_active_device(RustRawAddress bt_addr); int set_volume(int8_t volume, RustRawAddress bt_addr); - int disconnect(RustRawAddress bt_addr); + uint32_t disconnect(RustRawAddress bt_addr); int disconnect_audio(RustRawAddress bt_addr); void cleanup(); diff --git a/system/gd/rust/topshim/src/btif.rs b/system/gd/rust/topshim/src/btif.rs index 850f0005b0..92c59f14bd 100644 --- a/system/gd/rust/topshim/src/btif.rs +++ b/system/gd/rust/topshim/src/btif.rs @@ -321,12 +321,25 @@ impl TryFrom<Vec<u8>> for Uuid { type Error = &'static str; fn try_from(value: Vec<u8>) -> std::result::Result<Self, Self::Error> { - if value.len() != 16 { - Err("Vector size must be exactly 16.") - } else { - let mut uu: [u8; 16] = Default::default(); - uu.copy_from_slice(&value[0..16]); - Ok(Uuid { uu }) + // base UUID defined in the Bluetooth specification + let mut uu: [u8; 16] = + [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x80, 0x0, 0x0, 0x80, 0x5f, 0x9b, 0x34, 0xfb]; + match value.len() { + 2 => { + uu[2..4].copy_from_slice(&value[0..2]); + Ok(Uuid { uu }) + } + 4 => { + uu[0..4].copy_from_slice(&value[0..4]); + Ok(Uuid { uu }) + } + 16 => { + uu.copy_from_slice(&value[0..16]); + Ok(Uuid { uu }) + } + _ => { + Err("Vector size must be exactly 2 (16 bit UUID), 4 (32 bit UUID), or 16 (128 bit UUID).") + } } } } diff --git a/system/gd/rust/topshim/src/profiles/a2dp.rs b/system/gd/rust/topshim/src/profiles/a2dp.rs index aa69a3ec30..cd2593be07 100644 --- a/system/gd/rust/topshim/src/profiles/a2dp.rs +++ b/system/gd/rust/topshim/src/profiles/a2dp.rs @@ -1,11 +1,11 @@ -use crate::btif::{BluetoothInterface, RawAddress}; +use crate::btif::{BluetoothInterface, BtStatus, RawAddress}; use crate::topstack::get_dispatchers; use num_traits::cast::FromPrimitive; use std::sync::{Arc, Mutex}; use topshim_macros::cb_variant; -#[derive(Debug, FromPrimitive, ToPrimitive, PartialEq, PartialOrd)] +#[derive(Debug, FromPrimitive, ToPrimitive, PartialEq, PartialOrd, Clone)] #[repr(u32)] pub enum BtavConnectionState { Disconnected = 0, @@ -77,6 +77,18 @@ impl From<i32> for A2dpCodecPriority { } } +#[derive(Debug)] +pub struct A2dpError { + /// Standard BT status come from a function return or the cloest approximation to the real + /// error. + pub status: BtStatus, + /// An additional value to help explain the error. In the A2Dp context, this is often referring + /// to the BTA_AV_XXX status. + pub error: i32, + /// An optional error message that the lower layer wants to deliver. + pub error_message: Option<String>, +} + bitflags! { pub struct A2dpCodecSampleRate: i32 { const RATE_NONE = 0x0; @@ -154,6 +166,13 @@ pub mod ffi { data_position_nsec: i32, } + #[derive(Debug, Default)] + pub struct A2dpError { + status: u32, + error_code: u8, + error_msg: String, + } + unsafe extern "C++" { include!("btav/btav_shim.h"); include!("btav_sink/btav_sink_shim.h"); @@ -164,8 +183,8 @@ pub mod ffi { unsafe fn GetA2dpProfile(btif: *const u8) -> UniquePtr<A2dpIntf>; fn init(self: &A2dpIntf) -> i32; - fn connect(self: &A2dpIntf, bt_addr: RustRawAddress) -> i32; - fn disconnect(self: &A2dpIntf, bt_addr: RustRawAddress) -> i32; + fn connect(self: &A2dpIntf, bt_addr: RustRawAddress) -> u32; + fn disconnect(self: &A2dpIntf, bt_addr: RustRawAddress) -> u32; fn set_silence_device(self: &A2dpIntf, bt_addr: RustRawAddress, silent: bool) -> i32; fn set_active_device(self: &A2dpIntf, bt_addr: RustRawAddress) -> i32; fn config_codec( @@ -189,7 +208,7 @@ pub mod ffi { fn cleanup(self: &A2dpSinkIntf); } extern "Rust" { - fn connection_state_callback(addr: RustRawAddress, state: u32); + fn connection_state_callback(addr: RustRawAddress, state: u32, error: A2dpError); fn audio_state_callback(addr: RustRawAddress, state: u32); fn audio_config_callback( addr: RustRawAddress, @@ -204,6 +223,7 @@ pub mod ffi { pub type FfiAddress = ffi::RustRawAddress; pub type A2dpCodecConfig = ffi::A2dpCodecConfig; pub type PresentationPosition = ffi::RustPresentationPosition; +pub type FfiA2dpError = ffi::A2dpError; impl From<RawAddress> for FfiAddress { fn from(addr: RawAddress) -> Self { @@ -233,9 +253,18 @@ impl Default for A2dpCodecConfig { } } +impl Into<A2dpError> for FfiA2dpError { + fn into(self) -> A2dpError { + A2dpError { + status: self.status.into(), + error: self.error_code as i32, + error_message: if self.error_msg == "" { None } else { Some(self.error_msg) }, + } + } +} #[derive(Debug)] pub enum A2dpCallbacks { - ConnectionState(RawAddress, BtavConnectionState), + ConnectionState(RawAddress, BtavConnectionState, A2dpError), AudioState(RawAddress, BtavAudioState), AudioConfig(RawAddress, A2dpCodecConfig, Vec<A2dpCodecConfig>, Vec<A2dpCodecConfig>), MandatoryCodecPreferred(RawAddress), @@ -248,8 +277,9 @@ pub struct A2dpCallbacksDispatcher { type A2dpCb = Arc<Mutex<A2dpCallbacksDispatcher>>; cb_variant!(A2dpCb, connection_state_callback -> A2dpCallbacks::ConnectionState, -FfiAddress -> RawAddress, u32 -> BtavConnectionState, { +FfiAddress -> RawAddress, u32 -> BtavConnectionState, FfiA2dpError -> A2dpError,{ let _0 = _0.into(); + let _2 = _2.into(); }); cb_variant!(A2dpCb, audio_state_callback -> A2dpCallbacks::AudioState, @@ -293,16 +323,16 @@ impl A2dp { true } - pub fn connect(&mut self, addr: RawAddress) { - self.internal.connect(addr.into()); + pub fn connect(&mut self, addr: RawAddress) -> BtStatus { + BtStatus::from(self.internal.connect(addr.into())) } pub fn set_active_device(&mut self, addr: RawAddress) { self.internal.set_active_device(addr.into()); } - pub fn disconnect(&mut self, addr: RawAddress) { - self.internal.disconnect(addr.into()); + pub fn disconnect(&mut self, addr: RawAddress) -> BtStatus { + BtStatus::from(self.internal.disconnect(addr.into())) } pub fn set_audio_config(&self, sample_rate: i32, bits_per_sample: i32, channel_mode: i32) { diff --git a/system/gd/rust/topshim/src/profiles/avrcp.rs b/system/gd/rust/topshim/src/profiles/avrcp.rs index 7e1bfcccf1..2a68ae2eea 100644 --- a/system/gd/rust/topshim/src/profiles/avrcp.rs +++ b/system/gd/rust/topshim/src/profiles/avrcp.rs @@ -1,4 +1,4 @@ -use crate::btif::{BluetoothInterface, RawAddress}; +use crate::btif::{BluetoothInterface, BtStatus, RawAddress}; use crate::topstack::get_dispatchers; use std::sync::{Arc, Mutex}; @@ -20,8 +20,8 @@ pub mod ffi { fn init(self: Pin<&mut AvrcpIntf>); fn cleanup(self: Pin<&mut AvrcpIntf>); - fn connect(self: Pin<&mut AvrcpIntf>, bt_addr: RustRawAddress) -> i32; - fn disconnect(self: Pin<&mut AvrcpIntf>, bt_addr: RustRawAddress) -> i32; + fn connect(self: Pin<&mut AvrcpIntf>, bt_addr: RustRawAddress) -> u32; + fn disconnect(self: Pin<&mut AvrcpIntf>, bt_addr: RustRawAddress) -> u32; fn set_volume(self: Pin<&mut AvrcpIntf>, volume: i8); } @@ -135,12 +135,12 @@ impl Avrcp { true } - pub fn connect(&mut self, addr: RawAddress) { - self.internal.pin_mut().connect(addr.into()); + pub fn connect(&mut self, addr: RawAddress) -> BtStatus { + BtStatus::from(self.internal.pin_mut().connect(addr.into())) } - pub fn disconnect(&mut self, addr: RawAddress) { - self.internal.pin_mut().disconnect(addr.into()); + pub fn disconnect(&mut self, addr: RawAddress) -> BtStatus { + BtStatus::from(self.internal.pin_mut().disconnect(addr.into())) } pub fn set_volume(&mut self, volume: i8) { diff --git a/system/gd/rust/topshim/src/profiles/hfp.rs b/system/gd/rust/topshim/src/profiles/hfp.rs index 54cc7c73b9..5dbf260286 100644 --- a/system/gd/rust/topshim/src/profiles/hfp.rs +++ b/system/gd/rust/topshim/src/profiles/hfp.rs @@ -1,4 +1,4 @@ -use crate::btif::{BluetoothInterface, RawAddress}; +use crate::btif::{BluetoothInterface, BtStatus, RawAddress}; use crate::topstack::get_dispatchers; use num_traits::cast::FromPrimitive; @@ -6,7 +6,7 @@ use std::convert::{TryFrom, TryInto}; use std::sync::{Arc, Mutex}; use topshim_macros::cb_variant; -#[derive(Debug, FromPrimitive, ToPrimitive, PartialEq, PartialOrd)] +#[derive(Debug, FromPrimitive, ToPrimitive, PartialEq, PartialOrd, Clone)] #[repr(u32)] pub enum BthfConnectionState { Disconnected = 0, @@ -75,7 +75,7 @@ pub mod ffi { unsafe fn GetHfpProfile(btif: *const u8) -> UniquePtr<HfpIntf>; fn init(self: Pin<&mut HfpIntf>) -> i32; - fn connect(self: Pin<&mut HfpIntf>, bt_addr: RustRawAddress) -> i32; + fn connect(self: Pin<&mut HfpIntf>, bt_addr: RustRawAddress) -> u32; fn connect_audio( self: Pin<&mut HfpIntf>, bt_addr: RustRawAddress, @@ -84,7 +84,7 @@ pub mod ffi { ) -> i32; fn set_active_device(self: Pin<&mut HfpIntf>, bt_addr: RustRawAddress) -> i32; fn set_volume(self: Pin<&mut HfpIntf>, volume: i8, bt_addr: RustRawAddress) -> i32; - fn disconnect(self: Pin<&mut HfpIntf>, bt_addr: RustRawAddress) -> i32; + fn disconnect(self: Pin<&mut HfpIntf>, bt_addr: RustRawAddress) -> u32; fn disconnect_audio(self: Pin<&mut HfpIntf>, bt_addr: RustRawAddress) -> i32; fn cleanup(self: Pin<&mut HfpIntf>); @@ -181,8 +181,8 @@ impl Hfp { true } - pub fn connect(&mut self, addr: RawAddress) { - self.internal.pin_mut().connect(addr.into()); + pub fn connect(&mut self, addr: RawAddress) -> BtStatus { + BtStatus::from(self.internal.pin_mut().connect(addr.into())) } pub fn connect_audio(&mut self, addr: RawAddress, sco_offload: bool, force_cvsd: bool) -> i32 { @@ -197,8 +197,8 @@ impl Hfp { self.internal.pin_mut().set_volume(volume, addr.into()) } - pub fn disconnect(&mut self, addr: RawAddress) { - self.internal.pin_mut().disconnect(addr.into()); + pub fn disconnect(&mut self, addr: RawAddress) -> BtStatus { + BtStatus::from(self.internal.pin_mut().disconnect(addr.into())) } pub fn disconnect_audio(&mut self, addr: RawAddress) -> i32 { diff --git a/system/gd/rust/topshim/src/profiles/socket.rs b/system/gd/rust/topshim/src/profiles/socket.rs index a8898743a1..9ead5f5beb 100644 --- a/system/gd/rust/topshim/src/profiles/socket.rs +++ b/system/gd/rust/topshim/src/profiles/socket.rs @@ -162,7 +162,7 @@ impl BtSocket { }; let uuid_ptr = match uuid { - Some(u) => &u as *const Uuid, + Some(ref u) => u as *const Uuid, None => std::ptr::null(), }; @@ -199,7 +199,7 @@ impl BtSocket { }; let uuid_ptr = match uuid { - Some(u) => &u as *const Uuid, + Some(ref u) => u as *const Uuid, None => std::ptr::null(), }; diff --git a/system/gd/security/pairing_handler_le.h b/system/gd/security/pairing_handler_le.h index 051ce34c8e..ed29316250 100644 --- a/system/gd/security/pairing_handler_le.h +++ b/system/gd/security/pairing_handler_le.h @@ -318,7 +318,8 @@ class PairingHandlerLe { std::optional<PairingEvent> WaitUiPairingAccept() { PairingEvent e = WaitForEvent(); - if (e.type == PairingEvent::UI & e.ui_action == PairingEvent::PAIRING_ACCEPTED) { + if (e.type == PairingEvent::UI && + e.ui_action == PairingEvent::PAIRING_ACCEPTED) { return e; } else { return std::nullopt; @@ -327,7 +328,8 @@ class PairingHandlerLe { std::optional<PairingEvent> WaitUiConfirmYesNo() { PairingEvent e = WaitForEvent(); - if (e.type == PairingEvent::UI & e.ui_action == PairingEvent::CONFIRM_YESNO) { + if (e.type == PairingEvent::UI && + e.ui_action == PairingEvent::CONFIRM_YESNO) { return e; } else { return std::nullopt; @@ -362,7 +364,8 @@ class PairingHandlerLe { e = WaitForEvent(); } - if (e.type == PairingEvent::UI & e.ui_action == PairingEvent::PASSKEY) { + if (e.type == PairingEvent::UI && + e.ui_action == PairingEvent::PASSKEY) { return e; } else { return std::nullopt; diff --git a/system/gd/storage/legacy_config_file.cc b/system/gd/storage/legacy_config_file.cc index 4a46dd5961..214184af71 100644 --- a/system/gd/storage/legacy_config_file.cc +++ b/system/gd/storage/legacy_config_file.cc @@ -46,6 +46,10 @@ std::optional<ConfigCache> LegacyConfigFile::Read(size_t temp_devices_capacity) while (std::getline(config_file, line)) { ++line_num; line = common::StringTrim(std::move(line)); + if (line.empty()) { + continue; + } + if (line.front() == '\0' || line.front() == '#') { continue; } diff --git a/system/include/hardware/bt_le_audio.h b/system/include/hardware/bt_le_audio.h index 34f8778772..978008b7d3 100644 --- a/system/include/hardware/bt_le_audio.h +++ b/system/include/hardware/bt_le_audio.h @@ -37,6 +37,7 @@ enum class ConnectionState { enum class GroupStatus { INACTIVE = 0, ACTIVE, + TURNED_IDLE_DURING_CALL, }; enum class GroupStreamStatus { diff --git a/system/internal_include/stack_config.h b/system/internal_include/stack_config.h index 080d40183a..efaec3f700 100644 --- a/system/internal_include/stack_config.h +++ b/system/internal_include/stack_config.h @@ -47,6 +47,7 @@ typedef struct { int (*get_pts_l2cap_ecoc_send_num_of_sdu)(void); bool (*get_pts_l2cap_ecoc_reconfigure)(void); const std::string* (*get_pts_broadcast_audio_config_options)(void); + bool (*get_pts_le_audio_disable_ases_before_stopping)(void); config_t* (*get_all)(void); } stack_config_t; diff --git a/system/main/shim/btm_api.cc b/system/main/shim/btm_api.cc index 26a006c07a..720e9d5e81 100644 --- a/system/main/shim/btm_api.cc +++ b/system/main/shim/btm_api.cc @@ -37,6 +37,7 @@ #include "main/shim/shim.h" #include "main/shim/stack.h" #include "osi/include/allocator.h" +#include "stack/btm/btm_ble_int.h" #include "stack/btm/btm_int_types.h" #include "stack/btm/btm_sec.h" #include "stack/include/bt_hdr.h" @@ -1392,3 +1393,8 @@ tBTM_STATUS bluetooth::shim::BTM_SetEventFilterInquiryResultAllDevices() { controller_get_interface()->set_event_filter_inquiry_result_all_devices(); return BTM_SUCCESS; } + +tBTM_STATUS bluetooth::shim::BTM_BleResetId() { + btm_ble_reset_id(); + return BTM_SUCCESS; +} diff --git a/system/main/shim/btm_api.h b/system/main/shim/btm_api.h index 3c56f2687d..8883140332 100644 --- a/system/main/shim/btm_api.h +++ b/system/main/shim/btm_api.h @@ -1924,6 +1924,15 @@ tBTM_STATUS BTM_SetDefaultEventMask(void); *******************************************************************************/ tBTM_STATUS BTM_SetEventFilterInquiryResultAllDevices(void); +/******************************************************************************* + * + * Function BTM_BleResetId + * + * Description Resets the local BLE keys + * + *******************************************************************************/ +tBTM_STATUS BTM_BleResetId(void); + /** * Send remote name request to GD shim Name module */ diff --git a/system/main/shim/hci_layer.cc b/system/main/shim/hci_layer.cc index 4eff5bedee..a3edcdc6aa 100644 --- a/system/main/shim/hci_layer.cc +++ b/system/main/shim/hci_layer.cc @@ -335,7 +335,6 @@ void OnTransmitPacketCommandComplete(command_complete_cb complete_callback, bluetooth::hci::CommandCompleteView view) { LOG_DEBUG("Received cmd complete for %s", bluetooth::hci::OpCodeText(view.GetCommandOpCode()).c_str()); - std::vector<uint8_t> data(view.begin(), view.end()); BT_HDR* response = WrapPacketAndCopy(MSG_HC_TO_STACK_HCI_EVT, &view); complete_callback(response, context); } diff --git a/system/main/stack_config.cc b/system/main/stack_config.cc index 58994735c0..5c2921f1fb 100644 --- a/system/main/stack_config.cc +++ b/system/main/stack_config.cc @@ -51,6 +51,7 @@ const char* PTS_L2CAP_ECOC_SEND_NUM_OF_SDU = "PTS_L2capEcocSendNumOfSdu"; const char* PTS_L2CAP_ECOC_RECONFIGURE = "PTS_L2capEcocReconfigure"; const char* PTS_BROADCAST_AUDIO_CONFIG_OPTION = "PTS_BroadcastAudioConfigOption"; +const char* PTS_LE_AUDIO_SUSPEND_STREAMING = "PTS_LeAudioSuspendStreaming"; static std::unique_ptr<config_t> config; } // namespace @@ -202,6 +203,11 @@ static const std::string* get_pts_broadcast_audio_config_options(void) { PTS_BROADCAST_AUDIO_CONFIG_OPTION, NULL); } +static bool get_pts_le_audio_disable_ases_before_stopping(void) { + return config_get_bool(*config, CONFIG_DEFAULT_SECTION, + PTS_LE_AUDIO_SUSPEND_STREAMING, false); +} + static config_t* get_all(void) { return config.get(); } const stack_config_t interface = { @@ -226,6 +232,7 @@ const stack_config_t interface = { get_pts_l2cap_ecoc_send_num_of_sdu, get_pts_l2cap_ecoc_reconfigure, get_pts_broadcast_audio_config_options, + get_pts_le_audio_disable_ases_before_stopping, get_all}; const stack_config_t* stack_config_get_interface(void) { return &interface; } diff --git a/system/profile/avrcp/tests/avrcp_device_fuzz/avrcp_device_fuzz.cc b/system/profile/avrcp/tests/avrcp_device_fuzz/avrcp_device_fuzz.cc index 42e9ba61ef..bca1f6f9ba 100644 --- a/system/profile/avrcp/tests/avrcp_device_fuzz/avrcp_device_fuzz.cc +++ b/system/profile/avrcp/tests/avrcp_device_fuzz/avrcp_device_fuzz.cc @@ -64,7 +64,8 @@ const stack_config_t interface = {nullptr, get_pts_avrcp_test, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - nullptr, nullptr}; + nullptr, nullptr, + nullptr}; void Callback(uint8_t, bool, std::unique_ptr<::bluetooth::PacketBuilder>) {} diff --git a/system/profile/avrcp/tests/avrcp_device_test.cc b/system/profile/avrcp/tests/avrcp_device_test.cc index f59330b287..e8058aa378 100644 --- a/system/profile/avrcp/tests/avrcp_device_test.cc +++ b/system/profile/avrcp/tests/avrcp_device_test.cc @@ -60,7 +60,8 @@ const stack_config_t interface = {nullptr, get_pts_avrcp_test, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - nullptr, nullptr}; + nullptr, nullptr, + nullptr}; // TODO (apanicke): All the tests below are just basic positive unit tests. // Add more tests to increase code coverage. diff --git a/system/stack/avrc/avrc_pars_ct.cc b/system/stack/avrc/avrc_pars_ct.cc index 12aee4ce69..a5710428f4 100644 --- a/system/stack/avrc/avrc_pars_ct.cc +++ b/system/stack/avrc/avrc_pars_ct.cc @@ -237,7 +237,7 @@ static tAVRC_STS avrc_pars_browse_rsp(tAVRC_MSG_BROWSE* p_msg, } BE_STREAM_TO_UINT8(pdu, p); uint16_t pkt_len; - int min_len = 0; + uint16_t min_len = 0; /* read the entire packet len */ BE_STREAM_TO_UINT16(pkt_len, p); @@ -380,8 +380,14 @@ static tAVRC_STS avrc_pars_browse_rsp(tAVRC_MSG_BROWSE* p_msg, /* Parse the name now */ BE_STREAM_TO_UINT16(attr_entry->name.charset_id, p); BE_STREAM_TO_UINT16(attr_entry->name.str_len, p); + if (static_cast<uint16_t>(min_len + attr_entry->name.str_len) < + min_len) { + // Check for overflow + android_errorWriteLog(0x534e4554, "205570663"); + } + if (pkt_len - min_len < attr_entry->name.str_len) + goto browse_length_error; min_len += attr_entry->name.str_len; - if (pkt_len < min_len) goto browse_length_error; attr_entry->name.p_str = (uint8_t*)osi_malloc( attr_entry->name.str_len * sizeof(uint8_t)); BE_STREAM_TO_ARRAY(p, attr_entry->name.p_str, @@ -444,8 +450,14 @@ static tAVRC_STS avrc_pars_browse_rsp(tAVRC_MSG_BROWSE* p_msg, BE_STREAM_TO_UINT32(attr_entry->attr_id, p); BE_STREAM_TO_UINT16(attr_entry->name.charset_id, p); BE_STREAM_TO_UINT16(attr_entry->name.str_len, p); + if (static_cast<uint16_t>(min_len + attr_entry->name.str_len) < + min_len) { + // Check for overflow + android_errorWriteLog(0x534e4554, "205570663"); + } + if (pkt_len - min_len < attr_entry->name.str_len) + goto browse_length_error; min_len += attr_entry->name.str_len; - if (pkt_len < min_len) goto browse_length_error; attr_entry->name.p_str = (uint8_t*)osi_malloc(attr_entry->name.str_len * sizeof(uint8_t)); BE_STREAM_TO_ARRAY(p, attr_entry->name.p_str, attr_entry->name.str_len); @@ -815,8 +827,12 @@ static tAVRC_STS avrc_ctrl_pars_vendor_rsp(tAVRC_MSG_VENDOR* p_msg, BE_STREAM_TO_UINT32(p_attrs[i].attr_id, p); BE_STREAM_TO_UINT16(p_attrs[i].name.charset_id, p); BE_STREAM_TO_UINT16(p_attrs[i].name.str_len, p); - min_len += p_attrs[i].name.str_len; - if (len < min_len) { + if (static_cast<uint16_t>(min_len + p_attrs[i].name.str_len) < + min_len) { + // Check for overflow + android_errorWriteLog(0x534e4554, "205570663"); + } + if (len - min_len < p_attrs[i].name.str_len) { for (int j = 0; j < i; j++) { osi_free(p_attrs[j].name.p_str); } @@ -824,6 +840,7 @@ static tAVRC_STS avrc_ctrl_pars_vendor_rsp(tAVRC_MSG_VENDOR* p_msg, p_result->get_attrs.num_attrs = 0; goto length_error; } + min_len += p_attrs[i].name.str_len; if (p_attrs[i].name.str_len > 0) { p_attrs[i].name.p_str = (uint8_t*)osi_calloc(p_attrs[i].name.str_len); diff --git a/system/stack/btm/btm_ble.cc b/system/stack/btm/btm_ble.cc index 5755ba2b94..57edb6190d 100644 --- a/system/stack/btm/btm_ble.cc +++ b/system/stack/btm/btm_ble.cc @@ -32,6 +32,7 @@ #include "main/shim/l2c_api.h" #include "main/shim/shim.h" #include "osi/include/allocator.h" +#include "osi/include/properties.h" #include "stack/btm/btm_dev.h" #include "stack/btm/btm_int_types.h" #include "stack/btm/security_device_record.h" @@ -56,6 +57,11 @@ extern bool btm_ble_init_pseudo_addr(tBTM_SEC_DEV_REC* p_dev_rec, extern void gatt_notify_phy_updated(tGATT_STATUS status, uint16_t handle, uint8_t tx_phy, uint8_t rx_phy); + +#ifndef PROPERTY_BLE_PRIVACY_ENABLED +#define PROPERTY_BLE_PRIVACY_ENABLED "bluetooth.core.gap.le.privacy.enabled" +#endif + /******************************************************************************/ /* External Function to be called by other modules */ /******************************************************************************/ @@ -82,7 +88,7 @@ void BTM_SecAddBleDevice(const RawAddress& bd_addr, tBT_DEVICE_TYPE dev_type, p_dev_rec->conn_params.peripheral_latency = BTM_BLE_CONN_PARAM_UNDEF; LOG_DEBUG("Device added, handle=0x%x, p_dev_rec=%p, bd_addr=%s", - p_dev_rec->ble_hci_handle, p_dev_rec, bd_addr.ToString().c_str()); + p_dev_rec->ble_hci_handle, p_dev_rec, PRIVATE_ADDRESS(bd_addr)); } memset(p_dev_rec->sec_bd_name, 0, sizeof(tBTM_BD_NAME)); @@ -2058,6 +2064,11 @@ static void btm_ble_reset_id_impl(const Octet16& rand1, const Octet16& rand2) { /* proceed generate ER */ btm_cb.devcb.ble_encryption_key_value = rand2; btm_notify_new_key(BTM_BLE_KEY_TYPE_ER); + + /* if privacy is enabled, update the irk and RPA in the LE address manager */ + if (btm_cb.ble_ctr_cb.privacy_mode != BTM_PRIVACY_NONE) { + BTM_BleConfigPrivacy(true); + } } struct reset_id_data { diff --git a/system/stack/btm/btm_ble_gap.cc b/system/stack/btm/btm_ble_gap.cc index 8266f632de..84341ad6a9 100644 --- a/system/stack/btm/btm_ble_gap.cc +++ b/system/stack/btm/btm_ble_gap.cc @@ -812,7 +812,7 @@ bool BTM_BleConfigPrivacy(bool privacy_mode) { GAP_BleAttrDBUpdate(GATT_UUID_GAP_CENTRAL_ADDR_RESOL, &gap_ble_attr_value); - bluetooth::shim::ACL_ConfigureLePrivacy(privacy_mode); + bluetooth::shim::ACL_ConfigureLePrivacy(privacy_mode); return true; } diff --git a/system/stack/btm/btm_iso.cc b/system/stack/btm/btm_iso.cc index 34bde1f6f1..ab92e7a8ce 100644 --- a/system/stack/btm/btm_iso.cc +++ b/system/stack/btm/btm_iso.cc @@ -71,8 +71,8 @@ void IsoManager::ReconfigureCig( pimpl_->iso_impl_->reconfigure_cig(cig_id, std::move(cig_params)); } -void IsoManager::RemoveCig(uint8_t cig_id) { - pimpl_->iso_impl_->remove_cig(cig_id); +void IsoManager::RemoveCig(uint8_t cig_id, bool force) { + pimpl_->iso_impl_->remove_cig(cig_id, force); } void IsoManager::EstablishCis( diff --git a/system/stack/btm/btm_iso_impl.h b/system/stack/btm/btm_iso_impl.h index 245a4ef696..358296e81e 100644 --- a/system/stack/btm/btm_iso_impl.h +++ b/system/stack/btm/btm_iso_impl.h @@ -201,8 +201,12 @@ struct iso_impl { cig_callbacks_->OnCigEvent(kIsoEventCigOnRemoveCmpl, &evt); } - void remove_cig(uint8_t cig_id) { - LOG_ASSERT(IsCigKnown(cig_id)) << "No such cig: " << +cig_id; + void remove_cig(uint8_t cig_id, bool force) { + if (!force) { + LOG_ASSERT(IsCigKnown(cig_id)) << "No such cig: " << +cig_id; + } else { + LOG_WARN("Forcing to remove CIG %d", cig_id); + } btsnd_hcic_remove_cig(cig_id, base::BindOnce(&iso_impl::on_remove_cig, base::Unretained(this))); diff --git a/system/stack/eatt/eatt.cc b/system/stack/eatt/eatt.cc index a023026309..8e88be96bc 100644 --- a/system/stack/eatt/eatt.cc +++ b/system/stack/eatt/eatt.cc @@ -175,7 +175,7 @@ bool EattExtension::IsOutstandingMsgInSendQueue(const RawAddress& bd_addr) { return pimpl_->eatt_impl_->is_outstanding_msg_in_send_queue(bd_addr); } -EattChannel* EattExtension::GetChannelWithQueuedData( +EattChannel* EattExtension::GetChannelWithQueuedDataToSend( const RawAddress& bd_addr) { return pimpl_->eatt_impl_->get_channel_with_queued_data(bd_addr); } diff --git a/system/stack/eatt/eatt.h b/system/stack/eatt/eatt.h index 828d17ac0e..40542b00ce 100644 --- a/system/stack/eatt/eatt.h +++ b/system/stack/eatt/eatt.h @@ -224,7 +224,8 @@ class EattExtension { * * @return pointer to EATT channel. */ - virtual EattChannel* GetChannelWithQueuedData(const RawAddress& bd_addr); + virtual EattChannel* GetChannelWithQueuedDataToSend( + const RawAddress& bd_addr); /** * Get EATT channel available to send GATT request. diff --git a/system/stack/eatt/eatt_impl.h b/system/stack/eatt/eatt_impl.h index a59afa999f..4c3a561552 100644 --- a/system/stack/eatt/eatt_impl.h +++ b/system/stack/eatt/eatt_impl.h @@ -706,7 +706,10 @@ struct eatt_impl { auto iter = find_if( eatt_dev->eatt_channels.begin(), eatt_dev->eatt_channels.end(), [](const std::pair<uint16_t, std::shared_ptr<EattChannel>>& el) { - return !el.second->cl_cmd_q_.empty(); + if (el.second->cl_cmd_q_.empty()) return false; + + tGATT_CMD_Q& cmd = el.second->cl_cmd_q_.front(); + return cmd.to_send; }); return (iter != eatt_dev->eatt_channels.end()); } @@ -718,7 +721,10 @@ struct eatt_impl { auto iter = find_if( eatt_dev->eatt_channels.begin(), eatt_dev->eatt_channels.end(), [](const std::pair<uint16_t, std::shared_ptr<EattChannel>>& el) { - return !el.second->cl_cmd_q_.empty(); + if (el.second->cl_cmd_q_.empty()) return false; + + tGATT_CMD_Q& cmd = el.second->cl_cmd_q_.front(); + return cmd.to_send; }); return (iter == eatt_dev->eatt_channels.end()) ? nullptr : iter->second.get(); diff --git a/system/stack/gatt/gatt_attr.cc b/system/stack/gatt/gatt_attr.cc index ed97e72214..2ea6e89b9e 100644 --- a/system/stack/gatt/gatt_attr.cc +++ b/system/stack/gatt/gatt_attr.cc @@ -79,6 +79,8 @@ static void gatt_cl_op_cmpl_cback(uint16_t conn_id, tGATTC_OPTYPE op, static void gatt_cl_start_config_ccc(tGATT_PROFILE_CLCB* p_clcb); +static bool gatt_cl_is_robust_caching_enabled(); + static bool gatt_sr_is_robust_caching_enabled(); static bool read_sr_supported_feat_req( @@ -430,7 +432,7 @@ void gatt_profile_db_init(void) { gatt_cb.gatt_svr_supported_feat_mask |= BLE_GATT_SVR_SUP_FEAT_EATT_BITMASK; gatt_cb.gatt_cl_supported_feat_mask |= BLE_GATT_CL_ANDROID_SUP_FEAT; - if (gatt_sr_is_robust_caching_enabled()) + if (gatt_cl_is_robust_caching_enabled()) gatt_cb.gatt_cl_supported_feat_mask |= BLE_GATT_CL_SUP_FEAT_CACHING_BITMASK; VLOG(1) << __func__ << ": gatt_if=" << gatt_cb.gatt_if << " EATT supported"; @@ -848,6 +850,19 @@ bool gatt_profile_get_eatt_support(const RawAddress& remote_bda) { /******************************************************************************* * + * Function gatt_cl_is_robust_caching_enabled + * + * Description Check if Robust Caching is enabled on client side. + * + * Returns true if enabled in gd flag, otherwise false + * + ******************************************************************************/ +static bool gatt_cl_is_robust_caching_enabled() { + return bluetooth::common::init_flags::gatt_robust_caching_client_is_enabled(); +} + +/******************************************************************************* + * * Function gatt_sr_is_robust_caching_enabled * * Description Check if Robust Caching is enabled on server side. @@ -856,7 +871,7 @@ bool gatt_profile_get_eatt_support(const RawAddress& remote_bda) { * ******************************************************************************/ static bool gatt_sr_is_robust_caching_enabled() { - return bluetooth::common::init_flags::gatt_robust_caching_is_enabled(); + return bluetooth::common::init_flags::gatt_robust_caching_server_is_enabled(); } /******************************************************************************* diff --git a/system/stack/gatt/gatt_cl.cc b/system/stack/gatt/gatt_cl.cc index d62a0872d1..215ea2abdf 100644 --- a/system/stack/gatt/gatt_cl.cc +++ b/system/stack/gatt/gatt_cl.cc @@ -1138,7 +1138,8 @@ bool gatt_cl_send_next_cmd_inq(tGATT_TCB& tcb) { cl_cmd_q = &tcb.cl_cmd_q; } else { EattChannel* channel = - EattExtension::GetInstance()->GetChannelWithQueuedData(tcb.peer_bda); + EattExtension::GetInstance()->GetChannelWithQueuedDataToSend( + tcb.peer_bda); cl_cmd_q = &channel->cl_cmd_q_; } diff --git a/system/stack/include/btm_iso_api.h b/system/stack/include/btm_iso_api.h index 221dfedb85..f0300366a5 100644 --- a/system/stack/include/btm_iso_api.h +++ b/system/stack/include/btm_iso_api.h @@ -107,8 +107,9 @@ class IsoManager { * Initiates removing of connected isochronous group (CIG). * * @param cig_id connected isochronous group id + * @param force do not check if CIG exist */ - virtual void RemoveCig(uint8_t cig_id); + virtual void RemoveCig(uint8_t cig_id, bool force = false); /** * Initiates creation of connected isochronous stream (CIS). diff --git a/system/stack/l2cap/l2c_fcr.cc b/system/stack/l2cap/l2c_fcr.cc index 4960858da8..98ebd2e12c 100644 --- a/system/stack/l2cap/l2c_fcr.cc +++ b/system/stack/l2cap/l2c_fcr.cc @@ -736,6 +736,10 @@ void l2c_lcc_proc_pdu(tL2C_CCB* p_ccb, BT_HDR* p_buf) { } else { p_data = p_ccb->ble_sdu; + if (p_data == NULL) { + osi_free(p_buf); + return; + } if (p_buf->len > (p_ccb->ble_sdu_length - p_data->len)) { L2CAP_TRACE_ERROR("%s: buffer length=%d too big. max=%d. Dropped", __func__, p_data->len, diff --git a/system/stack/sdp/sdp_main.cc b/system/stack/sdp/sdp_main.cc index 1cdeb9673a..9719d0faeb 100644 --- a/system/stack/sdp/sdp_main.cc +++ b/system/stack/sdp/sdp_main.cc @@ -22,8 +22,10 @@ * ******************************************************************************/ +#include <base/logging.h> #include <string.h> // memset +#include "gd/common/init_flags.h" #include "osi/include/allocator.h" #include "osi/include/osi.h" // UNUSED_ATTR #include "stack/include/bt_hdr.h" @@ -34,8 +36,6 @@ #include "stack/sdp/sdpint.h" #include "types/raw_address.h" -#include <base/logging.h> - /******************************************************************************/ /* G L O B A L S D P D A T A */ /******************************************************************************/ @@ -351,7 +351,8 @@ tCONN_CB* sdp_conn_originate(const RawAddress& p_bd_addr) { // Look for any active sdp connection on the remote device cid = sdpu_get_active_ccb_cid(p_bd_addr); - if (cid == 0) { + if (!bluetooth::common::init_flags::sdp_serialization_is_enabled() || + cid == 0) { p_ccb->con_state = SDP_STATE_CONN_SETUP; cid = L2CA_ConnectReq2(BT_PSM_SDP, p_bd_addr, BTM_SEC_NONE); } else { diff --git a/system/stack/test/btm/stack_btm_test.cc b/system/stack/test/btm/stack_btm_test.cc index 150012eee9..b9285e643a 100644 --- a/system/stack/test/btm/stack_btm_test.cc +++ b/system/stack/test/btm/stack_btm_test.cc @@ -88,6 +88,7 @@ bool get_pts_l2cap_ecoc_reconfigure(void) { return false; } const std::string* get_pts_broadcast_audio_config_options(void) { return &kBroadcastAudioConfigOptions; } +bool get_pts_le_audio_disable_ases_before_stopping(void) { return false; } config_t* get_all(void) { return nullptr; } const packet_fragmenter_t* packet_fragmenter_get_interface() { return nullptr; } @@ -119,6 +120,8 @@ stack_config_t mock_stack_config{ .get_pts_l2cap_ecoc_reconfigure = get_pts_l2cap_ecoc_reconfigure, .get_pts_broadcast_audio_config_options = get_pts_broadcast_audio_config_options, + .get_pts_le_audio_disable_ases_before_stopping = + get_pts_le_audio_disable_ases_before_stopping, .get_all = get_all, }; const stack_config_t* stack_config_get_interface(void) { diff --git a/system/stack/test/btm_iso_test.cc b/system/stack/test/btm_iso_test.cc index e18932b44e..a5554a06a7 100644 --- a/system/stack/test/btm_iso_test.cc +++ b/system/stack/test/btm_iso_test.cc @@ -770,6 +770,14 @@ TEST_F(IsoManagerDeathTest, RemoveCigWithNoSuchCig) { ::testing::KilledBySignal(SIGABRT), "No such cig"); } +TEST_F(IsoManagerDeathTest, RemoveCigForceNoSuchCig) { + EXPECT_CALL(hcic_interface_, + RemoveCig(volatile_test_cig_create_cmpl_evt_.cig_id, _)) + .Times(1); + IsoManager::GetInstance()->RemoveCig( + volatile_test_cig_create_cmpl_evt_.cig_id, true); +} + TEST_F(IsoManagerDeathTest, RemoveSameCigTwice) { IsoManager::GetInstance()->CreateCig( volatile_test_cig_create_cmpl_evt_.cig_id, kDefaultCigParams); diff --git a/system/stack/test/common/mock_eatt.cc b/system/stack/test/common/mock_eatt.cc index 6e07bf5469..dc9867fb36 100644 --- a/system/stack/test/common/mock_eatt.cc +++ b/system/stack/test/common/mock_eatt.cc @@ -86,9 +86,9 @@ bool EattExtension::IsOutstandingMsgInSendQueue(const RawAddress& bd_addr) { return pimpl_->IsOutstandingMsgInSendQueue(bd_addr); } -EattChannel* EattExtension::GetChannelWithQueuedData( +EattChannel* EattExtension::GetChannelWithQueuedDataToSend( const RawAddress& bd_addr) { - return pimpl_->GetChannelWithQueuedData(bd_addr); + return pimpl_->GetChannelWithQueuedDataToSend(bd_addr); } EattChannel* EattExtension::GetChannelAvailableForClientRequest( diff --git a/system/stack/test/common/mock_eatt.h b/system/stack/test/common/mock_eatt.h index 6d1682befc..bbeb502297 100644 --- a/system/stack/test/common/mock_eatt.h +++ b/system/stack/test/common/mock_eatt.h @@ -52,7 +52,7 @@ class MockEattExtension : public EattExtension { (const RawAddress& bd_addr)); MOCK_METHOD((void), FreeGattResources, (const RawAddress& bd_addr)); MOCK_METHOD((bool), IsOutstandingMsgInSendQueue, (const RawAddress& bd_addr)); - MOCK_METHOD((EattChannel*), GetChannelWithQueuedData, + MOCK_METHOD((EattChannel*), GetChannelWithQueuedDataToSend, (const RawAddress& bd_addr)); MOCK_METHOD((EattChannel*), GetChannelAvailableForClientRequest, (const RawAddress& bd_addr)); diff --git a/system/stack/test/sdp/stack_sdp_test.cc b/system/stack/test/sdp/stack_sdp_test.cc index 0251aab54f..98bb1365e4 100644 --- a/system/stack/test/sdp/stack_sdp_test.cc +++ b/system/stack/test/sdp/stack_sdp_test.cc @@ -110,6 +110,8 @@ tCONN_CB* find_ccb(uint16_t cid, uint8_t state) { } TEST_F(StackSdpMainTest, sdp_service_search_request_queuing) { + bluetooth::common::InitFlags::SetAllForTesting(); + ASSERT_TRUE(SDP_ServiceSearchRequest(addr, sdp_db, nullptr)); const int cid = L2CA_ConnectReq2_cid; tCONN_CB* p_ccb1 = find_ccb(cid, SDP_STATE_CONN_SETUP); diff --git a/system/stack/test/stack_avrcp_test.cc b/system/stack/test/stack_avrcp_test.cc index 72ec45f290..e731e98b76 100644 --- a/system/stack/test/stack_avrcp_test.cc +++ b/system/stack/test/stack_avrcp_test.cc @@ -27,6 +27,56 @@ class StackAvrcpTest : public ::testing::Test { virtual ~StackAvrcpTest() = default; }; +TEST_F(StackAvrcpTest, test_avrcp_ctrl_parse_vendor_rsp) { + uint8_t scratch_buf[512]{}; + uint16_t scratch_buf_len = 512; + tAVRC_MSG msg{}; + tAVRC_RESPONSE result{}; + uint8_t vendor_rsp_buf[512]{}; + + msg.hdr.opcode = AVRC_OP_VENDOR; + msg.hdr.ctype = AVRC_CMD_STATUS; + + memset(vendor_rsp_buf, 0, sizeof(vendor_rsp_buf)); + vendor_rsp_buf[0] = AVRC_PDU_GET_ELEMENT_ATTR; + uint8_t* p = &vendor_rsp_buf[2]; + UINT16_TO_BE_STREAM(p, 0x0009); // parameter length + UINT8_TO_STREAM(p, 0x01); // number of attributes + UINT32_TO_STREAM(p, 0x00000000); // attribute ID + UINT16_TO_STREAM(p, 0x0000); // character set ID + UINT16_TO_STREAM(p, 0xffff); // attribute value length + msg.vendor.p_vendor_data = vendor_rsp_buf; + msg.vendor.vendor_len = 13; + EXPECT_EQ( + AVRC_Ctrl_ParsResponse(&msg, &result, scratch_buf, &scratch_buf_len), + AVRC_STS_INTERNAL_ERR); +} + +TEST_F(StackAvrcpTest, test_avrcp_parse_browse_rsp) { + uint8_t scratch_buf[512]{}; + uint16_t scratch_buf_len = 512; + tAVRC_MSG msg{}; + tAVRC_RESPONSE result{}; + uint8_t browse_rsp_buf[512]{}; + + msg.hdr.opcode = AVRC_OP_BROWSE; + + memset(browse_rsp_buf, 0, sizeof(browse_rsp_buf)); + browse_rsp_buf[0] = AVRC_PDU_GET_ITEM_ATTRIBUTES; + uint8_t* p = &browse_rsp_buf[1]; + UINT16_TO_BE_STREAM(p, 0x000a); // parameter length; + UINT8_TO_STREAM(p, 0x04); // status + UINT8_TO_STREAM(p, 0x01); // number of attribute + UINT32_TO_STREAM(p, 0x00000000); // attribute ID + UINT16_TO_STREAM(p, 0x0000); // character set ID + UINT16_TO_STREAM(p, 0xffff); // attribute value length + msg.browse.p_browse_data = browse_rsp_buf; + msg.browse.browse_len = 13; + EXPECT_EQ( + AVRC_Ctrl_ParsResponse(&msg, &result, scratch_buf, &scratch_buf_len), + AVRC_STS_BAD_CMD); +} + TEST_F(StackAvrcpTest, test_avrcp_parse_browse_cmd) { uint8_t scratch_buf[512]{}; tAVRC_MSG msg{}; diff --git a/system/stack/test/stack_smp_test.cc b/system/stack/test/stack_smp_test.cc index d0c7c0fabc..85a16f9797 100644 --- a/system/stack/test/stack_smp_test.cc +++ b/system/stack/test/stack_smp_test.cc @@ -63,6 +63,7 @@ bool get_pts_l2cap_ecoc_reconfigure(void) { return false; } const std::string* get_pts_broadcast_audio_config_options(void) { return &kBroadcastAudioConfigOptions; } +bool get_pts_le_audio_disable_ases_before_stopping(void) { return false; } config_t* get_all(void) { return nullptr; } const packet_fragmenter_t* packet_fragmenter_get_interface() { return nullptr; } @@ -95,6 +96,8 @@ stack_config_t mock_stack_config{ .get_pts_l2cap_ecoc_reconfigure = get_pts_l2cap_ecoc_reconfigure, .get_pts_broadcast_audio_config_options = get_pts_broadcast_audio_config_options, + .get_pts_le_audio_disable_ases_before_stopping = + get_pts_le_audio_disable_ases_before_stopping, .get_all = get_all, }; const stack_config_t* stack_config_get_interface(void) { diff --git a/system/test/common/stack_config.cc b/system/test/common/stack_config.cc index 300f0d6442..bb783f9f64 100644 --- a/system/test/common/stack_config.cc +++ b/system/test/common/stack_config.cc @@ -48,6 +48,7 @@ bool get_pts_l2cap_ecoc_reconfigure(void) { return false; } const std::string* get_pts_broadcast_audio_config_options(void) { return &kBroadcastAudioConfigOptions; } +bool get_pts_le_audio_disable_ases_before_stopping(void) { return false; } struct config_t; config_t* get_all(void) { return nullptr; } struct packet_fragmenter_t; @@ -82,6 +83,8 @@ stack_config_t mock_stack_config{ .get_pts_l2cap_ecoc_reconfigure = get_pts_l2cap_ecoc_reconfigure, .get_pts_broadcast_audio_config_options = get_pts_broadcast_audio_config_options, + .get_pts_le_audio_disable_ases_before_stopping = + get_pts_le_audio_disable_ases_before_stopping, .get_all = get_all, }; diff --git a/system/test/mock/mock_bta_dm_act.h b/system/test/mock/mock_bta_dm_act.h index 5a566dcb7d..af586006e6 100644 --- a/system/test/mock/mock_bta_dm_act.h +++ b/system/test/mock/mock_bta_dm_act.h @@ -264,6 +264,15 @@ struct bta_dm_clear_event_filter { }; extern struct bta_dm_clear_event_filter bta_dm_clear_event_filter; +// Name: bta_dm_ble_reset_id +// Params: None +// Return: void +struct bta_dm_ble_reset_id { + std::function<void()> body{[]() {}}; + void operator()() { body(); }; +}; +extern struct bta_dm_ble_reset_id bta_dm_ble_reset_id; + // Name: bta_dm_ble_passkey_reply // Params: const RawAddress& bd_addr, bool accept, uint32_t passkey // Return: void diff --git a/system/test/mock/mock_main_shim_btm_api.cc b/system/test/mock/mock_main_shim_btm_api.cc index 84c19ae40a..89ce392638 100644 --- a/system/test/mock/mock_main_shim_btm_api.cc +++ b/system/test/mock/mock_main_shim_btm_api.cc @@ -475,3 +475,8 @@ tBTM_STATUS bluetooth::shim::BTM_SetEventFilterInquiryResultAllDevices() { mock_function_count_map[__func__]++; return BTM_SUCCESS; } + +tBTM_STATUS bluetooth::shim::BTM_BleResetId() { + mock_function_count_map[__func__]++; + return BTM_SUCCESS; +} diff --git a/system/test/mock/mock_stack_btm_iso.cc b/system/test/mock/mock_stack_btm_iso.cc index 4f99d66579..6c5cda35d2 100644 --- a/system/test/mock/mock_stack_btm_iso.cc +++ b/system/test/mock/mock_stack_btm_iso.cc @@ -18,7 +18,7 @@ void IsoManager::CreateCig(uint8_t cig_id, struct iso_manager::cig_create_params cig_params) {} void IsoManager::ReconfigureCig( uint8_t cig_id, struct iso_manager::cig_create_params cig_params) {} -void IsoManager::RemoveCig(uint8_t cig_id) {} +void IsoManager::RemoveCig(uint8_t cig_id, bool force) {} void IsoManager::EstablishCis( struct iso_manager::cis_establish_params conn_params) {} void IsoManager::DisconnectCis(uint16_t cis_handle, uint8_t reason) {} diff --git a/system/test/rootcanal/Android.bp b/system/test/rootcanal/Android.bp index 38a6cdd5f1..bf81f49929 100644 --- a/system/test/rootcanal/Android.bp +++ b/system/test/rootcanal/Android.bp @@ -51,9 +51,6 @@ cc_binary { ], cflags: [ "-fvisibility=hidden", - "-Wall", - "-Wextra", - "-Werror", "-DHAS_NO_BDROID_BUILDCFG", ], generated_headers: [ @@ -106,9 +103,6 @@ cc_library_shared { "libprotobuf-cpp-lite", ], cflags: [ - "-Wall", - "-Wextra", - "-Werror", "-DHAS_NO_BDROID_BUILDCFG", ], generated_headers: [ diff --git a/system/tools/scripts/dump_le_audio.py b/system/tools/scripts/dump_le_audio.py index 806cdfb357..048facbfa8 100755 --- a/system/tools/scripts/dump_le_audio.py +++ b/system/tools/scripts/dump_le_audio.py @@ -79,6 +79,13 @@ OPCODE_RELEASE = 0x08 # opcode for hci command OPCODE_HCI_CREATE_CIS = 0x2064 OPCODE_REMOVE_ISO_DATA_PATH = 0x206F +OPCODE_LE_SET_PERIODIC_ADVERTISING_DATA = 0x203F +OPCODE_LE_CREATE_BIG = 0x2068 +OPCODE_LE_SETUP_ISO_DATA_PATH = 0x206E + +# HCI event +EVENT_CODE_LE_META_EVENT = 0x3E +SUBEVENT_CODE_LE_CREATE_BIG_COMPLETE = 0x1B TYPE_STREAMING_AUDIO_CONTEXTS = 0x02 @@ -114,6 +121,9 @@ AUDIO_LOCATION_LEFT = 0x01 AUDIO_LOCATION_RIGHT = 0x02 AUDIO_LOCATION_CENTER = 0x04 +AD_TYPE_SERVICE_DATA_16_BIT = 0x16 +BASIC_AUDIO_ANNOUNCEMENT_SERVICE = 0x1851 + packet_number = 0 debug_enable = False add_header = False @@ -158,35 +168,77 @@ class AseStream: print("octets_per_frame: " + str(self.octets_per_frame)) +class Broadcast: + + def __init__(self): + self.num_of_bis = defaultdict(int) # subgroup - num_of_bis + self.bis = defaultdict(BisStream) # bis_index - codec_config + self.bis_index_handle_map = defaultdict(int) # bis_index - bis_handle + self.bis_index_list = [] + + def dump(self): + for bis_index, iso_stream in self.bis.items(): + print("bis_index: " + str(bis_index) + " bis handle: " + str(self.bis_index_handle_map[bis_index])) + iso_stream.dump() + + +class BisStream: + + def __init__(self): + self.sampling_frequencies = 0xFF + self.frame_duration = 0xFF + self.channel_allocation = 0xFFFFFFFF + self.octets_per_frame = 0xFFFF + self.output_dump = [] + self.start_time = 0xFFFFFFFF + + def dump(self): + print("start_time: " + str(self.start_time)) + print("sampling_frequencies: " + str(self.sampling_frequencies)) + print("frame_duration: " + str(self.frame_duration)) + print("channel_allocation: " + str(self.channel_allocation)) + print("octets_per_frame: " + str(self.octets_per_frame)) + + connection_map = defaultdict(Connection) cis_acl_map = defaultdict(int) +broadcast_map = defaultdict(Broadcast) +big_adv_map = defaultdict(int) +bis_stream_map = defaultdict(BisStream) + + +def generate_header(file, stream, is_cis): + sf_case = { + SAMPLE_FREQUENCY_8000: 80, + SAMPLE_FREQUENCY_11025: 110, + SAMPLE_FREQUENCY_16000: 160, + SAMPLE_FREQUENCY_22050: 220, + SAMPLE_FREQUENCY_24000: 240, + SAMPLE_FREQUENCY_32000: 320, + SAMPLE_FREQUENCY_44100: 441, + SAMPLE_FREQUENCY_48000: 480, + SAMPLE_FREQUENCY_88200: 882, + SAMPLE_FREQUENCY_96000: 960, + SAMPLE_FREQUENCY_176400: 1764, + SAMPLE_FREQUENCY_192000: 1920, + SAMPLE_FREQUENCY_384000: 2840, + } + fd_case = {FRAME_DURATION_7_5: 7.5, FRAME_DURATION_10: 10} + al_case = {AUDIO_LOCATION_MONO: 1, AUDIO_LOCATION_LEFT: 1, AUDIO_LOCATION_RIGHT: 1, AUDIO_LOCATION_CENTER: 2} - -def generate_header(file, connection): header = bytearray.fromhex('1ccc1200') - for ase in connection.ase.values(): - sf_case = { - SAMPLE_FREQUENCY_8000: 80, - SAMPLE_FREQUENCY_11025: 110, - SAMPLE_FREQUENCY_16000: 160, - SAMPLE_FREQUENCY_22050: 220, - SAMPLE_FREQUENCY_24000: 240, - SAMPLE_FREQUENCY_32000: 320, - SAMPLE_FREQUENCY_44100: 441, - SAMPLE_FREQUENCY_48000: 480, - SAMPLE_FREQUENCY_88200: 882, - SAMPLE_FREQUENCY_96000: 960, - SAMPLE_FREQUENCY_176400: 1764, - SAMPLE_FREQUENCY_192000: 1920, - SAMPLE_FREQUENCY_384000: 2840, - } - header = header + struct.pack("<H", sf_case[ase.sampling_frequencies]) - fd_case = {FRAME_DURATION_7_5: 7.5, FRAME_DURATION_10: 10} - header = header + struct.pack("<H", int(ase.octets_per_frame * 8 * 10 / fd_case[ase.frame_duration])) - al_case = {AUDIO_LOCATION_MONO: 1, AUDIO_LOCATION_LEFT: 1, AUDIO_LOCATION_RIGHT: 1, AUDIO_LOCATION_CENTER: 2} - header = header + struct.pack("<HHHL", al_case[ase.channel_allocation], fd_case[ase.frame_duration] * 100, 0, - 48000000) - break + if is_cis: + for ase in stream.ase.values(): + header = header + struct.pack("<H", sf_case[ase.sampling_frequencies]) + header = header + struct.pack("<H", int(ase.octets_per_frame * 8 * 10 / fd_case[ase.frame_duration])) + header = header + struct.pack("<HHHL", al_case[ase.channel_allocation], fd_case[ase.frame_duration] * 100, + 0, 48000000) + break + else: + header = header + struct.pack("<H", sf_case[stream.sampling_frequencies]) + header = header + struct.pack("<H", int(stream.octets_per_frame * 8 * 10 / fd_case[stream.frame_duration])) + header = header + struct.pack("<HHHL", al_case[stream.channel_allocation], fd_case[stream.frame_duration] * 100, + 0, 48000000) file.write(header) @@ -206,7 +258,7 @@ def parse_codec_information(connection_handle, ase_id, packet): ase.frame_duration = value elif config_type == TYPE_CHANNEL_ALLOCATION: ase.channel_allocation = value - elif TYPE_OCTETS_PER_FRAME: + elif config_type == TYPE_OCTETS_PER_FRAME: ase.octets_per_frame = value length -= (config_length + 1) @@ -284,6 +336,64 @@ def parse_att_packet(packet, connection_handle, flags, timestamp): packet_handle.get((opcode, flags), lambda x, y, z: None)(packet, connection_handle, timestamp) +def parse_big_codec_information(adv_handle, packet): + # Ignore presentation delay + packet = unpack_data(packet, 3, True) + number_of_subgroup, packet = unpack_data(packet, 1, False) + for subgroup in range(number_of_subgroup): + num_of_bis, packet = unpack_data(packet, 1, False) + broadcast_map[adv_handle].num_of_bis[subgroup] = num_of_bis + # Ignore codec id + packet = unpack_data(packet, 5, True) + length, packet = unpack_data(packet, 1, False) + if len(packet) < length: + print("Invalid subgroup codec information length") + return + + while length > 0: + config_length, packet = unpack_data(packet, 1, False) + config_type, packet = unpack_data(packet, 1, False) + value, packet = unpack_data(packet, config_length - 1, False) + if config_type == TYPE_SAMPLING_FREQUENCIES: + sampling_frequencies = value + elif config_type == TYPE_FRAME_DURATION: + frame_duration = value + elif config_type == TYPE_OCTETS_PER_FRAME: + octets_per_frame = value + else: + print("Unknown config type") + length -= (config_length + 1) + + # Ignore metadata + metadata_length, packet = unpack_data(packet, 1, False) + packet = unpack_data(packet, metadata_length, True) + + for count in range(num_of_bis): + bis_index, packet = unpack_data(packet, 1, False) + broadcast_map[adv_handle].bis_index_list.append(bis_index) + length, packet = unpack_data(packet, 1, False) + if len(packet) < length: + print("Invalid level 3 codec information length") + return + + while length > 0: + config_length, packet = unpack_data(packet, 1, False) + config_type, packet = unpack_data(packet, 1, False) + value, packet = unpack_data(packet, config_length - 1, False) + if config_type == TYPE_CHANNEL_ALLOCATION: + channel_allocation = value + else: + print("Ignored config type") + length -= (config_length + 1) + + broadcast_map[adv_handle].bis[bis_index].sampling_frequencies = sampling_frequencies + broadcast_map[adv_handle].bis[bis_index].frame_duration = frame_duration + broadcast_map[adv_handle].bis[bis_index].octets_per_frame = octets_per_frame + broadcast_map[adv_handle].bis[bis_index].channel_allocation = channel_allocation + + return packet + + def debug_print(log): global packet_number print("#" + str(packet_number) + ": " + log) @@ -303,7 +413,7 @@ def unpack_data(data, byte, ignore): return value, data[byte:] -def parse_command_packet(packet): +def parse_command_packet(packet, timestamp): opcode, packet = unpack_data(packet, 2, False) if opcode == OPCODE_HCI_CREATE_CIS: debug_print("OPCODE_HCI_CREATE_CIS") @@ -330,9 +440,96 @@ def parse_command_packet(packet): debug_print("Invalid cmd length") return - cis_handle, packet = unpack_data(packet, 2, False) - acl_handle = cis_acl_map[cis_handle] - dump_audio_data_to_file(acl_handle) + iso_handle, packet = unpack_data(packet, 2, False) + # CIS stream + if iso_handle in cis_acl_map: + acl_handle = cis_acl_map[iso_handle] + dump_cis_audio_data_to_file(acl_handle) + # To Do: BIS stream + elif iso_handle in bis_stream_map: + dump_bis_audio_data_to_file(iso_handle) + elif opcode == OPCODE_LE_SET_PERIODIC_ADVERTISING_DATA: + debug_print("OPCODE_LE_SET_PERIODIC_ADVERTISING_DATA") + + length, packet = unpack_data(packet, 1, False) + if length != len(packet): + debug_print("Invalid cmd length") + return + + if length < 21: + debug_print("Ignored. Not basic audio announcement") + return + + adv_hdl, packet = unpack_data(packet, 1, False) + #ignore operation, advertising_data_length + packet = unpack_data(packet, 2, True) + length, packet = unpack_data(packet, 1, False) + if length != len(packet): + debug_print("Invalid AD element length") + return + + ad_type, packet = unpack_data(packet, 1, False) + service, packet = unpack_data(packet, 2, False) + if ad_type != AD_TYPE_SERVICE_DATA_16_BIT or service != BASIC_AUDIO_ANNOUNCEMENT_SERVICE: + debug_print("Ignored. Not basic audio announcement") + return + + packet = parse_big_codec_information(adv_hdl, packet) + elif opcode == OPCODE_LE_CREATE_BIG: + debug_print("OPCODE_LE_CREATE_BIG") + + length, packet = unpack_data(packet, 1, False) + if length != len(packet) and length < 31: + debug_print("Invalid Create BIG command length") + return + + big_handle, packet = unpack_data(packet, 1, False) + adv_handle, packet = unpack_data(packet, 1, False) + big_adv_map[big_handle] = adv_handle + elif opcode == OPCODE_LE_SETUP_ISO_DATA_PATH: + debug_print("OPCODE_LE_SETUP_ISO_DATA_PATH") + length, packet = unpack_data(packet, 1, False) + if len(packet) != length: + debug_print("Invalid LE SETUP ISO DATA PATH command length") + return + + iso_handle, packet = unpack_data(packet, 2, False) + if iso_handle in bis_stream_map: + bis_stream_map[iso_handle].start_time = timestamp + + +def parse_event_packet(packet): + event_code, packet = unpack_data(packet, 1, False) + if event_code != EVENT_CODE_LE_META_EVENT: + return + + length, packet = unpack_data(packet, 1, False) + if len(packet) != length: + print("Invalid LE mata event length") + return + + subevent_code, packet = unpack_data(packet, 1, False) + if subevent_code != SUBEVENT_CODE_LE_CREATE_BIG_COMPLETE: + return + + status, packet = unpack_data(packet, 1, False) + if status != 0x00: + debug_print("Create_BIG failed") + return + + big_handle, packet = unpack_data(packet, 1, False) + if big_handle not in big_adv_map: + print("Invalid BIG handle") + return + adv_handle = big_adv_map[big_handle] + # Ignore, we don't care these parameter + packet = unpack_data(packet, 15, True) + num_of_bis, packet = unpack_data(packet, 1, False) + for count in range(num_of_bis): + bis_handle, packet = unpack_data(packet, 2, False) + bis_index = broadcast_map[adv_handle].bis_index_list[count] + broadcast_map[adv_handle].bis_index_handle_map[bis_index] = bis_handle + bis_stream_map[bis_handle] = broadcast_map[adv_handle].bis[bis_index] def convert_time_str(timestamp): @@ -348,7 +545,7 @@ def convert_time_str(timestamp): return full_str_format -def dump_audio_data_to_file(acl_handle): +def dump_cis_audio_data_to_file(acl_handle): if debug_enable: connection_map[acl_handle].dump() file_name = "" @@ -389,20 +586,20 @@ def dump_audio_data_to_file(acl_handle): break if connection_map[acl_handle].input_dump != []: - debug_print("Dump input...") + debug_print("Dump unicast input...") f = open(file_name + "_input.bin", 'wb') if add_header == True: - generate_header(f, connection_map[acl_handle]) + generate_header(f, connection_map[acl_handle], True) arr = bytearray(connection_map[acl_handle].input_dump) f.write(arr) f.close() connection_map[acl_handle].input_dump = [] if connection_map[acl_handle].output_dump != []: - debug_print("Dump output...") + debug_print("Dump unicast output...") f = open(file_name + "_output.bin", 'wb') if add_header == True: - generate_header(f, connection_map[acl_handle]) + generate_header(f, connection_map[acl_handle], True) arr = bytearray(connection_map[acl_handle].output_dump) f.write(arr) f.close() @@ -411,6 +608,51 @@ def dump_audio_data_to_file(acl_handle): return +def dump_bis_audio_data_to_file(iso_handle): + if debug_enable: + bis_stream_map[iso_handle].dump() + file_name = "broadcast" + sf_case = { + SAMPLE_FREQUENCY_8000: "8000", + SAMPLE_FREQUENCY_11025: "11025", + SAMPLE_FREQUENCY_16000: "16000", + SAMPLE_FREQUENCY_22050: "22050", + SAMPLE_FREQUENCY_24000: "24000", + SAMPLE_FREQUENCY_32000: "32000", + SAMPLE_FREQUENCY_44100: "44100", + SAMPLE_FREQUENCY_48000: "48000", + SAMPLE_FREQUENCY_88200: "88200", + SAMPLE_FREQUENCY_96000: "96000", + SAMPLE_FREQUENCY_176400: "176400", + SAMPLE_FREQUENCY_192000: "192000", + SAMPLE_FREQUENCY_384000: "284000" + } + file_name += ("_sf" + sf_case[bis_stream_map[iso_handle].sampling_frequencies]) + fd_case = {FRAME_DURATION_7_5: "7_5", FRAME_DURATION_10: "10"} + file_name += ("_fd" + fd_case[bis_stream_map[iso_handle].frame_duration]) + al_case = { + AUDIO_LOCATION_MONO: "mono", + AUDIO_LOCATION_LEFT: "left", + AUDIO_LOCATION_RIGHT: "right", + AUDIO_LOCATION_CENTER: "center" + } + file_name += ("_" + al_case[bis_stream_map[iso_handle].channel_allocation]) + file_name += ("_frame" + str(bis_stream_map[iso_handle].octets_per_frame)) + file_name += ("_" + convert_time_str(bis_stream_map[iso_handle].start_time)) + + if bis_stream_map[iso_handle].output_dump != []: + debug_print("Dump broadcast output...") + f = open(file_name + "_output.bin", 'wb') + if add_header == True: + generate_header(f, bis_stream_map[iso_handle], False) + arr = bytearray(bis_stream_map[iso_handle].output_dump) + f.write(arr) + f.close() + bis_stream_map[iso_handle].output_dump = [] + + return + + def parse_acl_packet(packet, flags, timestamp): # Check the minimum acl length, HCI leader (4 bytes) # + L2CAP header (4 bytes) @@ -441,8 +683,8 @@ def parse_acl_packet(packet, flags, timestamp): def parse_iso_packet(packet, flags): - cis_handle, packet = unpack_data(packet, 2, False) - cis_handle &= 0x0EFF + iso_handle, packet = unpack_data(packet, 2, False) + iso_handle &= 0x0EFF iso_data_load_length, packet = unpack_data(packet, 2, False) if iso_data_load_length != len(packet): debug_print("Invalid iso data load length") @@ -457,13 +699,18 @@ def parse_iso_packet(packet, flags): debug_print("Invalid iso sdu length") return - acl_handle = cis_acl_map[cis_handle] - if flags == SENT: - connection_map[acl_handle].output_dump.extend(struct.pack("<H", len(packet))) - connection_map[acl_handle].output_dump.extend(list(packet)) - elif flags == RECEIVED: - connection_map[acl_handle].input_dump.extend(struct.pack("<H", len(packet))) - connection_map[acl_handle].input_dump.extend(list(packet)) + # CIS stream + if iso_handle in cis_acl_map: + acl_handle = cis_acl_map[iso_handle] + if flags == SENT: + connection_map[acl_handle].output_dump.extend(struct.pack("<H", len(packet))) + connection_map[acl_handle].output_dump.extend(list(packet)) + elif flags == RECEIVED: + connection_map[acl_handle].input_dump.extend(struct.pack("<H", len(packet))) + connection_map[acl_handle].input_dump.extend(list(packet)) + elif iso_handle in bis_stream_map: + bis_stream_map[iso_handle].output_dump.extend(struct.pack("<H", len(packet))) + bis_stream_map[iso_handle].output_dump.extend(list(packet)) def parse_next_packet(btsnoop_file): @@ -490,10 +737,10 @@ def parse_next_packet(btsnoop_file): return False packet_handle = { - COMMADN_PACKET: (lambda x, y, z: parse_command_packet(x)), + COMMADN_PACKET: (lambda x, y, z: parse_command_packet(x, z)), ACL_PACKET: (lambda x, y, z: parse_acl_packet(x, y, z)), SCO_PACKET: (lambda x, y, z: None), - EVENT_PACKET: (lambda x, y, z: None), + EVENT_PACKET: (lambda x, y, z: parse_event_packet(x)), ISO_PACKET: (lambda x, y, z: parse_iso_packet(x, y)) } packet_handle.get(type, lambda x, y, z: None)(packet, flags, timestamp) @@ -535,7 +782,10 @@ def main(): break for handle in connection_map.keys(): - dump_audio_data_to_file(handle) + dump_cis_audio_data_to_file(handle) + + for handle in bis_stream_map.keys(): + dump_bis_audio_data_to_file(handle) if __name__ == "__main__": diff --git a/system/vendor_libs/linux/interface/Android.bp b/system/vendor_libs/linux/interface/Android.bp index 85fde7d473..a92124e77f 100644 --- a/system/vendor_libs/linux/interface/Android.bp +++ b/system/vendor_libs/linux/interface/Android.bp @@ -24,6 +24,7 @@ package { cc_binary { name: "android.hardware.bluetooth@1.1-service.btlinux", + defaults: ["fluoride_common_options"], proprietary: true, relative_install_path: "hw", srcs: [ @@ -32,10 +33,6 @@ cc_binary { "bluetooth_hci.cc", "service.cc", ], - cflags: [ - "-Wall", - "-Werror", - ], header_libs: ["libbluetooth_headers"], shared_libs: [ "android.hardware.bluetooth@1.0", @@ -57,14 +54,11 @@ cc_binary { cc_library_static { name: "async_fd_watcher", + defaults: ["fluoride_common_options"], proprietary: true, srcs: [ "async_fd_watcher.cc", ], - cflags: [ - "-Wall", - "-Werror", - ], shared_libs: [ "liblog", ], diff --git a/tools/pdl/Android.bp b/tools/pdl/Android.bp index 80473ac6a2..663972c9a8 100644 --- a/tools/pdl/Android.bp +++ b/tools/pdl/Android.bp @@ -56,6 +56,8 @@ rust_test_host { "tests/generated/packet_decl_empty.rs", "tests/generated/packet_decl_simple_little_endian.rs", "tests/generated/packet_decl_simple_big_endian.rs", + "tests/generated/packet_decl_complex_little_endian.rs", + "tests/generated/packet_decl_complex_big_endian.rs", ], } diff --git a/tools/pdl/doc/reference.md b/tools/pdl/doc/reference.md index f30db0816d..3cbd256590 100644 --- a/tools/pdl/doc/reference.md +++ b/tools/pdl/doc/reference.md @@ -563,11 +563,13 @@ packet CRCedBrew { > padding_field:\ > `_padding_` `[` [INTEGER](#integer) `]` -A *\_padding\_* field adds a number of **octet** of padding. +A *\_padding\_* field immediately following an array field pads the array field with `0`s to the +specified number of **octets**. ``` -packet Padded { - _padding_[1] // 1 octet/8bit of padding +packet PaddedCoffee { + additions: CoffeeAddition[], + _padding_[100] } ``` diff --git a/tools/pdl/scripts/generate_python_backend.py b/tools/pdl/scripts/generate_python_backend.py index 34497a844a..172998ab7a 100755 --- a/tools/pdl/scripts/generate_python_backend.py +++ b/tools/pdl/scripts/generate_python_backend.py @@ -177,6 +177,8 @@ class FieldParser: """Parse the selected array field.""" array_size = core.get_array_field_size(field) element_width = core.get_array_element_size(field) + padded_size = field.padded_size + if element_width: if element_width % 8 != 0: raise Exception('Array element size is not a multiple of 8') @@ -202,6 +204,12 @@ class FieldParser: if field.size_modifier and size: self.append_(f"{size} = {size} - {field.size_modifier}") + # Parse from the padded array if padding is present. + if padded_size: + self.check_size_(padded_size) + self.append_(f"remaining_span = span[{padded_size}:]") + self.append_(f"span = span[:{padded_size}]") + # 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. if element_width is None and size is not None: @@ -263,6 +271,10 @@ class FieldParser: if size is not None: self.append_(f"span = span[{size}:]") + # Drop the padding + if padded_size: + self.append_(f"span = remaining_span") + def parse_bit_field_(self, field: ast.Field): """Parse the selected field as a bit field. The field is added to the current chunk. When a byte boundary @@ -346,7 +358,7 @@ class FieldParser: if self.shift != 0: raise Exception('Padding field does not start on an octet boundary') - self.offset += field.width + self.offset += field.size def parse_payload_field_(self, field: Union[ast.BodyField, ast.PayloadField]): """Parse body and payload fields.""" @@ -535,6 +547,9 @@ class FieldSerializer: def serialize_array_field_(self, field: ast.ArrayField): """Serialize the selected array field.""" + if field.padded_size: + self.append_(f"_{field.id}_start = len(_span)") + if field.width == 8: self.append_(f"_span.extend(self.{field.id})") else: @@ -543,6 +558,9 @@ class FieldSerializer: self.serialize_array_element_(field) self.unindent_() + if field.padded_size: + self.append_(f"_span.extend([0] * ({field.padded_size} - len(_span) + _{field.id}_start))") + 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 diff --git a/tools/pdl/scripts/pdl/ast.py b/tools/pdl/scripts/pdl/ast.py index 7bab412ac5..266635f48b 100644 --- a/tools/pdl/scripts/pdl/ast.py +++ b/tools/pdl/scripts/pdl/ast.py @@ -59,7 +59,7 @@ class ChecksumField(Field): @node('padding_field') class PaddingField(Field): - width: int + size: int @node('size_field') @@ -109,6 +109,7 @@ class ArrayField(Field): type_id: Optional[str] size_modifier: Optional[str] size: Optional[int] + padded_size: Optional[int] = field(init=False, default=None) @property def type(self) -> Optional['Declaration']: diff --git a/tools/pdl/scripts/pdl/core.py b/tools/pdl/scripts/pdl/core.py index f6ad1f730c..a4609c8f1c 100644 --- a/tools/pdl/scripts/pdl/core.py +++ b/tools/pdl/scripts/pdl/core.py @@ -2,7 +2,7 @@ from typing import Optional, List, Dict, Union, Tuple from .ast import * -def desugar_field_(field: Field, constraints: Dict[str, Constraint]) -> List[Field]: +def desugar_field_(field: Field, previous: Field, constraints: Dict[str, Constraint]) -> List[Field]: """Inline group and constrained fields. Constrained fields are transformed into fixed fields. Group fields are inlined and recursively desugared.""" @@ -13,6 +13,10 @@ def desugar_field_(field: Field, constraints: Dict[str, Constraint]) -> List[Fie fixed.parent = field.parent return [fixed] + elif isinstance(field, PaddingField): + previous.padded_size = field.size + return [] + elif isinstance(field, TypedefField) and field.id in constraints: tag_id = constraints[field.id].tag_id fixed = FixedField(kind='fixed_field', loc=field.loc, enum_id=field.type_id, tag_id=tag_id) @@ -24,7 +28,8 @@ def desugar_field_(field: Field, constraints: Dict[str, Constraint]) -> List[Fie constraints = dict([(c.id, c) for c in field.constraints]) fields = [] for f in group.fields: - fields.extend(desugar_field_(f, constraints)) + fields.extend(desugar_field_(f, previous, constraints)) + previous = f return fields else: @@ -45,7 +50,7 @@ def desugar(file: File): if isinstance(d, (PacketDeclaration, StructDeclaration)): fields = [] for f in d.fields: - fields.extend(desugar_field_(f, {})) + fields.extend(desugar_field_(f, fields[-1] if len(fields) > 0 else None, {})) d.fields = fields declarations.append(d) diff --git a/tools/pdl/scripts/pdl/parser.py b/tools/pdl/scripts/pdl/parser.py index a896b4be8e..d3fa4c2463 100644 --- a/tools/pdl/scripts/pdl/parser.py +++ b/tools/pdl/scripts/pdl/parser.py @@ -67,8 +67,8 @@ def parse_fields(data): 'field_id': m['field_id'], }) elif name == '_padding_': - m = re.match(rule(fr' \[ {g(integer, "width")} \]'), rest) - fields.append({'kind': 'padding_field', 'width': int(m['width'], 0)}) + m = re.match(rule(fr' \[ {g(integer, "size")} \]'), rest) + fields.append({'kind': 'padding_field', 'size': int(m['size'], 0)}) elif name == '_size_': m = re.match(rule(fr' \( {g(identifier, "field_id")} \) : {g(integer, "width")}'), rest) fields.append({'kind': 'size_field', 'field_id': m['field_id'], 'width': int(m['width'], 0)}) diff --git a/tools/pdl/src/ast.rs b/tools/pdl/src/ast.rs index 33d5537392..4792f11bbb 100644 --- a/tools/pdl/src/ast.rs +++ b/tools/pdl/src/ast.rs @@ -73,7 +73,7 @@ pub enum Field { #[serde(rename = "checksum_field")] Checksum { loc: SourceRange, field_id: String }, #[serde(rename = "padding_field")] - Padding { loc: SourceRange, width: usize }, + Padding { loc: SourceRange, size: usize }, #[serde(rename = "size_field")] Size { loc: SourceRange, field_id: String, width: usize }, #[serde(rename = "count_field")] diff --git a/tools/pdl/src/backends.rs b/tools/pdl/src/backends.rs new file mode 100644 index 0000000000..7c159f2178 --- /dev/null +++ b/tools/pdl/src/backends.rs @@ -0,0 +1,4 @@ +//! Compiler backends. + +pub mod json; +pub mod rust; diff --git a/tools/pdl/src/backends/json.rs b/tools/pdl/src/backends/json.rs new file mode 100644 index 0000000000..f113cbc560 --- /dev/null +++ b/tools/pdl/src/backends/json.rs @@ -0,0 +1,9 @@ +//! Rust compiler backend. + +use crate::ast; + +/// Turn the AST into a JSON representation. +pub fn generate(file: &ast::File) -> Result<String, String> { + serde_json::to_string_pretty(&file) + .map_err(|err| format!("could not JSON serialize grammar: {err}")) +} diff --git a/tools/pdl/src/backends/rust.rs b/tools/pdl/src/backends/rust.rs new file mode 100644 index 0000000000..13b30ea811 --- /dev/null +++ b/tools/pdl/src/backends/rust.rs @@ -0,0 +1,1028 @@ +//! Rust compiler backend. + +// The `format-push-string` lint was briefly enabled present in Rust +// 1.62. It is now moved the disabled "restriction" category instead. +// See https://github.com/rust-lang/rust-clippy/issues/9077 for the +// problems with this lint. +// +// Remove this when we use Rust 1.63 or later. +#![allow(clippy::format_push_string)] + +use crate::ast; +use quote::{format_ident, quote}; +use std::collections::HashMap; +use std::path::Path; +use syn::parse_quote; + +mod preamble; +mod types; + +/// Generate a block of code. +/// +/// Like `quote!`, but the code block will be followed by an empty +/// line of code. This makes the generated code more readable. +#[macro_export] +macro_rules! quote_block { + ($($tt:tt)*) => { + format!("{}\n\n", ::quote::quote!($($tt)*)) + } +} + +fn generate_field(field: &ast::Field, visibility: syn::Visibility) -> proc_macro2::TokenStream { + match field { + ast::Field::Scalar { id, width, .. } => { + let field_name = format_ident!("{id}"); + let field_type = types::Integer::new(*width); + quote! { + #visibility #field_name: #field_type + } + } + _ => todo!("unsupported field: {:?}", field), + } +} + +fn generate_field_getter(packet_name: &syn::Ident, field: &ast::Field) -> proc_macro2::TokenStream { + match field { + ast::Field::Scalar { id, width, .. } => { + // TODO(mgeisler): refactor with generate_field above. + let getter_name = format_ident!("get_{id}"); + let field_name = format_ident!("{id}"); + let field_type = types::Integer::new(*width); + quote! { + pub fn #getter_name(&self) -> #field_type { + self.#packet_name.as_ref().#field_name + } + } + } + _ => todo!("unsupported field: {:?}", field), + } +} + +/// Find byte indices covering `offset..offset+width` bits. +fn get_field_range(offset: usize, width: usize) -> std::ops::Range<usize> { + let start = offset / 8; + let mut end = (offset + width) / 8; + if (offset + width) % 8 != 0 { + end += 1; + } + start..end +} + +fn get_chunk_width(fields: &[ast::Field]) -> usize { + fields.iter().map(get_field_width).sum() +} + +/// Read data for a byte-aligned chunk. +fn generate_chunk_read( + packet_name: &str, + endianness_value: ast::EndiannessValue, + offset: usize, + chunk: &[ast::Field], +) -> proc_macro2::TokenStream { + assert!(offset % 8 == 0, "Chunks must be byte-aligned, got offset: {offset}"); + let getter = match endianness_value { + ast::EndiannessValue::BigEndian => format_ident!("from_be_bytes"), + ast::EndiannessValue::LittleEndian => format_ident!("from_le_bytes"), + }; + + // Work directly with the field name if we are reading a single + // field. This generates simpler code. + let chunk_name = match chunk { + [ast::Field::Scalar { id: field_name, .. }] => format_ident!("{}", field_name), + _ => format_ident!("chunk"), + }; + let chunk_width = get_chunk_width(chunk); + let chunk_type = types::Integer::new(chunk_width); + assert!(chunk_width % 8 == 0, "Chunks must have a byte size, got width: {chunk_width}"); + + let range = get_field_range(offset, chunk_width); + let indices = range.map(syn::Index::from).collect::<Vec<_>>(); + + let mut field_offset = offset; + let mut last_field_range_end = 0; + // TODO(mgeisler): emit just a single length check per chunk. We + // could even emit a single length check per packet. + let length_checks = chunk.iter().map(|field| match field { + ast::Field::Scalar { id, width, .. } => { + let field_range = get_field_range(field_offset, *width); + field_offset += *width; + if field_range.end == last_field_range_end { + None // Suppress redundant length check. + } else { + last_field_range_end = field_range.end; + let range_end = syn::Index::from(field_range.end); + Some(quote! { + if bytes.len() < #range_end { + return Err(Error::InvalidLengthError { + obj: #packet_name.to_string(), + field: #id.to_string(), + wanted: #range_end, + got: bytes.len(), + }); + } + }) + } + } + _ => todo!("unsupported field: {:?}", field), + }); + + // When the chunk_type.width is larger than chunk_width (e.g. + // chunk_width is 24 but chunk_type.width is 32), then we need + // zero padding. + let zero_padding_len = (chunk_type.width - chunk_width) / 8; + // We need the padding on the MSB side of the payload, so for + // big-endian, we need to padding on the left, for little-endian + // we need it on the right. + let (zero_padding_before, zero_padding_after) = match endianness_value { + ast::EndiannessValue::BigEndian => (vec![syn::Index::from(0); zero_padding_len], vec![]), + ast::EndiannessValue::LittleEndian => (vec![], vec![syn::Index::from(0); zero_padding_len]), + }; + + quote! { + #(#length_checks)* + let #chunk_name = #chunk_type::#getter([ + #(#zero_padding_before,)* #(bytes[#indices]),* #(, #zero_padding_after)* + ]); + } +} + +fn generate_chunk_read_field_adjustments(fields: &[ast::Field]) -> proc_macro2::TokenStream { + // If there is a single field in the chunk, then we don't have to + // shift, mask, or cast. + if fields.len() == 1 { + return quote! {}; + } + + let chunk_width = get_chunk_width(fields); + let chunk_type = types::Integer::new(chunk_width); + + let mut field_parsers = Vec::new(); + let mut field_offset = 0; + for field in fields { + match field { + ast::Field::Scalar { id, width, .. } => { + let field_name = format_ident!("{id}"); + let field_type = types::Integer::new(*width); + + let mut field = quote! { + chunk + }; + if field_offset > 0 { + let field_offset = syn::Index::from(field_offset); + let op = syn::parse_str::<syn::BinOp>(">>").unwrap(); + field = quote! { + (#field #op #field_offset) + }; + } + + if *width < field_type.width { + let bit_mask = mask_bits(*width); + field = quote! { + (#field & #bit_mask) + }; + } + + if field_type.width < chunk_type.width { + field = quote! { + #field as #field_type; + }; + } + + field_offset += width; + field_parsers.push(quote! { + let #field_name = #field; + }); + } + _ => todo!("unsupported field: {:?}", field), + } + } + + quote! { + #(#field_parsers)* + } +} + +fn generate_chunk_write_field_adjustments(chunk: &[ast::Field]) -> proc_macro2::TokenStream { + // Work directly with the field name if we are writing a single + // field. This generates simpler code. + if let [ast::Field::Scalar { id, .. }] = chunk { + // If there is a single field in the chunk, then we don't have to + // shift, mask, or cast. + let field_name = format_ident!("{id}"); + return quote! { + let #field_name = self.#field_name; + }; + } + + let chunk_width = get_chunk_width(chunk); + let chunk_type = types::Integer::new(chunk_width); + + let mut field_parsers = Vec::new(); + let mut field_offset = 0; + for field in chunk { + match field { + ast::Field::Scalar { id, width, .. } => { + let field_name = format_ident!("{id}"); + let field_type = types::Integer::new(*width); + + let mut field = quote! { + self.#field_name + }; + + if field_type.width < chunk_type.width { + field = quote! { + (#field as #chunk_type) + }; + } + + if *width < field_type.width { + let bit_mask = mask_bits(*width); + field = quote! { + (#field & #bit_mask) + }; + } + + if field_offset > 0 { + let field_offset = syn::Index::from(field_offset); + let op = syn::parse_str::<syn::BinOp>("<<").unwrap(); + field = quote! { + (#field #op #field_offset) + }; + } + + field_offset += width; + field_parsers.push(quote! { + let chunk = chunk | #field; + }); + } + _ => todo!("unsupported field: {:?}", field), + } + } + + quote! { + let chunk = 0; + #(#field_parsers)* + } +} + +/// Generate a bit-mask which masks out `n` least significant bits. +fn mask_bits(n: usize) -> syn::LitInt { + syn::parse_str::<syn::LitInt>(&format!("{:#x}", (1u64 << n) - 1)).unwrap() +} + +fn generate_chunk_write( + endianness_value: ast::EndiannessValue, + offset: usize, + chunk: &[ast::Field], +) -> proc_macro2::TokenStream { + let writer = match endianness_value { + ast::EndiannessValue::BigEndian => format_ident!("to_be_bytes"), + ast::EndiannessValue::LittleEndian => format_ident!("to_le_bytes"), + }; + + // Work directly with the field name if we are writing a single + // field. This generates simpler code. + let chunk_name = match chunk { + [ast::Field::Scalar { id, .. }] => format_ident!("{id}"), + _ => format_ident!("chunk"), + }; + let chunk_width = get_chunk_width(chunk); + assert!(chunk_width % 8 == 0, "Chunks must have a byte size, got width: {chunk_width}"); + + let range = get_field_range(offset, chunk_width); + let start = syn::Index::from(range.start); + let end = syn::Index::from(range.end); + // TODO(mgeisler): let slice = (chunk_type_width > chunk_width).then( ... ) + let chunk_byte_width = syn::Index::from(chunk_width / 8); + quote! { + buffer[#start..#end].copy_from_slice(&#chunk_name.#writer()[0..#chunk_byte_width]); + } +} + +/// Field size in bits. +fn get_field_width(field: &ast::Field) -> usize { + match field { + ast::Field::Scalar { width, .. } => *width, + _ => todo!("unsupported field: {:?}", field), + } +} + +/// Generate code for an `ast::Decl::Packet` enum value. +fn generate_packet_decl( + file: &ast::File, + packets: &HashMap<&str, &ast::Decl>, + child_ids: &[&str], + id: &str, + fields: &[ast::Field], + parent_id: &Option<String>, +) -> String { + // TODO(mgeisler): use the convert_case crate to convert between + // `FooBar` and `foo_bar` in the code below. + let mut code = String::new(); + + let has_children = !child_ids.is_empty(); + let child_idents = child_ids.iter().map(|id| format_ident!("{id}")).collect::<Vec<_>>(); + + let ident = format_ident!("{}", id.to_lowercase()); + let data_child_ident = format_ident!("{id}DataChild"); + let child_decl_packet_name = + child_idents.iter().map(|ident| format_ident!("{ident}Packet")).collect::<Vec<_>>(); + let child_name = format_ident!("{id}Child"); + if has_children { + let child_data_idents = child_idents.iter().map(|ident| format_ident!("{ident}Data")); + code.push_str("e_block! { + #[derive(Debug)] + enum #data_child_ident { + #(#child_idents(Arc<#child_data_idents>),)* + None, + } + + impl #data_child_ident { + fn get_total_size(&self) -> usize { + // TODO(mgeisler): use Self instad of #data_child_ident. + match self { + #(#data_child_ident::#child_idents(value) => value.get_total_size(),)* + #data_child_ident::None => 0, + } + } + } + + #[derive(Debug)] + pub enum #child_name { + #(#child_idents(#child_decl_packet_name),)* + None, + } + }); + } + + let data_name = format_ident!("{id}Data"); + let child_field = has_children.then(|| { + quote! { + child: #data_child_ident, + } + }); + let plain_fields = fields.iter().map(|field| generate_field(field, parse_quote!())); + code.push_str("e_block! { + #[derive(Debug)] + struct #data_name { + #(#plain_fields,)* + #child_field + } + }); + + let parent = parent_id.as_ref().map(|parent_id| match packets.get(parent_id.as_str()) { + Some(ast::Decl::Packet { id, .. }) => { + let parent_ident = format_ident!("{}", id.to_lowercase()); + let parent_data = format_ident!("{id}Data"); + quote! { + #parent_ident: Arc<#parent_data>, + } + } + _ => panic!("Could not find {parent_id}"), + }); + + let packet_name = format_ident!("{id}Packet"); + code.push_str("e_block! { + #[derive(Debug, Clone)] + pub struct #packet_name { + #parent + #ident: Arc<#data_name>, + } + }); + + let builder_name = format_ident!("{id}Builder"); + let pub_fields = fields.iter().map(|field| generate_field(field, parse_quote!(pub))); + code.push_str("e_block! { + #[derive(Debug)] + pub struct #builder_name { + #(#pub_fields,)* + } + }); + + let mut chunk_width = 0; + let chunks = fields.split_inclusive(|field| { + chunk_width += get_field_width(field); + chunk_width % 8 == 0 + }); + let mut field_parsers = Vec::new(); + let mut field_writers = Vec::new(); + let mut offset = 0; + for chunk in chunks { + field_parsers.push(generate_chunk_read(id, file.endianness.value, offset, chunk)); + field_parsers.push(generate_chunk_read_field_adjustments(chunk)); + + field_writers.push(generate_chunk_write_field_adjustments(chunk)); + field_writers.push(generate_chunk_write(file.endianness.value, offset, chunk)); + + offset += get_chunk_width(chunk); + } + + let field_names = fields + .iter() + .map(|field| match field { + ast::Field::Scalar { id, .. } => format_ident!("{id}"), + _ => todo!("unsupported field: {:?}", field), + }) + .collect::<Vec<_>>(); + + let packet_size_bits = get_chunk_width(fields); + if packet_size_bits % 8 != 0 { + panic!("packet {id} does not end on a byte boundary, size: {packet_size_bits} bits",); + } + let packet_size_bytes = syn::Index::from(packet_size_bits / 8); + let get_size_adjustment = (packet_size_bytes.index > 0).then(|| { + Some(quote! { + let ret = ret + #packet_size_bytes; + }) + }); + + code.push_str("e_block! { + impl #data_name { + fn conforms(bytes: &[u8]) -> bool { + // TODO(mgeisler): return Boolean expression directly. + // TODO(mgeisler): skip when total_field_size == 0. + if bytes.len() < #packet_size_bytes { + return false; + } + true + } + + fn parse(bytes: &[u8]) -> Result<Self> { + #(#field_parsers)* + Ok(Self { #(#field_names),* }) + } + + fn write_to(&self, buffer: &mut BytesMut) { + #(#field_writers)* + } + + fn get_total_size(&self) -> usize { + self.get_size() + } + + fn get_size(&self) -> usize { + let ret = 0; + #get_size_adjustment + ret + } + } + }); + + code.push_str("e_block! { + impl Packet for #packet_name { + fn to_bytes(self) -> Bytes { + let mut buffer = BytesMut::new(); + buffer.resize(self.#ident.get_total_size(), 0); + self.#ident.write_to(&mut buffer); + buffer.freeze() + } + fn to_vec(self) -> Vec<u8> { + self.to_bytes().to_vec() + } + } + impl From<#packet_name> for Bytes { + fn from(packet: #packet_name) -> Self { + packet.to_bytes() + } + } + impl From<#packet_name> for Vec<u8> { + fn from(packet: #packet_name) -> Self { + packet.to_vec() + } + } + }); + + let specialize = has_children.then(|| { + quote! { + pub fn specialize(&self) -> #child_name { + match &self.#ident.child { + #(#data_child_ident::#child_idents(_) => + #child_name::#child_idents( + #child_decl_packet_name::new(self.#ident.clone()).unwrap()),)* + #data_child_ident::None => #child_name::None, + } + } + } + }); + let field_getters = fields.iter().map(|field| generate_field_getter(&ident, field)); + code.push_str("e_block! { + impl #packet_name { + pub fn parse(bytes: &[u8]) -> Result<Self> { + Ok(Self::new(Arc::new(#data_name::parse(bytes)?)).unwrap()) + } + + #specialize + + fn new(root: Arc<#data_name>) -> std::result::Result<Self, &'static str> { + let #ident = root; + Ok(Self { #ident }) + } + + #(#field_getters)* + } + }); + + let child = has_children.then(|| { + quote! { + child: #data_child_ident::None, + } + }); + code.push_str("e_block! { + impl #builder_name { + pub fn build(self) -> #packet_name { + let #ident = Arc::new(#data_name { + #(#field_names: self.#field_names,)* + #child + }); + #packet_name::new(#ident).unwrap() + } + } + }); + + code +} + +fn generate_decl( + file: &ast::File, + packets: &HashMap<&str, &ast::Decl>, + children: &HashMap<&str, Vec<&str>>, + decl: &ast::Decl, +) -> String { + let empty: Vec<&str> = vec![]; + match decl { + ast::Decl::Packet { id, fields, parent_id, .. } => generate_packet_decl( + file, + packets, + children.get(id.as_str()).unwrap_or(&empty), + id, + fields, + parent_id, + ), + _ => todo!("unsupported Decl::{:?}", decl), + } +} + +/// Generate Rust code from an AST. +/// +/// The code is not formatted, pipe it through `rustfmt` to get +/// readable source code. +pub fn generate(sources: &ast::SourceDatabase, file: &ast::File) -> String { + let source = sources.get(file.file).expect("could not read source"); + + let mut children = HashMap::new(); + let mut packets = HashMap::new(); + for decl in &file.declarations { + if let ast::Decl::Packet { id, parent_id, .. } = decl { + packets.insert(id.as_str(), decl); + if let Some(parent_id) = parent_id { + children.entry(parent_id.as_str()).or_insert_with(Vec::new).push(id.as_str()); + } + } + } + + let mut code = String::new(); + + code.push_str(&preamble::generate(Path::new(source.name()))); + + for decl in &file.declarations { + code.push_str(&generate_decl(file, &packets, &children, decl)); + code.push_str("\n\n"); + } + + code +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast; + use crate::parser::parse_inline; + use crate::test_utils::{assert_eq_with_diff, assert_snapshot_eq, rustfmt}; + + /// Parse a string fragment as a PDL file. + /// + /// # Panics + /// + /// Panics on parse errors. + pub fn parse_str(text: &str) -> ast::File { + let mut db = ast::SourceDatabase::new(); + parse_inline(&mut db, String::from("stdin"), String::from(text)).expect("parse error") + } + + #[test] + fn test_generate_packet_decl_empty() { + let file = parse_str( + r#" + big_endian_packets + packet Foo {} + "#, + ); + let packets = HashMap::new(); + let children = HashMap::new(); + let decl = &file.declarations[0]; + let actual_code = generate_decl(&file, &packets, &children, decl); + assert_snapshot_eq("tests/generated/packet_decl_empty.rs", &rustfmt(&actual_code)); + } + + #[test] + fn test_generate_packet_decl_simple_little_endian() { + let file = parse_str( + r#" + little_endian_packets + + packet Foo { + x: 8, + y: 16, + z: 24, + } + "#, + ); + let packets = HashMap::new(); + let children = HashMap::new(); + let decl = &file.declarations[0]; + let actual_code = generate_decl(&file, &packets, &children, decl); + assert_snapshot_eq( + "tests/generated/packet_decl_simple_little_endian.rs", + &rustfmt(&actual_code), + ); + } + + #[test] + fn test_generate_packet_decl_simple_big_endian() { + let file = parse_str( + r#" + big_endian_packets + + packet Foo { + x: 8, + y: 16, + z: 24, + } + "#, + ); + let packets = HashMap::new(); + let children = HashMap::new(); + let decl = &file.declarations[0]; + let actual_code = generate_decl(&file, &packets, &children, decl); + assert_snapshot_eq( + "tests/generated/packet_decl_simple_big_endian.rs", + &rustfmt(&actual_code), + ); + } + + #[test] + fn test_generate_packet_decl_complex_little_endian() { + let grammar = parse_str( + r#" + little_endian_packets + + packet Foo { + a: 3, + b: 8, + c: 5, + d: 24, + e: 12, + f: 4, + } + "#, + ); + let packets = HashMap::new(); + let children = HashMap::new(); + let decl = &grammar.declarations[0]; + let actual_code = generate_decl(&grammar, &packets, &children, decl); + assert_snapshot_eq( + "tests/generated/packet_decl_complex_little_endian.rs", + &rustfmt(&actual_code), + ); + } + + #[test] + fn test_generate_packet_decl_complex_big_endian() { + let grammar = parse_str( + r#" + big_endian_packets + + packet Foo { + a: 3, + b: 8, + c: 5, + d: 24, + e: 12, + f: 4, + } + "#, + ); + let packets = HashMap::new(); + let children = HashMap::new(); + let decl = &grammar.declarations[0]; + let actual_code = generate_decl(&grammar, &packets, &children, decl); + assert_snapshot_eq( + "tests/generated/packet_decl_complex_big_endian.rs", + &rustfmt(&actual_code), + ); + } + + #[test] + fn test_get_field_range() { + // Zero widths will give you an empty slice iff the offset is + // byte aligned. In both cases, the slice covers the empty + // width. In practice, PDL doesn't allow zero-width fields. + assert_eq!(get_field_range(/*offset=*/ 0, /*width=*/ 0), (0..0)); + assert_eq!(get_field_range(/*offset=*/ 5, /*width=*/ 0), (0..1)); + assert_eq!(get_field_range(/*offset=*/ 8, /*width=*/ 0), (1..1)); + assert_eq!(get_field_range(/*offset=*/ 9, /*width=*/ 0), (1..2)); + + // Non-zero widths work as expected. + assert_eq!(get_field_range(/*offset=*/ 0, /*width=*/ 1), (0..1)); + assert_eq!(get_field_range(/*offset=*/ 0, /*width=*/ 5), (0..1)); + assert_eq!(get_field_range(/*offset=*/ 0, /*width=*/ 8), (0..1)); + assert_eq!(get_field_range(/*offset=*/ 0, /*width=*/ 20), (0..3)); + + assert_eq!(get_field_range(/*offset=*/ 5, /*width=*/ 1), (0..1)); + assert_eq!(get_field_range(/*offset=*/ 5, /*width=*/ 3), (0..1)); + assert_eq!(get_field_range(/*offset=*/ 5, /*width=*/ 4), (0..2)); + assert_eq!(get_field_range(/*offset=*/ 5, /*width=*/ 20), (0..4)); + } + + // Assert that an expression equals the given expression. + // + // Both expressions are wrapped in a `main` function (so we can + // format it with `rustfmt`) and a diff is be shown if they + // differ. + #[track_caller] + fn assert_expr_eq(left: proc_macro2::TokenStream, right: proc_macro2::TokenStream) { + let left = quote! { + fn main() { #left } + }; + let right = quote! { + fn main() { #right } + }; + assert_eq_with_diff( + "left", + &rustfmt(&left.to_string()), + "right", + &rustfmt(&right.to_string()), + ); + } + + #[test] + fn test_generate_chunk_read_8bit() { + let loc = ast::SourceRange::default(); + let fields = &[ast::Field::Scalar { loc, id: String::from("a"), width: 8 }]; + assert_expr_eq( + generate_chunk_read("Foo", ast::EndiannessValue::BigEndian, 80, fields), + quote! { + if bytes.len() < 11 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "a".to_string(), + wanted: 11, + got: bytes.len(), + }); + } + let a = u8::from_be_bytes([bytes[10]]); + }, + ); + } + + #[test] + fn test_generate_chunk_read_16bit_le() { + let loc = ast::SourceRange::default(); + let fields = &[ast::Field::Scalar { loc, id: String::from("a"), width: 16 }]; + assert_expr_eq( + generate_chunk_read("Foo", ast::EndiannessValue::LittleEndian, 80, fields), + quote! { + if bytes.len() < 12 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "a".to_string(), + wanted: 12, + got: bytes.len(), + }); + } + let a = u16::from_le_bytes([bytes[10], bytes[11]]); + }, + ); + } + + #[test] + fn test_generate_chunk_read_16bit_be() { + let loc = ast::SourceRange::default(); + let fields = &[ast::Field::Scalar { loc, id: String::from("a"), width: 16 }]; + assert_expr_eq( + generate_chunk_read("Foo", ast::EndiannessValue::BigEndian, 80, fields), + quote! { + if bytes.len() < 12 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "a".to_string(), + wanted: 12, + got: bytes.len(), + }); + } + let a = u16::from_be_bytes([bytes[10], bytes[11]]); + }, + ); + } + + #[test] + fn test_generate_chunk_read_24bit_le() { + let loc = ast::SourceRange::default(); + let fields = &[ast::Field::Scalar { loc, id: String::from("a"), width: 24 }]; + assert_expr_eq( + generate_chunk_read("Foo", ast::EndiannessValue::LittleEndian, 80, fields), + quote! { + if bytes.len() < 13 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "a".to_string(), + wanted: 13, + got: bytes.len(), + }); + } + let a = u32::from_le_bytes([bytes[10], bytes[11], bytes[12], 0]); + }, + ); + } + + #[test] + fn test_generate_chunk_read_24bit_be() { + let loc = ast::SourceRange::default(); + let fields = &[ast::Field::Scalar { loc, id: String::from("a"), width: 24 }]; + assert_expr_eq( + generate_chunk_read("Foo", ast::EndiannessValue::BigEndian, 80, fields), + quote! { + if bytes.len() < 13 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "a".to_string(), + wanted: 13, + got: bytes.len(), + }); + } + let a = u32::from_be_bytes([0, bytes[10], bytes[11], bytes[12]]); + }, + ); + } + + #[test] + fn test_generate_chunk_read_multiple_fields() { + let loc = ast::SourceRange::default(); + let fields = &[ + ast::Field::Scalar { loc, id: String::from("a"), width: 16 }, + ast::Field::Scalar { loc, id: String::from("b"), width: 24 }, + ]; + assert_expr_eq( + generate_chunk_read("Foo", ast::EndiannessValue::BigEndian, 80, fields), + quote! { + if bytes.len() < 12 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "a".to_string(), + wanted: 12, + got: bytes.len(), + }); + } + if bytes.len() < 15 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "b".to_string(), + wanted: 15, + got: bytes.len(), + }); + } + let chunk = + u64::from_be_bytes([0, 0, 0, bytes[10], bytes[11], bytes[12], bytes[13], bytes[14]]); + }, + ); + } + + #[test] + fn test_generate_chunk_read_field_adjustments_8bit() { + let loc = ast::SourceRange::default(); + let fields = vec![ + ast::Field::Scalar { loc, id: String::from("a"), width: 3 }, + ast::Field::Scalar { loc, id: String::from("b"), width: 5 }, + ]; + assert_expr_eq( + generate_chunk_read_field_adjustments(&fields), + quote! { + let a = (chunk & 0x7); + let b = ((chunk >> 3) & 0x1f); + }, + ); + } + + #[test] + fn test_generate_chunk_read_field_adjustments_48bit() { + let loc = ast::SourceRange::default(); + let fields = vec![ + ast::Field::Scalar { loc, id: String::from("a"), width: 3 }, + ast::Field::Scalar { loc, id: String::from("b"), width: 8 }, + ast::Field::Scalar { loc, id: String::from("c"), width: 10 }, + ast::Field::Scalar { loc, id: String::from("d"), width: 18 }, + ast::Field::Scalar { loc, id: String::from("e"), width: 9 }, + ]; + assert_expr_eq( + generate_chunk_read_field_adjustments(&fields), + quote! { + let a = (chunk & 0x7) as u8; + let b = (chunk >> 3) as u8; + let c = ((chunk >> 11) & 0x3ff) as u16; + let d = ((chunk >> 21) & 0x3ffff) as u32; + let e = ((chunk >> 39) & 0x1ff) as u16; + }, + ); + } + + #[test] + fn test_generate_chunk_write_8bit() { + let loc = ast::SourceRange::default(); + let fields = &[ast::Field::Scalar { loc, id: String::from("a"), width: 8 }]; + assert_expr_eq( + generate_chunk_write(ast::EndiannessValue::BigEndian, 80, fields), + quote! { + buffer[10..11].copy_from_slice(&a.to_be_bytes()[0..1]); + }, + ); + } + + #[test] + fn test_generate_chunk_write_16bit() { + let loc = ast::SourceRange::default(); + let fields = &[ast::Field::Scalar { loc, id: String::from("a"), width: 16 }]; + assert_expr_eq( + generate_chunk_write(ast::EndiannessValue::BigEndian, 80, fields), + quote! { + buffer[10..12].copy_from_slice(&a.to_be_bytes()[0..2]); + }, + ); + } + + #[test] + fn test_generate_chunk_write_24bit() { + let loc = ast::SourceRange::default(); + let fields = &[ast::Field::Scalar { loc, id: String::from("a"), width: 24 }]; + assert_expr_eq( + generate_chunk_write(ast::EndiannessValue::BigEndian, 80, fields), + quote! { + buffer[10..13].copy_from_slice(&a.to_be_bytes()[0..3]); + }, + ); + } + + #[test] + fn test_generate_chunk_write_multiple_fields() { + let loc = ast::SourceRange::default(); + let fields = &[ + ast::Field::Scalar { loc, id: String::from("a"), width: 16 }, + ast::Field::Scalar { loc, id: String::from("b"), width: 24 }, + ]; + assert_expr_eq( + generate_chunk_write(ast::EndiannessValue::BigEndian, 80, fields), + quote! { + buffer[10..15].copy_from_slice(&chunk.to_be_bytes()[0..5]); + }, + ); + } + + #[test] + fn test_generate_chunk_write_field_adjustments_8bit() { + let loc = ast::SourceRange::default(); + let fields = vec![ + ast::Field::Scalar { loc, id: String::from("a"), width: 3 }, + ast::Field::Scalar { loc, id: String::from("b"), width: 5 }, + ]; + assert_expr_eq( + generate_chunk_write_field_adjustments(&fields), + quote! { + let chunk = 0; + let chunk = chunk | (self.a & 0x7) ; + let chunk = chunk | ((self.b & 0x1f) << 3); + }, + ); + } + + #[test] + fn test_generate_chunk_write_field_adjustments_48bit() { + let loc = ast::SourceRange::default(); + let fields = vec![ + ast::Field::Scalar { loc, id: String::from("a"), width: 3 }, + ast::Field::Scalar { loc, id: String::from("b"), width: 8 }, + ast::Field::Scalar { loc, id: String::from("c"), width: 10 }, + ast::Field::Scalar { loc, id: String::from("d"), width: 18 }, + ast::Field::Scalar { loc, id: String::from("e"), width: 9 }, + ]; + assert_expr_eq( + generate_chunk_write_field_adjustments(&fields), + quote! { + let chunk = 0; + let chunk = chunk | ((self.a as u64) & 0x7); + let chunk = chunk | ((self.b as u64) << 3); + let chunk = chunk | (((self.c as u64) & 0x3ff) << 11); + let chunk = chunk | (((self.d as u64) & 0x3ffff) << 21); + let chunk = chunk | (((self.e as u64) & 0x1ff) << 39); + }, + ); + } +} diff --git a/tools/pdl/src/backends/rust/preamble.rs b/tools/pdl/src/backends/rust/preamble.rs new file mode 100644 index 0000000000..62fab56e2f --- /dev/null +++ b/tools/pdl/src/backends/rust/preamble.rs @@ -0,0 +1,67 @@ +use std::path::Path; + +use crate::quote_block; + +/// Generate the file preamble. +pub fn generate(path: &Path) -> String { + let mut code = String::new(); + let filename = path.file_name().unwrap().to_str().expect("non UTF-8 filename"); + code.push_str(&format!("// @generated rust packets from {filename}\n\n")); + + code.push_str("e_block! { + use bytes::{BufMut, Bytes, BytesMut}; + use num_derive::{FromPrimitive, ToPrimitive}; + use num_traits::{FromPrimitive, ToPrimitive}; + use std::convert::{TryFrom, TryInto}; + use std::fmt; + use std::sync::Arc; + use thiserror::Error; + }); + + code.push_str("e_block! { + type Result<T> = std::result::Result<T, Error>; + }); + + code.push_str("e_block! { + #[derive(Debug, Error)] + pub enum Error { + #[error("Packet parsing failed")] + InvalidPacketError, + #[error("{field} was {value:x}, which is not known")] + ConstraintOutOfBounds { field: String, value: u64 }, + #[error("when parsing {obj}.{field} needed length of {wanted} but got {got}")] + InvalidLengthError { obj: String, field: String, wanted: usize, got: usize }, + #[error("Due to size restrictions a struct could not be parsed.")] + ImpossibleStructError, + #[error("when parsing field {obj}.{field}, {value} is not a valid {type_} value")] + InvalidEnumValueError { obj: String, field: String, value: u64, type_: String }, + } + }); + + code.push_str("e_block! { + #[derive(Debug, Error)] + #[error("{0}")] + pub struct TryFromError(&'static str); + }); + + code.push_str("e_block! { + pub trait Packet { + fn to_bytes(self) -> Bytes; + fn to_vec(self) -> Vec<u8>; + } + }); + + code +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{assert_snapshot_eq, rustfmt}; + + #[test] + fn test_generate_preamble() { + let actual_code = generate(Path::new("some/path/foo.pdl")); + assert_snapshot_eq("tests/generated/preamble.rs", &rustfmt(&actual_code)); + } +} diff --git a/tools/pdl/src/backends/rust/types.rs b/tools/pdl/src/backends/rust/types.rs new file mode 100644 index 0000000000..d53101daeb --- /dev/null +++ b/tools/pdl/src/backends/rust/types.rs @@ -0,0 +1,49 @@ +//! Utility functions for dealing with Rust integer types. + +/// A Rust integer type such as `u8`. +pub struct Integer { + pub width: usize, +} + +impl Integer { + /// Get the Rust integer type for the given bit width. + /// + /// This will round up the size to the nearest Rust integer size. + /// PDL supports integers up to 64 bit, so it is an error to call + /// this with a width larger than 64. + pub fn new(width: usize) -> Integer { + for integer_width in [8, 16, 32, 64] { + if width <= integer_width { + return Integer { width: integer_width }; + } + } + panic!("Cannot construct Integer with width: {width}") + } +} + +impl quote::ToTokens for Integer { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let t: syn::Type = syn::parse_str(&format!("u{}", self.width)) + .expect("Could not parse integer, unsupported width?"); + t.to_tokens(tokens); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_integer_new() { + assert_eq!(Integer::new(0).width, 8); + assert_eq!(Integer::new(8).width, 8); + assert_eq!(Integer::new(9).width, 16); + assert_eq!(Integer::new(64).width, 64); + } + + #[test] + #[should_panic] + fn test_integer_new_panics_on_large_width() { + Integer::new(65); + } +} diff --git a/tools/pdl/src/generator.rs b/tools/pdl/src/generator.rs deleted file mode 100644 index a1bff766e9..0000000000 --- a/tools/pdl/src/generator.rs +++ /dev/null @@ -1,685 +0,0 @@ -// The `format-push-string` lint was briefly enabled present in Rust -// 1.62. It is now moved the disabled "restriction" category instead. -// See https://github.com/rust-lang/rust-clippy/issues/9077 for the -// problems with this lint. -// -// Remove this when we use Rust 1.63 or later. -#![allow(clippy::format_push_string)] - -use crate::ast; -use quote::{format_ident, quote}; -use std::collections::HashMap; -use std::path::Path; -use syn::parse_quote; - -/// Generate a block of code. -/// -/// Like `quote!`, but the code block will be followed by an empty -/// line of code. This makes the generated code more readable. -macro_rules! quote_block { - ($($tt:tt)*) => { - format!("{}\n\n", quote!($($tt)*)) - } -} - -/// Generate the file preamble. -fn generate_preamble(path: &Path) -> String { - let mut code = String::new(); - let filename = path.file_name().unwrap().to_str().expect("non UTF-8 filename"); - code.push_str(&format!("// @generated rust packets from {filename}\n\n")); - - code.push_str("e_block! { - use bytes::{BufMut, Bytes, BytesMut}; - use num_derive::{FromPrimitive, ToPrimitive}; - use num_traits::{FromPrimitive, ToPrimitive}; - use std::convert::{TryFrom, TryInto}; - use std::fmt; - use std::sync::Arc; - use thiserror::Error; - }); - - code.push_str("e_block! { - type Result<T> = std::result::Result<T, Error>; - }); - - code.push_str("e_block! { - #[derive(Debug, Error)] - pub enum Error { - #[error("Packet parsing failed")] - InvalidPacketError, - #[error("{field} was {value:x}, which is not known")] - ConstraintOutOfBounds { field: String, value: u64 }, - #[error("when parsing {obj}.{field} needed length of {wanted} but got {got}")] - InvalidLengthError { obj: String, field: String, wanted: usize, got: usize }, - #[error("Due to size restrictions a struct could not be parsed.")] - ImpossibleStructError, - #[error("when parsing field {obj}.{field}, {value} is not a valid {type_} value")] - InvalidEnumValueError { obj: String, field: String, value: u64, type_: String }, - } - }); - - code.push_str("e_block! { - #[derive(Debug, Error)] - #[error("{0}")] - pub struct TryFromError(&'static str); - }); - - code.push_str("e_block! { - pub trait Packet { - fn to_bytes(self) -> Bytes; - fn to_vec(self) -> Vec<u8>; - } - }); - - code -} - -/// Round up the bit width to a Rust integer size. -fn round_bit_width(width: usize) -> usize { - match width { - 8 => 8, - 16 => 16, - 24 | 32 => 32, - 40 | 48 | 56 | 64 => 64, - _ => todo!("unsupported field width: {width}"), - } -} - -/// Generate a Rust unsigned integer type large enough to hold -/// integers of the given bit width. -fn type_for_width(width: usize) -> syn::Type { - let rounded_width = round_bit_width(width); - syn::parse_str(&format!("u{rounded_width}")).unwrap() -} - -fn generate_field(field: &ast::Field, visibility: syn::Visibility) -> proc_macro2::TokenStream { - match field { - ast::Field::Scalar { id, width, .. } => { - let field_name = format_ident!("{id}"); - let field_type = type_for_width(*width); - quote! { - #visibility #field_name: #field_type - } - } - _ => todo!("unsupported field: {:?}", field), - } -} - -fn generate_field_getter(packet_name: &syn::Ident, field: &ast::Field) -> proc_macro2::TokenStream { - match field { - ast::Field::Scalar { id, width, .. } => { - // TODO(mgeisler): refactor with generate_field above. - let getter_name = format_ident!("get_{id}"); - let field_name = format_ident!("{id}"); - let field_type = type_for_width(*width); - quote! { - pub fn #getter_name(&self) -> #field_type { - self.#packet_name.as_ref().#field_name - } - } - } - _ => todo!("unsupported field: {:?}", field), - } -} - -/// Mask and rebind the field value (if necessary). -fn mask_field_value(field: &ast::Field) -> Option<proc_macro2::TokenStream> { - match field { - ast::Field::Scalar { id, width, .. } => { - let field_name = format_ident!("{id}"); - let type_width = round_bit_width(*width); - if *width != type_width { - let mask = - syn::parse_str::<syn::LitInt>(&format!("{:#x}", (1u64 << *width) - 1)).unwrap(); - Some(quote! { - let #field_name = #field_name & #mask; - }) - } else { - None - } - } - _ => todo!("unsupported field: {:?}", field), - } -} - -fn generate_field_parser( - endianness_value: ast::EndiannessValue, - packet_name: &str, - field: &ast::Field, - offset: usize, -) -> proc_macro2::TokenStream { - match field { - ast::Field::Scalar { id, width, .. } => { - let field_name = format_ident!("{id}"); - let type_width = round_bit_width(*width); - let field_type = type_for_width(*width); - - let getter = match endianness_value { - ast::EndiannessValue::BigEndian => format_ident!("from_be_bytes"), - ast::EndiannessValue::LittleEndian => format_ident!("from_le_bytes"), - }; - - // We need the padding on the MSB side of the payload, so - // for big-endian, we need to padding on the left, for - // little-endian we need it on the right. - let padding = vec![syn::Index::from(0); (type_width - width) / 8]; - let (padding_before, padding_after) = match endianness_value { - ast::EndiannessValue::BigEndian => (padding, vec![]), - ast::EndiannessValue::LittleEndian => (vec![], padding), - }; - - let wanted_len = syn::Index::from(offset + width / 8); - let indices = (offset..offset + width / 8).map(syn::Index::from); - let mask = mask_field_value(field); - - quote! { - // TODO(mgeisler): call a function instead to avoid - // generating so much code for this. - if bytes.len() < #wanted_len { - return Err(Error::InvalidLengthError { - obj: #packet_name.to_string(), - field: #id.to_string(), - wanted: #wanted_len, - got: bytes.len(), - }); - } - let #field_name = #field_type::#getter([ - #(#padding_before,)* #(bytes[#indices]),* #(, #padding_after)* - ]); - #mask - } - } - _ => todo!("unsupported field: {:?}", field), - } -} - -fn generate_field_writer( - file: &ast::File, - field: &ast::Field, - offset: usize, -) -> proc_macro2::TokenStream { - match field { - ast::Field::Scalar { id, width, .. } => { - let field_name = format_ident!("{id}"); - let start = syn::Index::from(offset); - let end = syn::Index::from(offset + width / 8); - let byte_width = syn::Index::from(width / 8); - let mask = mask_field_value(field); - let writer = match file.endianness.value { - ast::EndiannessValue::BigEndian => format_ident!("to_be_bytes"), - ast::EndiannessValue::LittleEndian => format_ident!("to_le_bytes"), - }; - quote! { - let #field_name = self.#field_name; - #mask - buffer[#start..#end].copy_from_slice(&#field_name.#writer()[0..#byte_width]); - } - } - _ => todo!("unsupported field: {:?}", field), - } -} - -fn get_field_size(field: &ast::Field) -> usize { - match field { - ast::Field::Scalar { width, .. } => width / 8, - _ => todo!("unsupported field: {:?}", field), - } -} - -/// Generate code for an `ast::Decl::Packet` enum value. -fn generate_packet_decl( - file: &ast::File, - packets: &HashMap<&str, &ast::Decl>, - child_ids: &[&str], - id: &str, - fields: &[ast::Field], - parent_id: &Option<String>, -) -> String { - // TODO(mgeisler): use the convert_case crate to convert between - // `FooBar` and `foo_bar` in the code below. - let mut code = String::new(); - - let has_children = !child_ids.is_empty(); - let child_idents = child_ids.iter().map(|id| format_ident!("{id}")).collect::<Vec<_>>(); - - let ident = format_ident!("{}", id.to_lowercase()); - let data_child_ident = format_ident!("{id}DataChild"); - let child_decl_packet_name = - child_idents.iter().map(|ident| format_ident!("{ident}Packet")).collect::<Vec<_>>(); - let child_name = format_ident!("{id}Child"); - if has_children { - let child_data_idents = child_idents.iter().map(|ident| format_ident!("{ident}Data")); - code.push_str("e_block! { - #[derive(Debug)] - enum #data_child_ident { - #(#child_idents(Arc<#child_data_idents>),)* - None, - } - - impl #data_child_ident { - fn get_total_size(&self) -> usize { - // TODO(mgeisler): use Self instad of #data_child_ident. - match self { - #(#data_child_ident::#child_idents(value) => value.get_total_size(),)* - #data_child_ident::None => 0, - } - } - } - - #[derive(Debug)] - pub enum #child_name { - #(#child_idents(#child_decl_packet_name),)* - None, - } - }); - } - - let data_name = format_ident!("{id}Data"); - let child_field = has_children.then(|| { - quote! { - child: #data_child_ident, - } - }); - let plain_fields = fields.iter().map(|field| generate_field(field, parse_quote!())); - code.push_str("e_block! { - #[derive(Debug)] - struct #data_name { - #(#plain_fields,)* - #child_field - } - }); - - let parent = parent_id.as_ref().map(|parent_id| match packets.get(parent_id.as_str()) { - Some(ast::Decl::Packet { id, .. }) => { - let parent_ident = format_ident!("{}", id.to_lowercase()); - let parent_data = format_ident!("{id}Data"); - quote! { - #parent_ident: Arc<#parent_data>, - } - } - _ => panic!("Could not find {parent_id}"), - }); - - let packet_name = format_ident!("{id}Packet"); - code.push_str("e_block! { - #[derive(Debug, Clone)] - pub struct #packet_name { - #parent - #ident: Arc<#data_name>, - } - }); - - let builder_name = format_ident!("{id}Builder"); - let pub_fields = fields.iter().map(|field| generate_field(field, parse_quote!(pub))); - code.push_str("e_block! { - #[derive(Debug)] - pub struct #builder_name { - #(#pub_fields,)* - } - }); - - // TODO(mgeisler): use the `Buf` trait instead of tracking - // the offset manually. - let mut offset = 0; - let field_parsers = fields.iter().map(|field| { - let parser = generate_field_parser(file.endianness.value, id, field, offset); - offset += get_field_size(field); - parser - }); - let field_names = fields - .iter() - .map(|field| match field { - ast::Field::Scalar { id, .. } => format_ident!("{id}"), - _ => todo!("unsupported field: {:?}", field), - }) - .collect::<Vec<_>>(); - let mut offset = 0; - let field_writers = fields.iter().map(|field| { - let writer = generate_field_writer(file, field, offset); - offset += get_field_size(field); - writer - }); - - let total_field_size = syn::Index::from(fields.iter().map(get_field_size).sum::<usize>()); - let get_size_adjustment = (total_field_size.index > 0).then(|| { - Some(quote! { - let ret = ret + #total_field_size; - }) - }); - - code.push_str("e_block! { - impl #data_name { - fn conforms(bytes: &[u8]) -> bool { - // TODO(mgeisler): return Boolean expression directly. - // TODO(mgeisler): skip when total_field_size == 0. - if bytes.len() < #total_field_size { - return false; - } - true - } - - fn parse(bytes: &[u8]) -> Result<Self> { - #(#field_parsers)* - Ok(Self { #(#field_names),* }) - } - - fn write_to(&self, buffer: &mut BytesMut) { - #(#field_writers)* - } - - fn get_total_size(&self) -> usize { - self.get_size() - } - - fn get_size(&self) -> usize { - let ret = 0; - #get_size_adjustment - ret - } - } - }); - - code.push_str("e_block! { - impl Packet for #packet_name { - fn to_bytes(self) -> Bytes { - let mut buffer = BytesMut::new(); - buffer.resize(self.#ident.get_total_size(), 0); - self.#ident.write_to(&mut buffer); - buffer.freeze() - } - fn to_vec(self) -> Vec<u8> { - self.to_bytes().to_vec() - } - } - impl From<#packet_name> for Bytes { - fn from(packet: #packet_name) -> Self { - packet.to_bytes() - } - } - impl From<#packet_name> for Vec<u8> { - fn from(packet: #packet_name) -> Self { - packet.to_vec() - } - } - }); - - let specialize = has_children.then(|| { - quote! { - pub fn specialize(&self) -> #child_name { - match &self.#ident.child { - #(#data_child_ident::#child_idents(_) => - #child_name::#child_idents( - #child_decl_packet_name::new(self.#ident.clone()).unwrap()),)* - #data_child_ident::None => #child_name::None, - } - } - } - }); - let field_getters = fields.iter().map(|field| generate_field_getter(&ident, field)); - code.push_str("e_block! { - impl #packet_name { - pub fn parse(bytes: &[u8]) -> Result<Self> { - Ok(Self::new(Arc::new(#data_name::parse(bytes)?)).unwrap()) - } - - #specialize - - fn new(root: Arc<#data_name>) -> std::result::Result<Self, &'static str> { - let #ident = root; - Ok(Self { #ident }) - } - - #(#field_getters)* - } - }); - - let child = has_children.then(|| { - quote! { - child: #data_child_ident::None, - } - }); - code.push_str("e_block! { - impl #builder_name { - pub fn build(self) -> #packet_name { - let #ident = Arc::new(#data_name { - #(#field_names: self.#field_names,)* - #child - }); - #packet_name::new(#ident).unwrap() - } - } - }); - - code -} - -fn generate_decl( - file: &ast::File, - packets: &HashMap<&str, &ast::Decl>, - children: &HashMap<&str, Vec<&str>>, - decl: &ast::Decl, -) -> String { - let empty: Vec<&str> = vec![]; - match decl { - ast::Decl::Packet { id, fields, parent_id, .. } => generate_packet_decl( - file, - packets, - children.get(id.as_str()).unwrap_or(&empty), - id, - fields, - parent_id, - ), - _ => todo!("unsupported Decl::{:?}", decl), - } -} - -/// Generate Rust code from an AST. -/// -/// The code is not formatted, pipe it through `rustfmt` to get -/// readable source code. -pub fn generate_rust(sources: &ast::SourceDatabase, file: &ast::File) -> String { - let source = sources.get(file.file).expect("could not read source"); - - let mut children = HashMap::new(); - let mut packets = HashMap::new(); - for decl in &file.declarations { - if let ast::Decl::Packet { id, parent_id, .. } = decl { - packets.insert(id.as_str(), decl); - if let Some(parent_id) = parent_id { - children.entry(parent_id.as_str()).or_insert_with(Vec::new).push(id.as_str()); - } - } - } - - let mut code = String::new(); - - code.push_str(&generate_preamble(Path::new(source.name()))); - - for decl in &file.declarations { - code.push_str(&generate_decl(file, &packets, &children, decl)); - code.push_str("\n\n"); - } - - code -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ast; - use crate::parser::parse_inline; - use crate::test_utils::{assert_eq_with_diff, assert_snapshot_eq, rustfmt}; - - /// Parse a string fragment as a PDL file. - /// - /// # Panics - /// - /// Panics on parse errors. - pub fn parse_str(text: &str) -> ast::File { - let mut db = ast::SourceDatabase::new(); - parse_inline(&mut db, String::from("stdin"), String::from(text)).expect("parse error") - } - - #[test] - fn test_generate_preamble() { - let actual_code = generate_preamble(Path::new("some/path/foo.pdl")); - assert_snapshot_eq("tests/generated/preamble.rs", &rustfmt(&actual_code)); - } - - #[test] - fn test_generate_packet_decl_empty() { - let file = parse_str( - r#" - big_endian_packets - packet Foo {} - "#, - ); - let packets = HashMap::new(); - let children = HashMap::new(); - let decl = &file.declarations[0]; - let actual_code = generate_decl(&file, &packets, &children, decl); - assert_snapshot_eq("tests/generated/packet_decl_empty.rs", &rustfmt(&actual_code)); - } - - #[test] - fn test_generate_packet_decl_little_endian() { - let file = parse_str( - r#" - little_endian_packets - - packet Foo { - x: 8, - y: 16, - z: 24, - } - "#, - ); - let packets = HashMap::new(); - let children = HashMap::new(); - let decl = &file.declarations[0]; - let actual_code = generate_decl(&file, &packets, &children, decl); - assert_snapshot_eq( - "tests/generated/packet_decl_simple_little_endian.rs", - &rustfmt(&actual_code), - ); - } - - #[test] - fn test_generate_packet_decl_simple_big_endian() { - let file = parse_str( - r#" - big_endian_packets - - packet Foo { - x: 8, - y: 16, - z: 24, - } - "#, - ); - let packets = HashMap::new(); - let children = HashMap::new(); - let decl = &file.declarations[0]; - let actual_code = generate_decl(&file, &packets, &children, decl); - assert_snapshot_eq( - "tests/generated/packet_decl_simple_big_endian.rs", - &rustfmt(&actual_code), - ); - } - - // Assert that an expression equals the given expression. - // - // Both expressions are wrapped in a `main` function (so we can - // format it with `rustfmt`) and a diff is be shown if they - // differ. - #[track_caller] - fn assert_expr_eq(left: proc_macro2::TokenStream, right: proc_macro2::TokenStream) { - let left = quote! { - fn main() { #left } - }; - let right = quote! { - fn main() { #right } - }; - assert_eq_with_diff( - "left", - &rustfmt(&left.to_string()), - "right", - &rustfmt(&right.to_string()), - ); - } - - #[test] - fn test_mask_field_value() { - let loc = ast::SourceRange::default(); - let field = ast::Field::Scalar { loc, id: String::from("a"), width: 8 }; - assert_eq!(mask_field_value(&field).map(|m| m.to_string()), None); - - let field = ast::Field::Scalar { loc, id: String::from("a"), width: 24 }; - assert_expr_eq(mask_field_value(&field).unwrap(), quote! { let a = a & 0xffffff; }); - } - - #[test] - fn test_generate_field_parser_no_padding() { - let loc = ast::SourceRange::default(); - let field = ast::Field::Scalar { loc, id: String::from("a"), width: 8 }; - - assert_expr_eq( - generate_field_parser(ast::EndiannessValue::BigEndian, "Foo", &field, 10), - quote! { - if bytes.len() < 11 { - return Err(Error::InvalidLengthError { - obj: "Foo".to_string(), - field: "a".to_string(), - wanted: 11, - got: bytes.len(), - }); - } - let a = u8::from_be_bytes([bytes[10]]); - }, - ); - } - - #[test] - fn test_generate_field_parser_little_endian_padding() { - // Test with width != type width. - let loc = ast::SourceRange::default(); - let field = ast::Field::Scalar { loc, id: String::from("a"), width: 24 }; - assert_expr_eq( - generate_field_parser(ast::EndiannessValue::LittleEndian, "Foo", &field, 10), - quote! { - if bytes.len() < 13 { - return Err(Error::InvalidLengthError { - obj: "Foo".to_string(), - field: "a".to_string(), - wanted: 13, - got: bytes.len(), - }); - } - let a = u32::from_le_bytes([bytes[10], bytes[11], bytes[12], 0]); - let a = a & 0xffffff; - }, - ); - } - - #[test] - fn test_generate_field_parser_big_endian_padding() { - // Test with width != type width. - let loc = ast::SourceRange::default(); - let field = ast::Field::Scalar { loc, id: String::from("a"), width: 24 }; - assert_expr_eq( - generate_field_parser(ast::EndiannessValue::BigEndian, "Foo", &field, 10), - quote! { - if bytes.len() < 13 { - return Err(Error::InvalidLengthError { - obj: "Foo".to_string(), - field: "a".to_string(), - wanted: 13, - got: bytes.len(), - }); - } - let a = u32::from_be_bytes([0, bytes[10], bytes[11], bytes[12]]); - let a = a & 0xffffff; - }, - ); - } -} diff --git a/tools/pdl/src/lint.rs b/tools/pdl/src/lint.rs index b2aa237fe6..3841799c49 100644 --- a/tools/pdl/src/lint.rs +++ b/tools/pdl/src/lint.rs @@ -3,6 +3,7 @@ use codespan_reporting::files; use codespan_reporting::term; use codespan_reporting::term::termcolor; use std::collections::HashMap; +use std::ptr; use crate::ast::*; @@ -347,6 +348,19 @@ impl<'d> PacketScope<'d> { } } + /// Return the field immediately preceding the selected field, or None + /// if no such field exists. + fn get_preceding_field(&self, searched_field: &FieldPath) -> Option<&FieldPath> { + let mut preceding_field: Option<&FieldPath> = None; + for field in self.fields.iter() { + if ptr::eq(field, searched_field) { + break; + } + preceding_field = Some(field); + } + preceding_field + } + /// Cleanup scope after processing all fields. fn finalize(&mut self, result: &mut LintDiagnostics) { // Check field shadowing. @@ -996,6 +1010,42 @@ fn lint_array( } } +// Helper for linting padding fields. +fn lint_padding( + _scope: &Scope, + packet_scope: &PacketScope, + path: &FieldPath, + _size: usize, + result: &mut LintDiagnostics, +) { + // The padding field must follow an array field. + + let padding_loc = path.loc(); + + match packet_scope.get_preceding_field(path).map(|f| f.0.last().unwrap()) { + None => result.push( + Diagnostic::error() + .with_message("padding field cannot be the first field of a packet") + .with_labels(vec![padding_loc.primary()]) + .with_notes(vec![ + "hint: padding fields must be placed after an array field".to_owned() + ]), + ), + Some(Field::Array { .. }) => (), + Some(preceding_field) => result.push( + Diagnostic::error() + .with_message(format!( + "padding field cannot be placed after {} field", + preceding_field.kind() + )) + .with_labels(vec![padding_loc.primary(), preceding_field.loc().secondary()]) + .with_notes(vec![ + "hint: padding fields must be placed after an array field".to_owned() + ]), + ), + } +} + // Helper for linting typedef fields. fn lint_typedef( scope: &Scope, @@ -1059,8 +1109,8 @@ fn lint_field( lint_array(scope, packet_scope, field, width, type_id, size_modifier, size, result) } Field::Typedef { type_id, .. } => lint_typedef(scope, packet_scope, field, type_id, result), - Field::Padding { .. } - | Field::Reserved { .. } + Field::Padding { size, .. } => lint_padding(scope, packet_scope, field, *size, result), + Field::Reserved { .. } | Field::Scalar { .. } | Field::Body { .. } | Field::Payload { .. } => (), @@ -1256,43 +1306,81 @@ mod test { use crate::lint::Lintable; use crate::parser::parse_inline; - macro_rules! parse { - ($db:expr, $text:literal) => { - parse_inline($db, "stdin".to_owned(), $text.to_owned()).expect("parsing failure") + macro_rules! lint_success { + ($name:ident, $text:literal) => { + #[test] + fn $name() { + let mut db = SourceDatabase::new(); + let file = parse_inline(&mut db, "stdin".to_owned(), $text.to_owned()) + .expect("parsing failure"); + assert!(file.lint().diagnostics.is_empty()); + } + }; + } + + macro_rules! lint_failure { + ($name:ident, $text:literal) => { + #[test] + fn $name() { + let mut db = SourceDatabase::new(); + let file = parse_inline(&mut db, "stdin".to_owned(), $text.to_owned()) + .expect("parsing failure"); + assert!(!file.lint().diagnostics.is_empty()); + } }; } - #[test] - fn test_packet_redeclared() { - let mut db = SourceDatabase::new(); - let file = parse!( - &mut db, - r#" + lint_failure!( + test_packet_redeclared, + r#" little_endian_packets struct Name { } packet Name { } "# - ); - let result = file.lint(); - assert!(!result.diagnostics.is_empty()); - } + ); - #[test] - fn test_packet_checksum_start() { - let mut db = SourceDatabase::new(); - let file = parse!( - &mut db, - r#" - little_endian_packets - checksum Checksum : 8 "Checksum" - packet P { - _checksum_start_(crc), - a: 16, - crc: Checksum, - } - "# - ); - let result = file.lint(); - assert!(dbg!(result.diagnostics).is_empty()); - } + lint_success!( + test_packet_checksum_start, + r#" + little_endian_packets + checksum Checksum : 8 "Checksum" + packet P { + _checksum_start_(crc), + a: 16, + crc: Checksum, + } + "# + ); + + lint_failure!( + test_padding_cannot_be_first_field, + r#" + little_endian_packets + struct Test { + _padding_[10], + } + "# + ); + + lint_failure!( + test_padding_cannot_follow_scalar_field, + r#" + little_endian_packets + struct Test { + scalar: 8, + _padding_[10], + } + "# + ); + + lint_success!( + test_padding, + r#" + little_endian_packets + struct Test { + array: 8[], + _padding_[10], + } + "# + ); } diff --git a/tools/pdl/src/main.rs b/tools/pdl/src/main.rs index 042f3858ee..afd9c70936 100644 --- a/tools/pdl/src/main.rs +++ b/tools/pdl/src/main.rs @@ -4,7 +4,7 @@ use codespan_reporting::term::{self, termcolor}; use structopt::StructOpt; mod ast; -mod generator; +mod backends; mod lint; mod parser; #[cfg(test)] @@ -67,10 +67,10 @@ fn main() -> std::process::ExitCode { match opt.output_format { OutputFormat::JSON => { - println!("{}", serde_json::to_string_pretty(&file).unwrap()) + println!("{}", backends::json::generate(&file).unwrap()) } OutputFormat::Rust => { - println!("{}", generator::generate_rust(&sources, &file)) + println!("{}", backends::rust::generate(&sources, &file)) } } std::process::ExitCode::SUCCESS diff --git a/tools/pdl/src/parser.rs b/tools/pdl/src/parser.rs index cb24a7ae45..f66774b0c6 100644 --- a/tools/pdl/src/parser.rs +++ b/tools/pdl/src/parser.rs @@ -308,8 +308,8 @@ fn parse_field(node: Node<'_>, context: &Context) -> Result<ast::Field, String> ast::Field::Checksum { loc, field_id } } Rule::padding_field => { - let width = parse_integer(&mut children)?; - ast::Field::Padding { loc, width } + let size = parse_integer(&mut children)?; + ast::Field::Padding { loc, size } } Rule::size_field => { let field_id = match children.next() { diff --git a/tools/pdl/tests/canonical/be_test_file.pdl b/tools/pdl/tests/canonical/be_test_file.pdl index 10b743056f..41c16b2002 100644 --- a/tools/pdl/tests/canonical/be_test_file.pdl +++ b/tools/pdl/tests/canonical/be_test_file.pdl @@ -206,14 +206,6 @@ packet Packet_Struct_Field { b: UnsizedStruct, } -// The parser must be able to handle padding fields. -// The parser should generate a static size guard. -packet Packet_Padding_Field { - a: 8, - _padding_ [1], - b: 8, -} - // The parser must be able to handle custom fields of constant size. // The parser should generate a static size guard. packet Packet_Custom_Field_ConstantSize { @@ -306,6 +298,21 @@ packet Packet_Array_Field_UnsizedElement_SizeModifier { array: UnsizedStruct[+2], } +// The parser must be able to handle arrays with padded size. +packet Packet_Array_Field_SizedElement_VariableSize_Padded { + _size_(array) : 4, + _reserved_: 4, + array: 16[], + _padding_ [16], +} + +// The parser must be able to handle arrays with padded size. +packet Packet_Array_Field_UnsizedElement_VariableCount_Padded { + _count_(array) : 8, + array: UnsizedStruct[], + _padding_ [16], +} + // Packet inheritance // The parser must handle specialization into @@ -472,17 +479,6 @@ packet Struct_Struct_Field { b: UnsizedStruct, } -// The parser must be able to handle padding fields. -// The parser should generate a static size guard. -struct Struct_Padding_Field_ { - a: 8, - _padding_ [1], - b: 8, -} -packet Struct_Padding_Field { - s: Struct_Padding_Field_, -} - // The parser must be able to handle custom fields of constant size. // The parser should generate a static size guard. struct Struct_Custom_Field_ConstantSize_ { @@ -613,3 +609,24 @@ struct Struct_Array_Field_UnsizedElement_SizeModifier_ { packet Struct_Array_Field_UnsizedElement_SizeModifier { s: Struct_Array_Field_UnsizedElement_SizeModifier_, } + +// The parser must be able to handle arrays with padded size. +struct Struct_Array_Field_SizedElement_VariableSize_Padded_ { + _size_(array) : 4, + _reserved_: 4, + array: 16[], + _padding_ [16], +} +packet Struct_Array_Field_SizedElement_VariableSize_Padded { + s: Struct_Array_Field_SizedElement_VariableSize_Padded_, +} + +// The parser must be able to handle arrays with padded size. +struct Struct_Array_Field_UnsizedElement_VariableCount_Padded_ { + _count_(array) : 8, + array: UnsizedStruct[], + _padding_ [16], +} +packet Struct_Array_Field_UnsizedElement_VariableCount_Padded { + s: Struct_Array_Field_UnsizedElement_VariableCount_Padded_, +} diff --git a/tools/pdl/tests/canonical/be_test_vectors.json b/tools/pdl/tests/canonical/be_test_vectors.json index de0b2b6782..b7b47e4027 100644 --- a/tools/pdl/tests/canonical/be_test_vectors.json +++ b/tools/pdl/tests/canonical/be_test_vectors.json @@ -383,18 +383,6 @@ ] }, { - "packet": "Packet_Padding_Field", - "tests": [ - { - "packed": "7e007f", - "unpacked": { - "a": 126, - "b": 127 - } - } - ] - }, - { "packet": "Packet_Array_Field_ScalarElement", "tests": [ { @@ -613,6 +601,31 @@ ] }, { + "packet": "Packet_Array_Field_SizedElement_VariableSize_Padded", + "tests": [ + { + "packed": "0000000000000000000000000000000000", + "unpacked": { + "array": [] + } + }, + { + "packed": "0e000102030405060708090a0b0c0d0000", + "unpacked": { + "array": [ + 1, + 515, + 1029, + 1543, + 2057, + 2571, + 3085 + ] + } + } + ] + }, + { "packet": "ScalarParent", "tests": [ { @@ -897,20 +910,6 @@ ] }, { - "packet": "Struct_Padding_Field", - "tests": [ - { - "packed": "2f0030", - "unpacked": { - "s": { - "a": 47, - "b": 48 - } - } - } - ] - }, - { "packet": "Struct_Array_Field_ScalarElement", "tests": [ { @@ -1147,5 +1146,34 @@ } } ] + }, + { + "packet": "Struct_Array_Field_SizedElement_VariableSize_Padded", + "tests": [ + { + "packed": "0000000000000000000000000000000000", + "unpacked": { + "s": { + "array": [] + } + } + }, + { + "packed": "0e000102030405060708090a0b0c0d0000", + "unpacked": { + "s": { + "array": [ + 1, + 515, + 1029, + 1543, + 2057, + 2571, + 3085 + ] + } + } + } + ] } ] diff --git a/tools/pdl/tests/canonical/le_test_file.pdl b/tools/pdl/tests/canonical/le_test_file.pdl index 0c6b181c6b..fa06829864 100644 --- a/tools/pdl/tests/canonical/le_test_file.pdl +++ b/tools/pdl/tests/canonical/le_test_file.pdl @@ -216,14 +216,6 @@ packet Packet_Struct_Field { b: UnsizedStruct, } -// The parser must be able to handle padding fields. -// The parser should generate a static size guard. -packet Packet_Padding_Field { - a: 8, - _padding_ [1], - b: 8, -} - // The parser must be able to handle custom fields of constant size. // The parser should generate a static size guard. packet Packet_Custom_Field_ConstantSize { @@ -316,6 +308,21 @@ packet Packet_Array_Field_UnsizedElement_SizeModifier { array: UnsizedStruct[+2], } +// The parser must be able to handle arrays with padded size. +packet Packet_Array_Field_SizedElement_VariableSize_Padded { + _size_(array) : 4, + _reserved_: 4, + array: 16[], + _padding_ [16], +} + +// The parser must be able to handle arrays with padded size. +packet Packet_Array_Field_UnsizedElement_VariableCount_Padded { + _count_(array) : 8, + array: UnsizedStruct[], + _padding_ [16], +} + // Packet inheritance // The parser must handle specialization into @@ -510,17 +517,6 @@ packet Struct_Struct_Field { b: UnsizedStruct, } -// The parser must be able to handle padding fields. -// The parser should generate a static size guard. -struct Struct_Padding_Field_ { - a: 8, - _padding_ [1], - b: 8, -} -packet Struct_Padding_Field { - s: Struct_Padding_Field_, -} - // The parser must be able to handle custom fields of constant size. // The parser should generate a static size guard. struct Struct_Custom_Field_ConstantSize_ { @@ -651,3 +647,24 @@ struct Struct_Array_Field_UnsizedElement_SizeModifier_ { packet Struct_Array_Field_UnsizedElement_SizeModifier { s: Struct_Array_Field_UnsizedElement_SizeModifier_, } + +// The parser must be able to handle arrays with padded size. +struct Struct_Array_Field_SizedElement_VariableSize_Padded_ { + _size_(array) : 4, + _reserved_: 4, + array: 16[], + _padding_ [16], +} +packet Struct_Array_Field_SizedElement_VariableSize_Padded { + s: Struct_Array_Field_SizedElement_VariableSize_Padded_, +} + +// The parser must be able to handle arrays with padded size. +struct Struct_Array_Field_UnsizedElement_VariableCount_Padded_ { + _count_(array) : 8, + array: UnsizedStruct[], + _padding_ [16], +} +packet Struct_Array_Field_UnsizedElement_VariableCount_Padded { + s: Struct_Array_Field_UnsizedElement_VariableCount_Padded_, +} diff --git a/tools/pdl/tests/canonical/le_test_vectors.json b/tools/pdl/tests/canonical/le_test_vectors.json index 8eff0837ac..7df394e8cf 100644 --- a/tools/pdl/tests/canonical/le_test_vectors.json +++ b/tools/pdl/tests/canonical/le_test_vectors.json @@ -383,18 +383,6 @@ ] }, { - "packet": "Packet_Padding_Field", - "tests": [ - { - "packed": "7e007f", - "unpacked": { - "a": 126, - "b": 127 - } - } - ] - }, - { "packet": "Packet_Array_Field_ScalarElement", "tests": [ { @@ -613,6 +601,31 @@ ] }, { + "packet": "Packet_Array_Field_SizedElement_VariableSize_Padded", + "tests": [ + { + "packed": "0000000000000000000000000000000000", + "unpacked": { + "array": [] + } + }, + { + "packed": "0e010003020504070609080b0a0d0c0000", + "unpacked": { + "array": [ + 1, + 515, + 1029, + 1543, + 2057, + 2571, + 3085 + ] + } + } + ] + }, + { "packet": "ScalarParent", "tests": [ { @@ -939,20 +952,6 @@ ] }, { - "packet": "Struct_Padding_Field", - "tests": [ - { - "packed": "2f0030", - "unpacked": { - "s": { - "a": 47, - "b": 48 - } - } - } - ] - }, - { "packet": "Struct_Array_Field_ScalarElement", "tests": [ { @@ -1189,5 +1188,34 @@ } } ] + }, + { + "packet": "Struct_Array_Field_SizedElement_VariableSize_Padded", + "tests": [ + { + "packed": "0000000000000000000000000000000000", + "unpacked": { + "s": { + "array": [] + } + } + }, + { + "packed": "0e010003020504070609080b0a0d0c0000", + "unpacked": { + "s": { + "array": [ + 1, + 515, + 1029, + 1543, + 2057, + 2571, + 3085 + ] + } + } + } + ] } ] diff --git a/tools/pdl/tests/generated/packet_decl_complex_big_endian.rs b/tools/pdl/tests/generated/packet_decl_complex_big_endian.rs new file mode 100644 index 0000000000..f1cf8afebf --- /dev/null +++ b/tools/pdl/tests/generated/packet_decl_complex_big_endian.rs @@ -0,0 +1,155 @@ +#[derive(Debug)] +struct FooData { + a: u8, + b: u8, + c: u8, + d: u32, + e: u16, + f: u8, +} + +#[derive(Debug, Clone)] +pub struct FooPacket { + foo: Arc<FooData>, +} + +#[derive(Debug)] +pub struct FooBuilder { + pub a: u8, + pub b: u8, + pub c: u8, + pub d: u32, + pub e: u16, + pub f: u8, +} + +impl FooData { + fn conforms(bytes: &[u8]) -> bool { + if bytes.len() < 7 { + return false; + } + true + } + fn parse(bytes: &[u8]) -> Result<Self> { + if bytes.len() < 1 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "a".to_string(), + wanted: 1, + got: bytes.len(), + }); + } + if bytes.len() < 2 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "b".to_string(), + wanted: 2, + got: bytes.len(), + }); + } + let chunk = u16::from_be_bytes([bytes[0], bytes[1]]); + let a = (chunk & 0x7) as u8; + let b = (chunk >> 3) as u8; + let c = ((chunk >> 11) & 0x1f) as u8; + if bytes.len() < 5 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "d".to_string(), + wanted: 5, + got: bytes.len(), + }); + } + let d = u32::from_be_bytes([0, bytes[2], bytes[3], bytes[4]]); + if bytes.len() < 7 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "e".to_string(), + wanted: 7, + got: bytes.len(), + }); + } + let chunk = u16::from_be_bytes([bytes[5], bytes[6]]); + let e = (chunk & 0xfff); + let f = ((chunk >> 12) & 0xf) as u8; + Ok(Self { a, b, c, d, e, f }) + } + fn write_to(&self, buffer: &mut BytesMut) { + let chunk = 0; + let chunk = chunk | ((self.a as u16) & 0x7); + let chunk = chunk | ((self.b as u16) << 3); + let chunk = chunk | (((self.c as u16) & 0x1f) << 11); + buffer[0..2].copy_from_slice(&chunk.to_be_bytes()[0..2]); + let d = self.d; + buffer[2..5].copy_from_slice(&d.to_be_bytes()[0..3]); + let chunk = 0; + let chunk = chunk | (self.e & 0xfff); + let chunk = chunk | (((self.f as u16) & 0xf) << 12); + buffer[5..7].copy_from_slice(&chunk.to_be_bytes()[0..2]); + } + fn get_total_size(&self) -> usize { + self.get_size() + } + fn get_size(&self) -> usize { + let ret = 0; + let ret = ret + 7; + ret + } +} + +impl Packet for FooPacket { + fn to_bytes(self) -> Bytes { + let mut buffer = BytesMut::new(); + buffer.resize(self.foo.get_total_size(), 0); + self.foo.write_to(&mut buffer); + buffer.freeze() + } + fn to_vec(self) -> Vec<u8> { + self.to_bytes().to_vec() + } +} +impl From<FooPacket> for Bytes { + fn from(packet: FooPacket) -> Self { + packet.to_bytes() + } +} +impl From<FooPacket> for Vec<u8> { + fn from(packet: FooPacket) -> Self { + packet.to_vec() + } +} + +impl FooPacket { + pub fn parse(bytes: &[u8]) -> Result<Self> { + Ok(Self::new(Arc::new(FooData::parse(bytes)?)).unwrap()) + } + fn new(root: Arc<FooData>) -> std::result::Result<Self, &'static str> { + let foo = root; + Ok(Self { foo }) + } + pub fn get_a(&self) -> u8 { + self.foo.as_ref().a + } + pub fn get_b(&self) -> u8 { + self.foo.as_ref().b + } + pub fn get_c(&self) -> u8 { + self.foo.as_ref().c + } + pub fn get_d(&self) -> u32 { + self.foo.as_ref().d + } + pub fn get_e(&self) -> u16 { + self.foo.as_ref().e + } + pub fn get_f(&self) -> u8 { + self.foo.as_ref().f + } +} + +impl FooBuilder { + pub fn build(self) -> FooPacket { + let foo = + Arc::new(FooData { a: self.a, b: self.b, c: self.c, d: self.d, e: self.e, f: self.f }); + FooPacket::new(foo).unwrap() + } +} diff --git a/tools/pdl/tests/generated/packet_decl_complex_little_endian.rs b/tools/pdl/tests/generated/packet_decl_complex_little_endian.rs new file mode 100644 index 0000000000..8c985d29a5 --- /dev/null +++ b/tools/pdl/tests/generated/packet_decl_complex_little_endian.rs @@ -0,0 +1,155 @@ +#[derive(Debug)] +struct FooData { + a: u8, + b: u8, + c: u8, + d: u32, + e: u16, + f: u8, +} + +#[derive(Debug, Clone)] +pub struct FooPacket { + foo: Arc<FooData>, +} + +#[derive(Debug)] +pub struct FooBuilder { + pub a: u8, + pub b: u8, + pub c: u8, + pub d: u32, + pub e: u16, + pub f: u8, +} + +impl FooData { + fn conforms(bytes: &[u8]) -> bool { + if bytes.len() < 7 { + return false; + } + true + } + fn parse(bytes: &[u8]) -> Result<Self> { + if bytes.len() < 1 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "a".to_string(), + wanted: 1, + got: bytes.len(), + }); + } + if bytes.len() < 2 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "b".to_string(), + wanted: 2, + got: bytes.len(), + }); + } + let chunk = u16::from_le_bytes([bytes[0], bytes[1]]); + let a = (chunk & 0x7) as u8; + let b = (chunk >> 3) as u8; + let c = ((chunk >> 11) & 0x1f) as u8; + if bytes.len() < 5 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "d".to_string(), + wanted: 5, + got: bytes.len(), + }); + } + let d = u32::from_le_bytes([bytes[2], bytes[3], bytes[4], 0]); + if bytes.len() < 7 { + return Err(Error::InvalidLengthError { + obj: "Foo".to_string(), + field: "e".to_string(), + wanted: 7, + got: bytes.len(), + }); + } + let chunk = u16::from_le_bytes([bytes[5], bytes[6]]); + let e = (chunk & 0xfff); + let f = ((chunk >> 12) & 0xf) as u8; + Ok(Self { a, b, c, d, e, f }) + } + fn write_to(&self, buffer: &mut BytesMut) { + let chunk = 0; + let chunk = chunk | ((self.a as u16) & 0x7); + let chunk = chunk | ((self.b as u16) << 3); + let chunk = chunk | (((self.c as u16) & 0x1f) << 11); + buffer[0..2].copy_from_slice(&chunk.to_le_bytes()[0..2]); + let d = self.d; + buffer[2..5].copy_from_slice(&d.to_le_bytes()[0..3]); + let chunk = 0; + let chunk = chunk | (self.e & 0xfff); + let chunk = chunk | (((self.f as u16) & 0xf) << 12); + buffer[5..7].copy_from_slice(&chunk.to_le_bytes()[0..2]); + } + fn get_total_size(&self) -> usize { + self.get_size() + } + fn get_size(&self) -> usize { + let ret = 0; + let ret = ret + 7; + ret + } +} + +impl Packet for FooPacket { + fn to_bytes(self) -> Bytes { + let mut buffer = BytesMut::new(); + buffer.resize(self.foo.get_total_size(), 0); + self.foo.write_to(&mut buffer); + buffer.freeze() + } + fn to_vec(self) -> Vec<u8> { + self.to_bytes().to_vec() + } +} +impl From<FooPacket> for Bytes { + fn from(packet: FooPacket) -> Self { + packet.to_bytes() + } +} +impl From<FooPacket> for Vec<u8> { + fn from(packet: FooPacket) -> Self { + packet.to_vec() + } +} + +impl FooPacket { + pub fn parse(bytes: &[u8]) -> Result<Self> { + Ok(Self::new(Arc::new(FooData::parse(bytes)?)).unwrap()) + } + fn new(root: Arc<FooData>) -> std::result::Result<Self, &'static str> { + let foo = root; + Ok(Self { foo }) + } + pub fn get_a(&self) -> u8 { + self.foo.as_ref().a + } + pub fn get_b(&self) -> u8 { + self.foo.as_ref().b + } + pub fn get_c(&self) -> u8 { + self.foo.as_ref().c + } + pub fn get_d(&self) -> u32 { + self.foo.as_ref().d + } + pub fn get_e(&self) -> u16 { + self.foo.as_ref().e + } + pub fn get_f(&self) -> u8 { + self.foo.as_ref().f + } +} + +impl FooBuilder { + pub fn build(self) -> FooPacket { + let foo = + Arc::new(FooData { a: self.a, b: self.b, c: self.c, d: self.d, e: self.e, f: self.f }); + FooPacket::new(foo).unwrap() + } +} diff --git a/tools/pdl/tests/generated/packet_decl_simple_big_endian.rs b/tools/pdl/tests/generated/packet_decl_simple_big_endian.rs index 6c5c7fb3e6..d2fc18663f 100644 --- a/tools/pdl/tests/generated/packet_decl_simple_big_endian.rs +++ b/tools/pdl/tests/generated/packet_decl_simple_big_endian.rs @@ -52,7 +52,6 @@ impl FooData { }); } let z = u32::from_be_bytes([0, bytes[3], bytes[4], bytes[5]]); - let z = z & 0xffffff; Ok(Self { x, y, z }) } fn write_to(&self, buffer: &mut BytesMut) { @@ -61,7 +60,6 @@ impl FooData { let y = self.y; buffer[1..3].copy_from_slice(&y.to_be_bytes()[0..2]); let z = self.z; - let z = z & 0xffffff; buffer[3..6].copy_from_slice(&z.to_be_bytes()[0..3]); } fn get_total_size(&self) -> usize { diff --git a/tools/pdl/tests/generated/packet_decl_simple_little_endian.rs b/tools/pdl/tests/generated/packet_decl_simple_little_endian.rs index 468dd70eaf..02cacbc16c 100644 --- a/tools/pdl/tests/generated/packet_decl_simple_little_endian.rs +++ b/tools/pdl/tests/generated/packet_decl_simple_little_endian.rs @@ -52,7 +52,6 @@ impl FooData { }); } let z = u32::from_le_bytes([bytes[3], bytes[4], bytes[5], 0]); - let z = z & 0xffffff; Ok(Self { x, y, z }) } fn write_to(&self, buffer: &mut BytesMut) { @@ -61,7 +60,6 @@ impl FooData { let y = self.y; buffer[1..3].copy_from_slice(&y.to_le_bytes()[0..2]); let z = self.z; - let z = z & 0xffffff; buffer[3..6].copy_from_slice(&z.to_le_bytes()[0..3]); } fn get_total_size(&self) -> usize { diff --git a/tools/rootcanal/Android.bp b/tools/rootcanal/Android.bp index 90841bd44a..702ddd3b8d 100644 --- a/tools/rootcanal/Android.bp +++ b/tools/rootcanal/Android.bp @@ -16,14 +16,12 @@ package { cc_defaults { name: "rootcanal_defaults", defaults: [ + "fluoride_common_options", "gd_defaults", "gd_clang_tidy", "gd_clang_tidy_ignore_android", ], cflags: [ - "-Wall", - "-Wextra", - "-Werror", "-fvisibility=hidden", "-DROOTCANAL_LMP", ], @@ -117,10 +115,47 @@ cc_library_static { srcs: ["model/devices/scripted_beacon_ble_payload.proto"], } +cc_test_host { + name: "rootcanal_hci_test", + defaults: [ + "clang_file_coverage", + "clang_coverage_bin", + "rootcanal_defaults", + ], + srcs: [ + "test/controller/le/le_clear_filter_accept_list_test.cc", + "test/controller/le/le_add_device_to_filter_accept_list_test.cc", + "test/controller/le/le_remove_device_from_filter_accept_list_test.cc", + "test/controller/le/le_add_device_to_resolving_list_test.cc", + "test/controller/le/le_clear_resolving_list_test.cc", + "test/controller/le/le_remove_device_from_resolving_list_test.cc", + "test/controller/le/le_set_address_resolution_enable_test.cc", + ], + header_libs: [ + "libbluetooth_headers", + ], + local_include_dirs: [ + "include", + ".", + ], + include_dirs: [ + "packages/modules/Bluetooth/system", + "packages/modules/Bluetooth/system/gd", + ], + shared_libs: [ + "liblog", + ], + static_libs: [ + "libbt-rootcanal", + "libjsoncpp", + ], +} + // test-vendor unit tests for host cc_test_host { name: "rootcanal_test_host", defaults: [ + "fluoride_common_options", "clang_file_coverage", "clang_coverage_bin", ], @@ -149,9 +184,6 @@ cc_test_host { "libbt-rootcanal", ], cflags: [ - "-Wall", - "-Wextra", - "-Werror", "-fvisibility=hidden", "-DLOG_NDEBUG=1", ], diff --git a/tools/rootcanal/lmp/build.rs b/tools/rootcanal/lmp/build.rs index 131103037b..a7eb2bc9c0 100644 --- a/tools/rootcanal/lmp/build.rs +++ b/tools/rootcanal/lmp/build.rs @@ -51,6 +51,7 @@ fn generate_packets() { ); } + println!("cargo:rerun-if-changed=lmp_packets.pdl"); let output = Command::new(packetgen.as_os_str().to_str().unwrap()) .arg("--out=".to_owned() + out_dir.as_os_str().to_str().unwrap()) .arg("--include=.") diff --git a/tools/rootcanal/lmp/src/procedure/encryption.rs b/tools/rootcanal/lmp/src/procedure/encryption.rs index df44124e76..841b20f4b6 100644 --- a/tools/rootcanal/lmp/src/procedure/encryption.rs +++ b/tools/rootcanal/lmp/src/procedure/encryption.rs @@ -1,9 +1,13 @@ // Bluetooth Core, Vol 2, Part C, 4.2.5 +use super::features; use crate::num_hci_command_packets; use crate::packets::{hci, lmp}; use crate::procedure::Context; +use hci::LMPFeaturesPage1Bits::SecureConnectionsHostSupport; +use hci::LMPFeaturesPage2Bits::SecureConnectionsControllerSupport; + pub async fn initiate(ctx: &impl Context) { // TODO: handle turn off let _ = ctx.receive_hci_command::<hci::SetConnectionEncryptionPacket>().await; @@ -36,11 +40,18 @@ pub async fn initiate(ctx: &impl Context) { ) .await; + let aes_ccm = features::supported_on_both_page1(ctx, SecureConnectionsHostSupport).await + && features::supported_on_both_page2(ctx, SecureConnectionsControllerSupport).await; + ctx.send_hci_event( hci::EncryptionChangeBuilder { status: hci::ErrorCode::Success, connection_handle: ctx.peer_handle(), - encryption_enabled: hci::EncryptionEnabled::On, + encryption_enabled: if aes_ccm { + hci::EncryptionEnabled::BrEdrAesCcm + } else { + hci::EncryptionEnabled::On + }, } .build(), ); @@ -72,12 +83,70 @@ pub async fn respond(ctx: &impl Context) { .build(), ); + let aes_ccm = features::supported_on_both_page1(ctx, SecureConnectionsHostSupport).await + && features::supported_on_both_page2(ctx, SecureConnectionsControllerSupport).await; + ctx.send_hci_event( hci::EncryptionChangeBuilder { status: hci::ErrorCode::Success, connection_handle: ctx.peer_handle(), - encryption_enabled: hci::EncryptionEnabled::On, + encryption_enabled: if aes_ccm { + hci::EncryptionEnabled::BrEdrAesCcm + } else { + hci::EncryptionEnabled::On + }, } .build(), ); } + +#[cfg(test)] +mod tests { + use super::initiate; + use super::respond; + use crate::procedure::Context; + use crate::test::{sequence, TestContext}; + + use crate::packets::hci::LMPFeaturesPage1Bits::SecureConnectionsHostSupport; + use crate::packets::hci::LMPFeaturesPage2Bits::SecureConnectionsControllerSupport; + + #[test] + fn accept_encryption() { + let context = TestContext::new(); + let procedure = respond; + + include!("../../test/ENC/BV-01-C.in"); + } + + #[test] + fn initiate_encryption() { + let context = TestContext::new(); + let procedure = initiate; + + include!("../../test/ENC/BV-05-C.in"); + } + + #[test] + fn accept_aes_ccm_encryption_request() { + let context = TestContext::new() + .with_page_1_feature(SecureConnectionsHostSupport) + .with_page_2_feature(SecureConnectionsControllerSupport) + .with_peer_page_1_feature(SecureConnectionsHostSupport) + .with_peer_page_2_feature(SecureConnectionsControllerSupport); + let procedure = respond; + + include!("../../test/ENC/BV-26-C.in"); + } + + #[test] + fn initiate_aes_ccm_encryption() { + let context = TestContext::new() + .with_page_1_feature(SecureConnectionsHostSupport) + .with_page_2_feature(SecureConnectionsControllerSupport) + .with_peer_page_1_feature(SecureConnectionsHostSupport) + .with_peer_page_2_feature(SecureConnectionsControllerSupport); + let procedure = initiate; + + include!("../../test/ENC/BV-34-C.in"); + } +} diff --git a/tools/rootcanal/lmp/src/procedure/features.rs b/tools/rootcanal/lmp/src/procedure/features.rs index 5de641cc76..d5a2eeab98 100644 --- a/tools/rootcanal/lmp/src/procedure/features.rs +++ b/tools/rootcanal/lmp/src/procedure/features.rs @@ -1,5 +1,7 @@ // Bluetooth Core, Vol 2, Part C, 4.3.4 +use num_traits::ToPrimitive; + use crate::packets::lmp; use crate::procedure::Context; @@ -34,21 +36,30 @@ pub async fn respond(ctx: &impl Context) { ); } -pub async fn supported_on_both_page1( - ctx: &impl Context, - feature: crate::packets::hci::LMPFeaturesPage1Bits, -) -> bool { - use num_traits::ToPrimitive; - let feature_mask = feature.to_u64().unwrap(); - let local_supported = ctx.extended_features(1) & feature_mask != 0; +async fn supported_on_both_page(ctx: &impl Context, page_number: u8, feature_mask: u64) -> bool { + let local_supported = ctx.extended_features(page_number) & feature_mask != 0; // Lazy peer features let peer_supported = async move { - let page = if let Some(page) = ctx.peer_extended_features(1) { + let page = if let Some(page) = ctx.peer_extended_features(page_number) { page } else { - crate::procedure::features::initiate(ctx, 1).await + crate::procedure::features::initiate(ctx, page_number).await }; page & feature_mask != 0 }; local_supported && peer_supported.await } + +pub async fn supported_on_both_page1( + ctx: &impl Context, + feature: crate::packets::hci::LMPFeaturesPage1Bits, +) -> bool { + supported_on_both_page(ctx, 1, feature.to_u64().unwrap()).await +} + +pub async fn supported_on_both_page2( + ctx: &impl Context, + feature: crate::packets::hci::LMPFeaturesPage2Bits, +) -> bool { + supported_on_both_page(ctx, 2, feature.to_u64().unwrap()).await +} diff --git a/tools/rootcanal/lmp/src/test/context.rs b/tools/rootcanal/lmp/src/test/context.rs index c784269be4..262b97f805 100644 --- a/tools/rootcanal/lmp/src/test/context.rs +++ b/tools/rootcanal/lmp/src/test/context.rs @@ -5,6 +5,8 @@ use std::future::Future; use std::pin::Pin; use std::task::{self, Poll}; +use num_traits::ToPrimitive; + use crate::ec::PrivateKey; use crate::packets::{hci, lmp}; @@ -17,11 +19,35 @@ pub struct TestContext { pub hci_events: RefCell<VecDeque<hci::EventPacket>>, pub hci_commands: RefCell<VecDeque<hci::CommandPacket>>, private_key: RefCell<Option<PrivateKey>>, + features_pages: [u64; 3], + peer_features_pages: [u64; 3], } impl TestContext { pub fn new() -> Self { - Default::default() + Self::default() + .with_page_1_feature(hci::LMPFeaturesPage1Bits::SecureSimplePairingHostSupport) + .with_peer_page_1_feature(hci::LMPFeaturesPage1Bits::SecureSimplePairingHostSupport) + } + + pub fn with_page_1_feature(mut self, feature: hci::LMPFeaturesPage1Bits) -> Self { + self.features_pages[1] |= feature.to_u64().unwrap(); + self + } + + pub fn with_page_2_feature(mut self, feature: hci::LMPFeaturesPage2Bits) -> Self { + self.features_pages[2] |= feature.to_u64().unwrap(); + self + } + + pub fn with_peer_page_1_feature(mut self, feature: hci::LMPFeaturesPage1Bits) -> Self { + self.peer_features_pages[1] |= feature.to_u64().unwrap(); + self + } + + pub fn with_peer_page_2_feature(mut self, feature: hci::LMPFeaturesPage2Bits) -> Self { + self.peer_features_pages[2] |= feature.to_u64().unwrap(); + self } } @@ -67,19 +93,11 @@ impl Context for TestContext { } fn peer_extended_features(&self, features_page: u8) -> Option<u64> { - if features_page == 1 { - Some(1) - } else { - Some(0) - } + Some(self.peer_features_pages[features_page as usize]) } fn extended_features(&self, features_page: u8) -> u64 { - if features_page == 1 { - 1 - } else { - 0 - } + self.features_pages[features_page as usize] } fn get_private_key(&self) -> Option<PrivateKey> { diff --git a/tools/rootcanal/lmp/test/ENC/BV-01-C.in b/tools/rootcanal/lmp/test/ENC/BV-01-C.in new file mode 100644 index 0000000000..cee42505db --- /dev/null +++ b/tools/rootcanal/lmp/test/ENC/BV-01-C.in @@ -0,0 +1,32 @@ +sequence! { procedure, context, + // ACL Connection Established + Lower Tester -> IUT: EncryptionModeReq { + transaction_id: 0, + encryption_mode: 0x01, + } + IUT ->Lower Tester: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::EncryptionModeReq, + } + Lower Tester -> IUT: EncryptionKeySizeReq { + transaction_id: 0, + key_size: 0x10, + } + IUT -> Lower Tester: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::EncryptionKeySizeReq, + } + Lower Tester -> IUT: StartEncryptionReq { + transaction_id: 0, + random_number: [0; 16], + } + IUT -> Lower Tester: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::StartEncryptionReq, + } + IUT -> Upper Tester: EncryptionChange { + status: ErrorCode::Success, + connection_handle: context.peer_handle(), + encryption_enabled: EncryptionEnabled::On, + } +} diff --git a/tools/rootcanal/lmp/test/ENC/BV-05-C.in b/tools/rootcanal/lmp/test/ENC/BV-05-C.in new file mode 100644 index 0000000000..b25d81ef90 --- /dev/null +++ b/tools/rootcanal/lmp/test/ENC/BV-05-C.in @@ -0,0 +1,40 @@ +sequence! { procedure, context, + // ACL Connection Established + Upper Tester -> IUT: SetConnectionEncryption { + connection_handle: context.peer_handle(), + encryption_enable: Enable::Enabled + } + IUT -> Upper Tester: SetConnectionEncryptionStatus { + num_hci_command_packets: 1, + status: ErrorCode::Success, + } + IUT -> Lower Tester: EncryptionModeReq { + transaction_id: 0, + encryption_mode: 0x01, + } + Lower Tester -> IUT: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::EncryptionModeReq, + } + IUT -> Lower Tester: EncryptionKeySizeReq { + transaction_id: 0, + key_size: 0x10, + } + Lower Tester -> IUT: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::EncryptionKeySizeReq, + } + IUT -> Lower Tester: StartEncryptionReq { + transaction_id: 0, + random_number: [0; 16], + } + Lower Tester -> IUT: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::StartEncryptionReq, + } + IUT -> Upper Tester: EncryptionChange { + status: ErrorCode::Success, + connection_handle: context.peer_handle(), + encryption_enabled: EncryptionEnabled::On, + } +} diff --git a/tools/rootcanal/lmp/test/ENC/BV-26-C.in b/tools/rootcanal/lmp/test/ENC/BV-26-C.in new file mode 100644 index 0000000000..01df56eb54 --- /dev/null +++ b/tools/rootcanal/lmp/test/ENC/BV-26-C.in @@ -0,0 +1,32 @@ +sequence! { procedure, context, + // ACL Connection Established + Lower Tester -> IUT: EncryptionModeReq { + transaction_id: 0, + encryption_mode: 0x01, + } + IUT ->Lower Tester: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::EncryptionModeReq, + } + Lower Tester -> IUT: EncryptionKeySizeReq { + transaction_id: 0, + key_size: 0x10, + } + IUT -> Lower Tester: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::EncryptionKeySizeReq, + } + Lower Tester -> IUT: StartEncryptionReq { + transaction_id: 0, + random_number: [0; 16], + } + IUT -> Lower Tester: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::StartEncryptionReq, + } + IUT -> Upper Tester: EncryptionChange { + status: ErrorCode::Success, + connection_handle: context.peer_handle(), + encryption_enabled: EncryptionEnabled::BrEdrAesCcm, + } +} diff --git a/tools/rootcanal/lmp/test/ENC/BV-34-C.in b/tools/rootcanal/lmp/test/ENC/BV-34-C.in new file mode 100644 index 0000000000..ea03d98702 --- /dev/null +++ b/tools/rootcanal/lmp/test/ENC/BV-34-C.in @@ -0,0 +1,40 @@ +sequence! { procedure, context, + // ACL Connection Established + Upper Tester -> IUT: SetConnectionEncryption { + connection_handle: context.peer_handle(), + encryption_enable: Enable::Enabled + } + IUT -> Upper Tester: SetConnectionEncryptionStatus { + num_hci_command_packets: 1, + status: ErrorCode::Success, + } + IUT -> Lower Tester: EncryptionModeReq { + transaction_id: 0, + encryption_mode: 0x01, + } + Lower Tester -> IUT: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::EncryptionModeReq, + } + IUT -> Lower Tester: EncryptionKeySizeReq { + transaction_id: 0, + key_size: 0x10, + } + Lower Tester -> IUT: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::EncryptionKeySizeReq, + } + IUT -> Lower Tester: StartEncryptionReq { + transaction_id: 0, + random_number: [0; 16], + } + Lower Tester -> IUT: Accepted { + transaction_id: 0, + accepted_opcode: Opcode::StartEncryptionReq, + } + IUT -> Upper Tester: EncryptionChange { + status: ErrorCode::Success, + connection_handle: context.peer_handle(), + encryption_enabled: EncryptionEnabled::BrEdrAesCcm, + } +} diff --git a/tools/rootcanal/model/controller/acl_connection.cc b/tools/rootcanal/model/controller/acl_connection.cc index e23b412f8c..35d2108e4b 100644 --- a/tools/rootcanal/model/controller/acl_connection.cc +++ b/tools/rootcanal/model/controller/acl_connection.cc @@ -20,11 +20,12 @@ namespace rootcanal { AclConnection::AclConnection(AddressWithType address, AddressWithType own_address, AddressWithType resolved_address, - Phy::Type phy_type) + Phy::Type phy_type, bluetooth::hci::Role role) : address_(address), own_address_(own_address), resolved_address_(resolved_address), type_(phy_type), + role_(role), last_packet_timestamp_(std::chrono::steady_clock::now()), timeout_(std::chrono::seconds(1)) {} diff --git a/tools/rootcanal/model/controller/acl_connection.h b/tools/rootcanal/model/controller/acl_connection.h index 40c8fcd6fc..c2c23c7eee 100644 --- a/tools/rootcanal/model/controller/acl_connection.h +++ b/tools/rootcanal/model/controller/acl_connection.h @@ -30,7 +30,8 @@ using ::bluetooth::hci::AddressWithType; class AclConnection { public: AclConnection(AddressWithType address, AddressWithType own_address, - AddressWithType resolved_address, Phy::Type phy_type); + AddressWithType resolved_address, Phy::Type phy_type, + bluetooth::hci::Role role); virtual ~AclConnection() = default; diff --git a/tools/rootcanal/model/controller/acl_connection_handler.cc b/tools/rootcanal/model/controller/acl_connection_handler.cc index a38b2724c7..9992121907 100644 --- a/tools/rootcanal/model/controller/acl_connection_handler.cc +++ b/tools/rootcanal/model/controller/acl_connection_handler.cc @@ -127,20 +127,22 @@ uint16_t AclConnectionHandler::CreateConnection(Address addr, AclConnection{ AddressWithType{addr, AddressType::PUBLIC_DEVICE_ADDRESS}, AddressWithType{own_addr, AddressType::PUBLIC_DEVICE_ADDRESS}, - AddressWithType(), Phy::Type::BR_EDR}); + AddressWithType(), Phy::Type::BR_EDR, + bluetooth::hci::Role::CENTRAL}); return handle; } return kReservedHandle; } uint16_t AclConnectionHandler::CreateLeConnection(AddressWithType addr, - AddressWithType own_addr) { + AddressWithType own_addr, + bluetooth::hci::Role role) { AddressWithType resolved_peer = pending_le_connection_resolved_address_; if (CancelPendingLeConnection(addr)) { uint16_t handle = GetUnusedHandle(); - acl_connections_.emplace( - handle, - AclConnection{addr, own_addr, resolved_peer, Phy::Type::LOW_ENERGY}); + acl_connections_.emplace(handle, + AclConnection{addr, own_addr, resolved_peer, + Phy::Type::LOW_ENERGY, role}); return handle; } return kReservedHandle; diff --git a/tools/rootcanal/model/controller/acl_connection_handler.h b/tools/rootcanal/model/controller/acl_connection_handler.h index d601728ec0..2b0de43a23 100644 --- a/tools/rootcanal/model/controller/acl_connection_handler.h +++ b/tools/rootcanal/model/controller/acl_connection_handler.h @@ -67,7 +67,8 @@ class AclConnectionHandler { uint16_t CreateConnection(bluetooth::hci::Address addr, bluetooth::hci::Address own_addr); uint16_t CreateLeConnection(bluetooth::hci::AddressWithType addr, - bluetooth::hci::AddressWithType own_addr); + bluetooth::hci::AddressWithType own_addr, + bluetooth::hci::Role role); bool Disconnect(uint16_t handle); bool HasHandle(uint16_t handle) const; bool HasScoHandle(uint16_t handle) const; diff --git a/tools/rootcanal/model/controller/controller_properties.cc b/tools/rootcanal/model/controller/controller_properties.cc index 41dfd8d1e0..7a5856eeb8 100644 --- a/tools/rootcanal/model/controller/controller_properties.cc +++ b/tools/rootcanal/model/controller/controller_properties.cc @@ -285,9 +285,6 @@ ControllerProperties::ControllerProperties(const std::string& file_name) ParseUint(root, "ManufacturerName", company_identifier); ParseHex64(root["LeSupportedFeatures"], &le_features); - ParseUint(root, "LeConnectListIgnoreReasons", le_connect_list_ignore_reasons); - ParseUint(root, "LeResolvingListIgnoreReasons", - le_resolving_list_ignore_reasons); // Configuration options. @@ -310,6 +307,7 @@ ControllerProperties::ControllerProperties(const std::string& file_name) ParseUint(root, "total_num_le_acl_data_packets ", total_num_le_acl_data_packets); ParseUint(root, "total_num_iso_data_packets ", total_num_iso_data_packets); + ParseUint(root, "num_supported_iac", num_supported_iac); ParseUintArray(root, "lmp_features", lmp_features); ParseUintVector(root, "supported_standard_codecs", supported_standard_codecs); diff --git a/tools/rootcanal/model/controller/controller_properties.h b/tools/rootcanal/model/controller/controller_properties.h index 9f551fd34f..5ec91a6dcd 100644 --- a/tools/rootcanal/model/controller/controller_properties.h +++ b/tools/rootcanal/model/controller/controller_properties.h @@ -76,6 +76,9 @@ struct ControllerProperties { uint8_t total_num_le_acl_data_packets{20}; uint8_t total_num_iso_data_packets{12}; + // Number of Supported IAC (Vol 4, Part E § 7.3.43). + uint8_t num_supported_iac{4}; + // Supported Codecs (Vol 4, Part E § 7.4.8). // Implements the [v1] version only. std::vector<uint8_t> supported_standard_codecs{0}; @@ -93,15 +96,6 @@ struct ControllerProperties { // Vendor Information. // Provide parameters returned by vendor specific commands. std::vector<uint8_t> le_vendor_capabilities{}; - - // LE Workarounds - uint16_t le_connect_list_ignore_reasons{0}; - uint16_t le_resolving_list_ignore_reasons{0}; - - // Workaround for misbehaving stacks - static constexpr uint8_t kLeListIgnoreScanEnable = 0x1; - static constexpr uint8_t kLeListIgnoreConnections = 0x2; - static constexpr uint8_t kLeListIgnoreAdvertising = 0x4; }; } // namespace rootcanal diff --git a/tools/rootcanal/model/controller/dual_mode_controller.cc b/tools/rootcanal/model/controller/dual_mode_controller.cc index 9a26337c95..c1a58fc4f8 100644 --- a/tools/rootcanal/model/controller/dual_mode_controller.cc +++ b/tools/rootcanal/model/controller/dual_mode_controller.cc @@ -1494,7 +1494,7 @@ void DualModeController::ReadPageTimeout(CommandView command) { auto command_view = gd_hci::ReadPageTimeoutView::Create( gd_hci::DiscoveryCommandView::Create(command)); ASSERT(command_view.IsValid()); - uint16_t page_timeout = 0x2000; + uint16_t page_timeout = link_layer_controller_.GetPageTimeout(); send_event_(bluetooth::hci::ReadPageTimeoutCompleteBuilder::Create( kNumCommandPackets, ErrorCode::SUCCESS, page_timeout)); } @@ -1503,6 +1503,7 @@ void DualModeController::WritePageTimeout(CommandView command) { auto command_view = gd_hci::WritePageTimeoutView::Create( gd_hci::DiscoveryCommandView::Create(command)); ASSERT(command_view.IsValid()); + link_layer_controller_.SetPageTimeout(command_view.GetPageTimeout()); send_event_(bluetooth::hci::WritePageTimeoutCompleteBuilder::Create( kNumCommandPackets, ErrorCode::SUCCESS)); } @@ -1744,25 +1745,24 @@ void DualModeController::ReadNumberOfSupportedIac(CommandView command) { auto command_view = gd_hci::ReadNumberOfSupportedIacView::Create( gd_hci::DiscoveryCommandView::Create(command)); ASSERT(command_view.IsValid()); - uint8_t num_support_iac = 0x1; send_event_(bluetooth::hci::ReadNumberOfSupportedIacCompleteBuilder::Create( - kNumCommandPackets, ErrorCode::SUCCESS, num_support_iac)); + kNumCommandPackets, ErrorCode::SUCCESS, properties_.num_supported_iac)); } void DualModeController::ReadCurrentIacLap(CommandView command) { auto command_view = gd_hci::ReadCurrentIacLapView::Create( gd_hci::DiscoveryCommandView::Create(command)); ASSERT(command_view.IsValid()); - gd_hci::Lap lap; - lap.lap_ = 0x30; send_event_(bluetooth::hci::ReadCurrentIacLapCompleteBuilder::Create( - kNumCommandPackets, ErrorCode::SUCCESS, {lap})); + kNumCommandPackets, ErrorCode::SUCCESS, + link_layer_controller_.ReadCurrentIacLap())); } void DualModeController::WriteCurrentIacLap(CommandView command) { auto command_view = gd_hci::WriteCurrentIacLapView::Create( gd_hci::DiscoveryCommandView::Create(command)); ASSERT(command_view.IsValid()); + link_layer_controller_.WriteCurrentIacLap(command_view.GetLapsToWrite()); send_event_(bluetooth::hci::WriteCurrentIacLapCompleteBuilder::Create( kNumCommandPackets, ErrorCode::SUCCESS)); } @@ -1807,8 +1807,19 @@ void DualModeController::ReadScanEnable(CommandView command) { auto command_view = gd_hci::ReadScanEnableView::Create( gd_hci::DiscoveryCommandView::Create(command)); ASSERT(command_view.IsValid()); + + bool inquiry_scan = link_layer_controller_.GetInquiryScanEnable(); + bool page_scan = link_layer_controller_.GetPageScanEnable(); + + bluetooth::hci::ScanEnable scan_enable = + inquiry_scan && page_scan + ? bluetooth::hci::ScanEnable::INQUIRY_AND_PAGE_SCAN + : inquiry_scan ? bluetooth::hci::ScanEnable::INQUIRY_SCAN_ONLY + : page_scan ? bluetooth::hci::ScanEnable::PAGE_SCAN_ONLY + : bluetooth::hci::ScanEnable::NO_SCANS; + send_event_(bluetooth::hci::ReadScanEnableCompleteBuilder::Create( - kNumCommandPackets, ErrorCode::SUCCESS, gd_hci::ScanEnable::NO_SCANS)); + kNumCommandPackets, ErrorCode::SUCCESS, scan_enable)); } void DualModeController::WriteScanEnable(CommandView command) { @@ -2041,7 +2052,7 @@ void DualModeController::LeSetAddressResolutionEnable(CommandView command) { gd_hci::LeSecurityCommandView::Create( gd_hci::SecurityCommandView::Create(command))); ASSERT(command_view.IsValid()); - auto status = link_layer_controller_.LeSetAddressResolutionEnable( + ErrorCode status = link_layer_controller_.LeSetAddressResolutionEnable( command_view.GetAddressResolutionEnable() == bluetooth::hci::Enable::ENABLED); send_event_( @@ -2097,7 +2108,7 @@ void DualModeController::LeSetAdvertisingParameters(CommandView command) { static_cast<uint8_t>(command_view.GetOwnAddressType()), static_cast<uint8_t>(command_view.GetPeerAddressType()), peer_address, command_view.GetAdvertisingChannelMap(), - static_cast<uint8_t>(command_view.GetAdvertisingFilterPolicy())); + command_view.GetAdvertisingFilterPolicy()); send_event_(bluetooth::hci::LeSetAdvertisingParametersCompleteBuilder::Create( kNumCommandPackets, ErrorCode::SUCCESS)); @@ -2165,7 +2176,7 @@ void DualModeController::LeSetScanParameters(CommandView command) { link_layer_controller_.SetLeScanWindow(command_view.GetLeScanWindow()); link_layer_controller_.SetLeAddressType(command_view.GetOwnAddressType()); link_layer_controller_.SetLeScanFilterPolicy( - static_cast<uint8_t>(command_view.GetScanningFilterPolicy())); + command_view.GetScanningFilterPolicy()); send_event_(bluetooth::hci::LeSetScanParametersCompleteBuilder::Create( kNumCommandPackets, ErrorCode::SUCCESS)); } @@ -2194,13 +2205,15 @@ void DualModeController::LeCreateConnection(CommandView command) { gd_hci::LeConnectionManagementCommandView::Create( gd_hci::AclCommandView::Create(command))); ASSERT(command_view.IsValid()); + auto initiator_filter_policy = command_view.GetInitiatorFilterPolicy(); + link_layer_controller_.SetLeScanInterval(command_view.GetLeScanInterval()); link_layer_controller_.SetLeScanWindow(command_view.GetLeScanWindow()); - uint8_t initiator_filter_policy = - static_cast<uint8_t>(command_view.GetInitiatorFilterPolicy()); link_layer_controller_.SetLeInitiatorFilterPolicy(initiator_filter_policy); - if (initiator_filter_policy == 0) { // Connect list not used + if (initiator_filter_policy == + bluetooth::hci::InitiatorFilterPolicy::USE_PEER_ADDRESS) { + // Connect list not used uint8_t peer_address_type = static_cast<uint8_t>(command_view.GetPeerAddressType()); Address peer_address = command_view.GetPeerAddress(); @@ -2326,9 +2339,9 @@ void DualModeController::LeClearFilterAcceptList(CommandView command) { gd_hci::LeConnectionManagementCommandView::Create( gd_hci::AclCommandView::Create(command))); ASSERT(command_view.IsValid()); - link_layer_controller_.LeFilterAcceptListClear(); + ErrorCode status = link_layer_controller_.LeClearFilterAcceptList(); send_event_(bluetooth::hci::LeClearFilterAcceptListCompleteBuilder::Create( - kNumCommandPackets, ErrorCode::SUCCESS)); + kNumCommandPackets, status)); } void DualModeController::LeAddDeviceToFilterAcceptList(CommandView command) { @@ -2336,17 +2349,11 @@ void DualModeController::LeAddDeviceToFilterAcceptList(CommandView command) { gd_hci::LeConnectionManagementCommandView::Create( gd_hci::AclCommandView::Create(command))); ASSERT(command_view.IsValid()); - - ErrorCode result = ErrorCode::INVALID_HCI_COMMAND_PARAMETERS; - if (command_view.GetAddressType() != - bluetooth::hci::FilterAcceptListAddressType::ANONYMOUS_ADVERTISERS) { - result = link_layer_controller_.LeFilterAcceptListAddDevice( - command_view.GetAddress(), static_cast<bluetooth::hci::AddressType>( - command_view.GetAddressType())); - } + ErrorCode status = link_layer_controller_.LeAddDeviceToFilterAcceptList( + command_view.GetAddressType(), command_view.GetAddress()); send_event_( bluetooth::hci::LeAddDeviceToFilterAcceptListCompleteBuilder::Create( - kNumCommandPackets, result)); + kNumCommandPackets, status)); } void DualModeController::LeRemoveDeviceFromFilterAcceptList( @@ -2355,16 +2362,8 @@ void DualModeController::LeRemoveDeviceFromFilterAcceptList( gd_hci::LeConnectionManagementCommandView::Create( gd_hci::AclCommandView::Create(command))); ASSERT(command_view.IsValid()); - - ErrorCode status = ErrorCode::SUCCESS; - if (command_view.GetAddressType() != - bluetooth::hci::FilterAcceptListAddressType::ANONYMOUS_ADVERTISERS) { - link_layer_controller_.LeFilterAcceptListAddDevice( - command_view.GetAddress(), static_cast<bluetooth::hci::AddressType>( - command_view.GetAddressType())); - } else { - status = ErrorCode::INVALID_HCI_COMMAND_PARAMETERS; - } + ErrorCode status = link_layer_controller_.LeRemoveDeviceFromFilterAcceptList( + command_view.GetAddressType(), command_view.GetAddress()); send_event_( bluetooth::hci::LeRemoveDeviceFromFilterAcceptListCompleteBuilder::Create( kNumCommandPackets, status)); @@ -2374,9 +2373,9 @@ void DualModeController::LeClearResolvingList(CommandView command) { auto command_view = gd_hci::LeClearResolvingListView::Create( gd_hci::LeSecurityCommandView::Create(command)); ASSERT(command_view.IsValid()); - link_layer_controller_.LeResolvingListClear(); + ErrorCode status = link_layer_controller_.LeClearResolvingList(); send_event_(bluetooth::hci::LeClearResolvingListCompleteBuilder::Create( - kNumCommandPackets, ErrorCode::SUCCESS)); + kNumCommandPackets, status)); } void DualModeController::LeReadResolvingListSize(CommandView command) { @@ -2438,17 +2437,17 @@ void DualModeController::LeAddDeviceToResolvingList(CommandView command) { auto command_view = gd_hci::LeAddDeviceToResolvingListView::Create( gd_hci::LeSecurityCommandView::Create(command)); ASSERT(command_view.IsValid()); - AddressType peer_address_type; + AddressType peer_identity_address_type; switch (command_view.GetPeerIdentityAddressType()) { case bluetooth::hci::PeerAddressType::PUBLIC_DEVICE_OR_IDENTITY_ADDRESS: - peer_address_type = AddressType::PUBLIC_DEVICE_ADDRESS; + peer_identity_address_type = AddressType::PUBLIC_DEVICE_ADDRESS; break; case bluetooth::hci::PeerAddressType::RANDOM_DEVICE_OR_IDENTITY_ADDRESS: - peer_address_type = AddressType::RANDOM_DEVICE_ADDRESS; + peer_identity_address_type = AddressType::RANDOM_DEVICE_ADDRESS; break; } - auto status = link_layer_controller_.LeResolvingListAddDevice( - command_view.GetPeerIdentityAddress(), peer_address_type, + ErrorCode status = link_layer_controller_.LeAddDeviceToResolvingList( + peer_identity_address_type, command_view.GetPeerIdentityAddress(), command_view.GetPeerIrk(), command_view.GetLocalIrk()); send_event_(bluetooth::hci::LeAddDeviceToResolvingListCompleteBuilder::Create( kNumCommandPackets, status)); @@ -2459,20 +2458,20 @@ void DualModeController::LeRemoveDeviceFromResolvingList(CommandView command) { gd_hci::LeSecurityCommandView::Create(command)); ASSERT(command_view.IsValid()); - AddressType peer_address_type; + AddressType peer_identity_address_type; switch (command_view.GetPeerIdentityAddressType()) { case bluetooth::hci::PeerAddressType::PUBLIC_DEVICE_OR_IDENTITY_ADDRESS: - peer_address_type = AddressType::PUBLIC_DEVICE_ADDRESS; + peer_identity_address_type = AddressType::PUBLIC_DEVICE_ADDRESS; break; case bluetooth::hci::PeerAddressType::RANDOM_DEVICE_OR_IDENTITY_ADDRESS: - peer_address_type = AddressType::RANDOM_DEVICE_ADDRESS; + peer_identity_address_type = AddressType::RANDOM_DEVICE_ADDRESS; break; } - link_layer_controller_.LeResolvingListRemoveDevice( - command_view.GetPeerIdentityAddress(), peer_address_type); + ErrorCode status = link_layer_controller_.LeRemoveDeviceFromResolvingList( + peer_identity_address_type, command_view.GetPeerIdentityAddress()); send_event_( bluetooth::hci::LeRemoveDeviceFromResolvingListCompleteBuilder::Create( - kNumCommandPackets, ErrorCode::SUCCESS)); + kNumCommandPackets, status)); } void DualModeController::LeSetExtendedScanParameters(CommandView command) { @@ -2492,7 +2491,7 @@ void DualModeController::LeSetExtendedScanParameters(CommandView command) { link_layer_controller_.SetLeScanWindow(parameters[0].le_scan_window_); link_layer_controller_.SetLeAddressType(command_view.GetOwnAddressType()); link_layer_controller_.SetLeScanFilterPolicy( - static_cast<uint8_t>(command_view.GetScanningFilterPolicy())); + command_view.GetScanningFilterPolicy()); } else { status = ErrorCode::COMMAND_DISALLOWED; } @@ -2524,11 +2523,11 @@ void DualModeController::LeExtendedCreateConnection(CommandView command) { ASSERT(command_view.IsValid()); ASSERT_LOG(command_view.GetInitiatingPhys() == 1, "Only LE_1M is supported"); auto params = command_view.GetPhyScanParameters(); + auto initiator_filter_policy = command_view.GetInitiatorFilterPolicy(); + link_layer_controller_.SetLeScanInterval(params[0].scan_interval_); link_layer_controller_.SetLeScanWindow(params[0].scan_window_); - auto initiator_filter_policy = command_view.GetInitiatorFilterPolicy(); - link_layer_controller_.SetLeInitiatorFilterPolicy( - static_cast<uint8_t>(initiator_filter_policy)); + link_layer_controller_.SetLeInitiatorFilterPolicy(initiator_filter_policy); if (initiator_filter_policy == gd_hci::InitiatorFilterPolicy::USE_PEER_ADDRESS) { @@ -2571,7 +2570,7 @@ void DualModeController::LeSetPrivacyMode(CommandView command) { break; } if (link_layer_controller_.LeResolvingListContainsDevice( - peer_identity_address, peer_identity_address_type)) { + peer_identity_address_type, peer_identity_address)) { link_layer_controller_.LeSetPrivacyMode( peer_identity_address_type, peer_identity_address, privacy_mode); } diff --git a/tools/rootcanal/model/controller/le_advertiser.cc b/tools/rootcanal/model/controller/le_advertiser.cc index 412f4ca562..3dac3771a1 100644 --- a/tools/rootcanal/model/controller/le_advertiser.cc +++ b/tools/rootcanal/model/controller/le_advertiser.cc @@ -23,7 +23,7 @@ namespace rootcanal { void LeAdvertiser::Initialize(OwnAddressType address_type, AddressWithType public_address, AddressWithType peer_address, - LeScanningFilterPolicy filter_policy, + AdvertisingFilterPolicy filter_policy, AdvertisingType type, const std::vector<uint8_t>& advertisement, const std::vector<uint8_t>& scan_response, @@ -44,7 +44,7 @@ void LeAdvertiser::Initialize(OwnAddressType address_type, void LeAdvertiser::InitializeExtended( unsigned advertising_handle, OwnAddressType address_type, AddressWithType public_address, AddressWithType peer_address, - LeScanningFilterPolicy filter_policy, AdvertisingType type, + AdvertisingFilterPolicy filter_policy, AdvertisingType type, std::chrono::steady_clock::duration interval, uint8_t tx_power, const std::function<bluetooth::hci::Address()>& get_address) { get_address_ = get_address; @@ -64,7 +64,7 @@ void LeAdvertiser::InitializeExtended( void LeAdvertiser::Clear() { address_ = AddressWithType{}; peer_address_ = AddressWithType{}; - filter_policy_ = LeScanningFilterPolicy::ACCEPT_ALL; + filter_policy_ = AdvertisingFilterPolicy::ALL_DEVICES; type_ = AdvertisingType::ADV_IND; advertisement_.clear(); scan_response_.clear(); @@ -256,24 +256,25 @@ LeAdvertiser::GetAdvertisement(std::chrono::steady_clock::time_point now) { std::unique_ptr<model::packets::LinkLayerPacketBuilder> LeAdvertiser::GetScanResponse(bluetooth::hci::Address scanned, - bluetooth::hci::Address scanner) { + bluetooth::hci::Address scanner, + bool scanner_in_filter_accept_list) { + (void)scanner; if (scanned != address_.GetAddress() || !enabled_) { return nullptr; } + switch (filter_policy_) { - case bluetooth::hci::LeScanningFilterPolicy:: - FILTER_ACCEPT_LIST_AND_INITIATORS_IDENTITY: - case bluetooth::hci::LeScanningFilterPolicy::FILTER_ACCEPT_LIST_ONLY: - LOG_WARN("ScanResponses don't handle connect list filters"); - return nullptr; - case bluetooth::hci::LeScanningFilterPolicy::CHECK_INITIATORS_IDENTITY: - if (scanner != peer_address_.GetAddress()) { + case bluetooth::hci::AdvertisingFilterPolicy::ALL_DEVICES: + case bluetooth::hci::AdvertisingFilterPolicy::LISTED_CONNECT: + break; + case bluetooth::hci::AdvertisingFilterPolicy::LISTED_SCAN: + case bluetooth::hci::AdvertisingFilterPolicy::LISTED_SCAN_AND_CONNECT: + if (!scanner_in_filter_accept_list) { return nullptr; } break; - case bluetooth::hci::LeScanningFilterPolicy::ACCEPT_ALL: - break; } + if (tx_power_ == kTxPowerUnavailable) { return model::packets::LeScanResponseBuilder::Create( address_.GetAddress(), peer_address_.GetAddress(), diff --git a/tools/rootcanal/model/controller/le_advertiser.h b/tools/rootcanal/model/controller/le_advertiser.h index bfebe1cd02..a90d2c5955 100644 --- a/tools/rootcanal/model/controller/le_advertiser.h +++ b/tools/rootcanal/model/controller/le_advertiser.h @@ -35,7 +35,7 @@ class LeAdvertiser { void Initialize(bluetooth::hci::OwnAddressType address_type, bluetooth::hci::AddressWithType public_address, bluetooth::hci::AddressWithType peer_address, - bluetooth::hci::LeScanningFilterPolicy filter_policy, + bluetooth::hci::AdvertisingFilterPolicy filter_policy, bluetooth::hci::AdvertisingType type, const std::vector<uint8_t>& advertisement, const std::vector<uint8_t>& scan_response, @@ -45,7 +45,7 @@ class LeAdvertiser { unsigned advertising_handle, bluetooth::hci::OwnAddressType address_type, bluetooth::hci::AddressWithType public_address, bluetooth::hci::AddressWithType peer_address, - bluetooth::hci::LeScanningFilterPolicy filter_policy, + bluetooth::hci::AdvertisingFilterPolicy filter_policy, bluetooth::hci::AdvertisingType type, std::chrono::steady_clock::duration interval, uint8_t tx_power, const std::function<bluetooth::hci::Address()>& get_address); @@ -67,13 +67,18 @@ class LeAdvertiser { std::unique_ptr<model::packets::LinkLayerPacketBuilder> GetScanResponse( bluetooth::hci::Address scanned_address, - bluetooth::hci::Address scanner_address); + bluetooth::hci::Address scanner_address, + bool scanner_in_filter_accept_list); void Clear(); void Disable(); void Enable(); void EnableExtended(std::chrono::milliseconds duration); + bluetooth::hci::AdvertisingFilterPolicy GetAdvertisingFilterPolicy() const { + return filter_policy_; + } + bool IsEnabled() const; bool IsExtended() const; bool IsConnectable() const; @@ -91,7 +96,7 @@ class LeAdvertiser { bluetooth::hci::OwnAddressType own_address_type_; bluetooth::hci::AddressWithType peer_address_{}; // For directed advertisements - bluetooth::hci::LeScanningFilterPolicy filter_policy_{}; + bluetooth::hci::AdvertisingFilterPolicy filter_policy_{}; bluetooth::hci::AdvertisingType type_{}; std::vector<uint8_t> advertisement_; std::vector<uint8_t> scan_response_; diff --git a/tools/rootcanal/model/controller/link_layer_controller.cc b/tools/rootcanal/model/controller/link_layer_controller.cc index 00a7fcaec7..698b40f300 100644 --- a/tools/rootcanal/model/controller/link_layer_controller.cc +++ b/tools/rootcanal/model/controller/link_layer_controller.cc @@ -67,6 +67,124 @@ bool LinkLayerController::IsLeEventUnmasked(SubeventCode subevent) const { (le_event_mask_ & bit) != 0; } +bool LinkLayerController::FilterAcceptListBusy() { + // Filter Accept List cannot be modified when + // • any advertising filter policy uses the Filter Accept List and + // advertising is enabled, + for (auto const& advertiser : advertisers_) { + if (advertiser.IsEnabled() && + advertiser.GetAdvertisingFilterPolicy() != + bluetooth::hci::AdvertisingFilterPolicy::ALL_DEVICES) { + return true; + } + } + + // • the scanning filter policy uses the Filter Accept List and scanning + // is enabled, + if (le_scan_enable_ != bluetooth::hci::OpCode::NONE && + (le_scan_filter_policy_ == + bluetooth::hci::LeScanningFilterPolicy::FILTER_ACCEPT_LIST_ONLY || + le_scan_filter_policy_ == + bluetooth::hci::LeScanningFilterPolicy:: + FILTER_ACCEPT_LIST_AND_INITIATORS_IDENTITY)) { + return true; + } + + // • the initiator filter policy uses the Filter Accept List and an + // HCI_LE_Create_Connection or HCI_LE_Extended_Create_Connection + // command is pending. + if (le_connect_ && + le_initiator_filter_policy_ == + bluetooth::hci::InitiatorFilterPolicy::USE_FILTER_ACCEPT_LIST) { + return true; + } + + return false; +} + +bool LinkLayerController::LeFilterAcceptListContainsDevice( + FilterAcceptListAddressType address_type, Address address) { + for (auto const& entry : le_filter_accept_list_) { + if (entry.address_type == address_type && + (address_type == FilterAcceptListAddressType::ANONYMOUS_ADVERTISERS || + entry.address == address)) { + return true; + } + } + + return false; +} + +bool LinkLayerController::LeFilterAcceptListContainsDevice( + AddressType address_type, Address address) { + FilterAcceptListAddressType filter_accept_list_address_type; + switch (address_type) { + case AddressType::PUBLIC_DEVICE_ADDRESS: + case AddressType::PUBLIC_IDENTITY_ADDRESS: + filter_accept_list_address_type = FilterAcceptListAddressType::PUBLIC; + break; + case AddressType::RANDOM_DEVICE_ADDRESS: + case AddressType::RANDOM_IDENTITY_ADDRESS: + filter_accept_list_address_type = FilterAcceptListAddressType::RANDOM; + break; + } + + return LeFilterAcceptListContainsDevice(filter_accept_list_address_type, + address); +} + +bool LinkLayerController::ResolvingListBusy() { + // The resolving list cannot be modified when + // • Advertising (other than periodic advertising) is enabled, + for (auto const& advertiser : advertisers_) { + if (advertiser.IsEnabled()) { + return true; + } + } + + // • Scanning is enabled, + if (le_scan_enable_ != bluetooth::hci::OpCode::NONE) { + return true; + } + + // • an HCI_LE_Create_Connection, HCI_LE_Extended_Create_Connection, or + // HCI_LE_Periodic_Advertising_Create_Sync command is pending. + if (le_connect_) { + return true; + } + + return false; +} + +bool LinkLayerController::LeResolvingListContainsDevice( + AddressType peer_identity_address_type, Address peer_identity_address) { + for (auto const& entry : le_resolving_list_) { + if (entry.peer_identity_address_type == peer_identity_address_type && + entry.peer_identity_address == peer_identity_address) { + return true; + } + } + + return false; +} + +bool LinkLayerController::LeResolvingListContainsDevice( + bluetooth::hci::PeerAddressType peer_identity_address_type, + Address peer_identity_address) { + AddressType peer_address_type; + switch (peer_identity_address_type) { + case bluetooth::hci::PeerAddressType::PUBLIC_DEVICE_OR_IDENTITY_ADDRESS: + peer_address_type = AddressType::PUBLIC_DEVICE_ADDRESS; + break; + case bluetooth::hci::PeerAddressType::RANDOM_DEVICE_OR_IDENTITY_ADDRESS: + peer_address_type = AddressType::RANDOM_DEVICE_ADDRESS; + break; + } + + return LeResolvingListContainsDevice(peer_address_type, + peer_identity_address); +} + // ============================================================================= // General LE Coommands // ============================================================================= @@ -130,6 +248,213 @@ ErrorCode LinkLayerController::LeSetHostFeature(uint8_t bit_number, return ErrorCode::SUCCESS; } +// ============================================================================= +// LE Resolving List +// ============================================================================= + +// HCI command LE_Add_Device_To_Resolving_List (Vol 4, Part E § 7.8.38). +ErrorCode LinkLayerController::LeAddDeviceToResolvingList( + AddressType peer_identity_address_type, Address peer_identity_address, + std::array<uint8_t, kIrkSize> peer_irk, + std::array<uint8_t, kIrkSize> local_irk) { + // This command shall not be used when address resolution is enabled in the + // Controller and: + // • Advertising (other than periodic advertising) is enabled, + // • Scanning is enabled, or + // • an HCI_LE_Create_Connection, HCI_LE_Extended_Create_Connection, or + // HCI_LE_Periodic_Advertising_Create_Sync command is pending. + if (le_resolving_list_enabled_ && ResolvingListBusy()) { + LOG_INFO( + "device is currently advertising, scanning, or establishing an" + " LE connection"); + return ErrorCode::COMMAND_DISALLOWED; + } + + // When a Controller cannot add a device to the list because there is no space + // available, it shall return the error code Memory Capacity Exceeded (0x07). + if (le_resolving_list_.size() >= properties_.le_resolving_list_size) { + LOG_INFO("resolving list is full"); + return ErrorCode::MEMORY_CAPACITY_EXCEEDED; + } + + // If there is an existing entry in the resolving list with the same + // Peer_Identity_Address and Peer_Identity_Address_Type, or with the same + // Peer_IRK, the Controller should return the error code Invalid HCI Command + // Parameters (0x12). + for (auto const& entry : le_resolving_list_) { + if ((entry.peer_identity_address_type == peer_identity_address_type && + entry.peer_identity_address == peer_identity_address) || + entry.peer_irk == peer_irk) { + LOG_INFO("device is already present in the resolving list"); + return ErrorCode::INVALID_HCI_COMMAND_PARAMETERS; + } + } + + le_resolving_list_.emplace_back(ResolvingListEntry{ + peer_identity_address_type, peer_identity_address, peer_irk, local_irk}); + return ErrorCode::SUCCESS; +} + +// HCI command LE_Remove_Device_From_Resolving_List (Vol 4, Part E § 7.8.39). +ErrorCode LinkLayerController::LeRemoveDeviceFromResolvingList( + AddressType peer_identity_address_type, Address peer_identity_address) { + // This command shall not be used when address resolution is enabled in the + // Controller and: + // • Advertising (other than periodic advertising) is enabled, + // • Scanning is enabled, or + // • an HCI_LE_Create_Connection, HCI_LE_Extended_Create_Connection, or + // HCI_LE_Periodic_Advertising_Create_Sync command is pending. + if (le_resolving_list_enabled_ && ResolvingListBusy()) { + LOG_INFO( + "device is currently advertising, scanning, or establishing an" + " LE connection"); + return ErrorCode::COMMAND_DISALLOWED; + } + + for (auto it = le_resolving_list_.begin(); it != le_resolving_list_.end(); + it++) { + if (it->peer_identity_address_type == peer_identity_address_type && + it->peer_identity_address == peer_identity_address) { + le_resolving_list_.erase(it); + return ErrorCode::SUCCESS; + } + } + + // When a Controller cannot remove a device from the resolving list because + // it is not found, it shall return the error code + // Unknown Connection Identifier (0x02). + LOG_INFO("peer address not found in the resolving list"); + return ErrorCode::UNKNOWN_CONNECTION; +} + +// HCI command LE_Clear_Resolving_List (Vol 4, Part E § 7.8.40). +ErrorCode LinkLayerController::LeClearResolvingList() { + // This command shall not be used when address resolution is enabled in the + // Controller and: + // • Advertising (other than periodic advertising) is enabled, + // • Scanning is enabled, or + // • an HCI_LE_Create_Connection, HCI_LE_Extended_Create_Connection, or + // HCI_LE_Periodic_Advertising_Create_Sync command is pending. + if (le_resolving_list_enabled_ && ResolvingListBusy()) { + LOG_INFO( + "device is currently advertising, scanning," + " or establishing an LE connection"); + return ErrorCode::COMMAND_DISALLOWED; + } + + le_resolving_list_.clear(); + return ErrorCode::SUCCESS; +} + +// HCI command LE_Set_Address_Resolution_Enable (Vol 4, Part E § 7.8.44). +ErrorCode LinkLayerController::LeSetAddressResolutionEnable(bool enable) { + // This command shall not be used when: + // • Advertising (other than periodic advertising) is enabled, + // • Scanning is enabled, or + // • an HCI_LE_Create_Connection, HCI_LE_Extended_Create_Connection, or + // HCI_LE_Periodic_Advertising_Create_Sync command is pending. + if (ResolvingListBusy()) { + LOG_INFO( + "device is currently advertising, scanning," + " or establishing an LE connection"); + return ErrorCode::COMMAND_DISALLOWED; + } + + le_resolving_list_enabled_ = enable; + return ErrorCode::SUCCESS; +} + +// ============================================================================= +// LE Filter Accept List +// ============================================================================= + +// HCI command LE_Clear_Filter_Accept_List (Vol 4, Part E § 7.8.15). +ErrorCode LinkLayerController::LeClearFilterAcceptList() { + // This command shall not be used when: + // • any advertising filter policy uses the Filter Accept List and + // advertising is enabled, + // • the scanning filter policy uses the Filter Accept List and scanning + // is enabled, or + // • the initiator filter policy uses the Filter Accept List and an + // HCI_LE_Create_Connection or HCI_LE_Extended_Create_Connection + // command is pending. + if (FilterAcceptListBusy()) { + LOG_INFO( + "device is currently advertising, scanning," + " or establishing an LE connection using the filter accept list"); + return ErrorCode::COMMAND_DISALLOWED; + } + + le_filter_accept_list_.clear(); + return ErrorCode::SUCCESS; +} + +// HCI command LE_Add_Device_To_Filter_Accept_List (Vol 4, Part E § 7.8.16). +ErrorCode LinkLayerController::LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType address_type, Address address) { + // This command shall not be used when: + // • any advertising filter policy uses the Filter Accept List and + // advertising is enabled, + // • the scanning filter policy uses the Filter Accept List and scanning + // is enabled, or + // • the initiator filter policy uses the Filter Accept List and an + // HCI_LE_Create_Connection or HCI_LE_Extended_Create_Connection + // command is pending. + if (FilterAcceptListBusy()) { + LOG_INFO( + "device is currently advertising, scanning," + " or establishing an LE connection using the filter accept list"); + return ErrorCode::COMMAND_DISALLOWED; + } + + // When a Controller cannot add a device to the Filter Accept List + // because there is no space available, it shall return the error code + // Memory Capacity Exceeded (0x07). + if (le_filter_accept_list_.size() >= properties_.le_filter_accept_list_size) { + LOG_INFO("filter accept list is full"); + return ErrorCode::MEMORY_CAPACITY_EXCEEDED; + } + + le_filter_accept_list_.emplace_back( + FilterAcceptListEntry{address_type, address}); + return ErrorCode::SUCCESS; +} + +// HCI command LE_Remove_Device_From_Filter_Accept_List (Vol 4, Part E +// § 7.8.17). +ErrorCode LinkLayerController::LeRemoveDeviceFromFilterAcceptList( + FilterAcceptListAddressType address_type, Address address) { + // This command shall not be used when: + // • any advertising filter policy uses the Filter Accept List and + // advertising is enabled, + // • the scanning filter policy uses the Filter Accept List and scanning + // is enabled, or + // • the initiator filter policy uses the Filter Accept List and an + // HCI_LE_Create_Connection or HCI_LE_Extended_Create_Connection + // command is pending. + if (FilterAcceptListBusy()) { + LOG_INFO( + "device is currently advertising, scanning," + " or establishing an LE connection using the filter accept list"); + return ErrorCode::COMMAND_DISALLOWED; + } + + for (auto it = le_filter_accept_list_.begin(); + it != le_filter_accept_list_.end(); it++) { + // Address shall be ignored when Address_Type is set to 0xFF. + if (it->address_type == address_type && + (address_type == FilterAcceptListAddressType::ANONYMOUS_ADVERTISERS || + it->address == address)) { + le_filter_accept_list_.erase(it); + return ErrorCode::SUCCESS; + } + } + + // Note: this case is not documented. + LOG_INFO("address not found in the filter accept list"); + return ErrorCode::SUCCESS; +} + void LinkLayerController::SetSecureSimplePairingSupport(bool enable) { uint64_t bit = 0x1; secure_simple_pairing_host_support_ = enable; @@ -171,7 +496,7 @@ void LinkLayerController::SetLeAdvertisingParameters( uint16_t interval_min, uint16_t interval_max, bluetooth::hci::AdvertisingType advertising_type, uint8_t own_address_type, uint8_t peer_address_type, Address peer_address, uint8_t channel_map, - uint8_t filter_policy) { + bluetooth::hci::AdvertisingFilterPolicy filter_policy) { le_advertising_type_ = advertising_type; le_advertising_interval_min_ = interval_min; le_advertising_interval_max_ = interval_max; @@ -967,6 +1292,14 @@ void LinkLayerController::IncomingInquiryPacket( ASSERT(inquiry.IsValid()); Address peer = incoming.GetSourceAddress(); + uint8_t lap = inquiry.GetLap(); + + // Filter out inquiry packets with IAC not present in the + // list Current_IAC_LAP. + if (std::none_of(current_iac_lap_list_.cbegin(), current_iac_lap_list_.cend(), + [lap](auto iac_lap) { return iac_lap.lap_ == lap; })) { + return; + } switch (inquiry.GetInquiryType()) { case (model::packets::InquiryType::STANDARD): { @@ -1596,10 +1929,10 @@ void LinkLayerController::IncomingLeLegacyAdvertisingPdu( for (const auto& entry : le_resolving_list_) { if (rpa_matches_irk(advertising_address, entry.peer_irk)) { LOG_INFO("Matched against IRK for %s", - entry.address.ToString().c_str()); + entry.peer_identity_address.ToString().c_str()); resolved = true; - resolved_address = entry.address; - resolved_address_type = entry.address_type; + resolved_address = entry.peer_identity_address; + resolved_address_type = entry.peer_identity_address_type; rpa = generate_rpa(entry.local_irk); } } @@ -1609,11 +1942,11 @@ void LinkLayerController::IncomingLeLegacyAdvertisingPdu( if ((le_peer_address_ == advertising_address && le_peer_address_type_ == static_cast<uint8_t>(advertising_address_type)) || - (LeFilterAcceptListContainsDevice( - advertising_address, - static_cast<AddressType>(advertising_address_type))) || - (resolved && LeFilterAcceptListContainsDevice(resolved_address, - resolved_address_type))) { + LeFilterAcceptListContainsDevice( + static_cast<AddressType>(advertising_address_type), + advertising_address) || + (resolved && LeFilterAcceptListContainsDevice(resolved_address_type, + resolved_address))) { Address own_address; auto own_address_type = static_cast<bluetooth::hci::OwnAddressType>(le_address_type_); @@ -1801,15 +2134,15 @@ void LinkLayerController::IncomingLmpPacket( #endif /* ROOTCANAL_LMP */ uint16_t LinkLayerController::HandleLeConnection( - AddressWithType address, AddressWithType own_address, uint8_t role, - uint16_t connection_interval, uint16_t connection_latency, - uint16_t supervision_timeout, + AddressWithType address, AddressWithType own_address, + bluetooth::hci::Role role, uint16_t connection_interval, + uint16_t connection_latency, uint16_t supervision_timeout, bool send_le_channel_selection_algorithm_event) { // Note: the HCI_LE_Connection_Complete event is not sent if the // HCI_LE_Enhanced_Connection_Complete event (see Section 7.7.65.10) is // unmasked. - uint16_t handle = connections_.CreateLeConnection(address, own_address); + uint16_t handle = connections_.CreateLeConnection(address, own_address, role); if (handle == kReservedHandle) { LOG_WARN("No pending connection for connection from %s", address.ToString().c_str()); @@ -1844,17 +2177,15 @@ uint16_t LinkLayerController::HandleLeConnection( } send_event_(bluetooth::hci::LeEnhancedConnectionCompleteBuilder::Create( - ErrorCode::SUCCESS, handle, static_cast<bluetooth::hci::Role>(role), - peer_address_type, connection_address, local_resolved_address, - peer_resolvable_private_address, connection_interval, - connection_latency, supervision_timeout, + ErrorCode::SUCCESS, handle, role, peer_address_type, connection_address, + local_resolved_address, peer_resolvable_private_address, + connection_interval, connection_latency, supervision_timeout, static_cast<bluetooth::hci::ClockAccuracy>(0x00))); } else if (IsLeEventUnmasked(SubeventCode::CONNECTION_COMPLETE)) { send_event_(bluetooth::hci::LeConnectionCompleteBuilder::Create( - ErrorCode::SUCCESS, handle, static_cast<bluetooth::hci::Role>(role), - address.GetAddressType(), address.GetAddress(), connection_interval, - connection_latency, supervision_timeout, - static_cast<bluetooth::hci::ClockAccuracy>(0x00))); + ErrorCode::SUCCESS, handle, role, address.GetAddressType(), + address.GetAddress(), connection_interval, connection_latency, + supervision_timeout, static_cast<bluetooth::hci::ClockAccuracy>(0x00))); } // Note: the HCI_LE_Connection_Complete event is immediately followed by @@ -1925,8 +2256,8 @@ void LinkLayerController::IncomingLeConnectPacket( AddressWithType( incoming.GetSourceAddress(), static_cast<bluetooth::hci::AddressType>(connect.GetAddressType())), - my_address, static_cast<uint8_t>(bluetooth::hci::Role::PERIPHERAL), - connection_interval, connect.GetLeConnectionLatency(), + my_address, bluetooth::hci::Role::PERIPHERAL, connection_interval, + connect.GetLeConnectionLatency(), connect.GetLeConnectionSupervisionTimeout(), false); SendLeLinkLayerPacket(model::packets::LeConnectCompleteBuilder::Create( @@ -1957,8 +2288,8 @@ void LinkLayerController::IncomingLeConnectCompletePacket( AddressWithType( incoming.GetDestinationAddress(), static_cast<bluetooth::hci::AddressType>(le_address_type_)), - static_cast<uint8_t>(bluetooth::hci::Role::CENTRAL), - complete.GetLeConnectionInterval(), complete.GetLeConnectionLatency(), + bluetooth::hci::Role::CENTRAL, complete.GetLeConnectionInterval(), + complete.GetLeConnectionLatency(), complete.GetLeConnectionSupervisionTimeout(), le_extended_connect_); le_connect_ = false; le_extended_connect_ = false; @@ -1978,11 +2309,21 @@ void LinkLayerController::IncomingLeConnectionParameterRequest( peer.ToString().c_str()); return; } - if (IsLeEventUnmasked(SubeventCode::CONNECTION_UPDATE_COMPLETE)) { + + if (IsLeEventUnmasked(SubeventCode::REMOTE_CONNECTION_PARAMETER_REQUEST)) { send_event_( bluetooth::hci::LeRemoteConnectionParameterRequestBuilder::Create( handle, request.GetIntervalMin(), request.GetIntervalMax(), request.GetLatency(), request.GetTimeout())); + } else { + // If the request is being indicated to the Host and the event to the Host + // is masked, then the Link Layer shall issue an LL_REJECT_EXT_IND PDU with + // the ErrorCode set to Unsupported Remote Feature (0x1A). + SendLeLinkLayerPacket( + model::packets::LeConnectionParameterUpdateBuilder::Create( + request.GetDestinationAddress(), request.GetSourceAddress(), + static_cast<uint8_t>(ErrorCode::UNSUPPORTED_REMOTE_OR_LMP_FEATURE), + 0, 0, 0)); } } @@ -2106,8 +2447,11 @@ void LinkLayerController::IncomingLeReadRemoteFeaturesResponse( void LinkLayerController::IncomingLeScanPacket( model::packets::LinkLayerPacketView incoming) { for (auto& advertiser : advertisers_) { - auto to_send = advertiser.GetScanResponse(incoming.GetDestinationAddress(), - incoming.GetSourceAddress()); + // TODO address type should be provided by the LL SCAN_REQ PDU. + auto to_send = advertiser.GetScanResponse( + incoming.GetDestinationAddress(), incoming.GetSourceAddress(), + LeFilterAcceptListContainsDevice(AddressType::PUBLIC_DEVICE_ADDRESS, + incoming.GetSourceAddress())); if (to_send != nullptr) { SendLeLinkLayerPacket(std::move(to_send)); } @@ -2345,6 +2689,7 @@ void LinkLayerController::IncomingPageResponsePacket( LOG_WARN("No free handles"); return; } + CancelScheduledTask(page_timeout_task_id_); #ifdef ROOTCANAL_LMP ASSERT(link_manager_add_link( lm_.get(), reinterpret_cast<const uint8_t(*)[6]>(peer.data()))); @@ -2915,6 +3260,22 @@ ErrorCode LinkLayerController::SetConnectionEncryption( } #endif /* ROOTCANAL_LMP */ +std::vector<bluetooth::hci::Lap> const& LinkLayerController::ReadCurrentIacLap() + const { + return current_iac_lap_list_; +} + +void LinkLayerController::WriteCurrentIacLap( + std::vector<bluetooth::hci::Lap> iac_lap) { + current_iac_lap_list_.swap(iac_lap); + + // If Num_Current_IAC is greater than Num_Supported_IAC then only the first + // Num_Supported_IAC shall be stored in the Controller + if (current_iac_lap_list_.size() > properties_.num_supported_iac) { + current_iac_lap_list_.resize(properties_.num_supported_iac); + } +} + ErrorCode LinkLayerController::AcceptConnectionRequest(const Address& bd_addr, bool try_role_switch) { if (connections_.HasPendingConnection(bd_addr)) { @@ -3030,6 +3391,14 @@ ErrorCode LinkLayerController::CreateConnection(const Address& addr, uint16_t, return ErrorCode::CONTROLLER_BUSY; } + page_timeout_task_id_ = ScheduleTask( + duration_cast<milliseconds>(page_timeout_ * microseconds(625)), + [this, addr] { + send_event_(bluetooth::hci::ConnectionCompleteBuilder::Create( + ErrorCode::PAGE_TIMEOUT, 0xeff, addr, bluetooth::hci::LinkType::ACL, + bluetooth::hci::Enable::DISABLED)); + }); + SendLinkLayerPacket(model::packets::PageBuilder::Create( GetAddress(), addr, class_of_device_, allow_role_switch)); @@ -3040,6 +3409,7 @@ ErrorCode LinkLayerController::CreateConnectionCancel(const Address& addr) { if (!connections_.CancelPendingConnection(addr)) { return ErrorCode::UNKNOWN_CONNECTION; } + CancelScheduledTask(page_timeout_task_id_); return ErrorCode::SUCCESS; } @@ -3386,31 +3756,11 @@ ErrorCode LinkLayerController::SetLeExtendedAdvertisingParameters( bluetooth::hci::PeerAddressTypeText(peer_address_type).c_str()); LOG_INFO("peer_address %s", peer_address.ToString().c_str()); - bluetooth::hci::LeScanningFilterPolicy scanning_filter_policy; - switch (filter_policy) { - case bluetooth::hci::AdvertisingFilterPolicy::ALL_DEVICES: - scanning_filter_policy = - bluetooth::hci::LeScanningFilterPolicy::ACCEPT_ALL; - break; - case bluetooth::hci::AdvertisingFilterPolicy::LISTED_SCAN: - scanning_filter_policy = - bluetooth::hci::LeScanningFilterPolicy::FILTER_ACCEPT_LIST_ONLY; - break; - case bluetooth::hci::AdvertisingFilterPolicy::LISTED_CONNECT: - scanning_filter_policy = - bluetooth::hci::LeScanningFilterPolicy::CHECK_INITIATORS_IDENTITY; - break; - case bluetooth::hci::AdvertisingFilterPolicy::LISTED_SCAN_AND_CONNECT: - scanning_filter_policy = bluetooth::hci::LeScanningFilterPolicy:: - FILTER_ACCEPT_LIST_AND_INITIATORS_IDENTITY; - break; - } - advertisers_[set].InitializeExtended( set, own_address_type, bluetooth::hci::AddressWithType( GetAddress(), bluetooth::hci::AddressType::PUBLIC_DEVICE_ADDRESS), - directed_address, scanning_filter_policy, advertising_type, + directed_address, filter_policy, advertising_type, std::chrono::milliseconds(interval_ms), tx_power, [this, own_address_type, peer_address]() { if (own_address_type == @@ -3418,8 +3768,9 @@ ErrorCode LinkLayerController::SetLeExtendedAdvertisingParameters( own_address_type == bluetooth::hci::OwnAddressType::RESOLVABLE_OR_RANDOM_ADDRESS) { for (const auto& entry : le_resolving_list_) { - if (entry.address == peer_address.GetAddress() && - entry.address_type == peer_address.GetAddressType()) { + if (entry.peer_identity_address == peer_address.GetAddress() && + entry.peer_identity_address_type == + peer_address.GetAddressType()) { return generate_rpa(entry.local_irk); } } @@ -3486,10 +3837,30 @@ ErrorCode LinkLayerController::LeConnectionUpdate( return ErrorCode::UNKNOWN_CONNECTION; } - SendLeLinkLayerPacket(LeConnectionParameterRequestBuilder::Create( - connections_.GetOwnAddress(handle).GetAddress(), - connections_.GetAddress(handle).GetAddress(), interval_min, interval_max, - latency, supervision_timeout)); + bluetooth::hci::Role role = connections_.GetAclRole(handle); + + if (role == bluetooth::hci::Role::CENTRAL) { + // As Central, it is allowed to directly send + // LL_CONNECTION_PARAM_UPDATE_IND to update the parameters. + SendLeLinkLayerPacket(LeConnectionParameterUpdateBuilder::Create( + connections_.GetOwnAddress(handle).GetAddress(), + connections_.GetAddress(handle).GetAddress(), + static_cast<uint8_t>(ErrorCode::SUCCESS), interval_max, latency, + supervision_timeout)); + + if (IsLeEventUnmasked(SubeventCode::CONNECTION_UPDATE_COMPLETE)) { + send_event_(bluetooth::hci::LeConnectionUpdateCompleteBuilder::Create( + ErrorCode::SUCCESS, handle, interval_max, latency, + supervision_timeout)); + } + } else { + // Send LL_CONNECTION_PARAM_REQ and wait for LL_CONNECTION_PARAM_RSP + // in return. + SendLeLinkLayerPacket(LeConnectionParameterRequestBuilder::Create( + connections_.GetOwnAddress(handle).GetAddress(), + connections_.GetAddress(handle).GetAddress(), interval_min, + interval_max, latency, supervision_timeout)); + } return ErrorCode::SUCCESS; } @@ -3531,64 +3902,6 @@ ErrorCode LinkLayerController::LeRemoteConnectionParameterRequestNegativeReply( return ErrorCode::SUCCESS; } -ErrorCode LinkLayerController::LeFilterAcceptListClear() { - if (FilterAcceptListBusy()) { - return ErrorCode::COMMAND_DISALLOWED; - } - - le_connect_list_.clear(); - return ErrorCode::SUCCESS; -} - -ErrorCode LinkLayerController::LeSetAddressResolutionEnable(bool enable) { - if (ResolvingListBusy()) { - return ErrorCode::COMMAND_DISALLOWED; - } - - le_resolving_list_enabled_ = enable; - return ErrorCode::SUCCESS; -} - -ErrorCode LinkLayerController::LeResolvingListClear() { - if (ResolvingListBusy()) { - return ErrorCode::COMMAND_DISALLOWED; - } - - le_resolving_list_.clear(); - return ErrorCode::SUCCESS; -} - -ErrorCode LinkLayerController::LeFilterAcceptListAddDevice( - Address addr, AddressType addr_type) { - if (FilterAcceptListBusy()) { - return ErrorCode::COMMAND_DISALLOWED; - } - for (auto dev : le_connect_list_) { - if (dev.address == addr && dev.address_type == addr_type) { - return ErrorCode::SUCCESS; - } - } - if (LeFilterAcceptListFull()) { - return ErrorCode::MEMORY_CAPACITY_EXCEEDED; - } - le_connect_list_.emplace_back(ConnectListEntry{addr, addr_type}); - return ErrorCode::SUCCESS; -} - -ErrorCode LinkLayerController::LeResolvingListAddDevice( - Address addr, AddressType addr_type, std::array<uint8_t, kIrkSize> peerIrk, - std::array<uint8_t, kIrkSize> localIrk) { - if (ResolvingListBusy()) { - return ErrorCode::COMMAND_DISALLOWED; - } - if (LeResolvingListFull()) { - return ErrorCode::MEMORY_CAPACITY_EXCEEDED; - } - le_resolving_list_.emplace_back( - ResolvingListEntry{addr, addr_type, peerIrk, localIrk}); - return ErrorCode::SUCCESS; -} - bool LinkLayerController::HasAclConnection() { return (connections_.GetAclHandles().size() > 0); } @@ -3864,10 +4177,8 @@ ErrorCode LinkLayerController::SetLeAdvertisingEnable( bluetooth::hci::AddressWithType(GetLeAdvertisingPeerAddress(), static_cast<bluetooth::hci::AddressType>( le_advertising_peer_address_type_)), - static_cast<bluetooth::hci::LeScanningFilterPolicy>( - GetLeAdvertisingFilterPolicy()), - le_advertising_type_, GetLeAdvertisingData(), GetLeScanResponseData(), - interval); + GetLeAdvertisingFilterPolicy(), le_advertising_type_, + GetLeAdvertisingData(), GetLeScanResponseData(), interval); advertisers_[0].Enable(); return ErrorCode::SUCCESS; } @@ -3901,101 +4212,9 @@ ErrorCode LinkLayerController::SetLeExtendedAdvertisingEnable( return ErrorCode::SUCCESS; } -bool LinkLayerController::ListBusy(uint16_t ignore) { - (void)ignore; - if (le_connect_) { - LOG_INFO("le_connect_"); - if (!(ignore & ControllerProperties::kLeListIgnoreConnections)) { - return true; - } - } - if (le_scan_enable_ != bluetooth::hci::OpCode::NONE) { - LOG_INFO("le_scan_enable"); - if (!(ignore & ControllerProperties::kLeListIgnoreScanEnable)) { - return true; - } - } - for (auto advertiser : advertisers_) { - if (advertiser.IsEnabled()) { - LOG_INFO("Advertising"); - if (!(ignore & ControllerProperties::kLeListIgnoreAdvertising)) { - return true; - } - } - } - // TODO: Add HCI_LE_Periodic_Advertising_Create_Sync - return false; -} - -bool LinkLayerController::FilterAcceptListBusy() { - return ListBusy(properties_.le_connect_list_ignore_reasons); -} - -bool LinkLayerController::ResolvingListBusy() { - return ListBusy(properties_.le_resolving_list_ignore_reasons); -} - -ErrorCode LinkLayerController::LeFilterAcceptListRemoveDevice( - Address addr, AddressType addr_type) { - if (FilterAcceptListBusy()) { - return ErrorCode::COMMAND_DISALLOWED; - } - for (size_t i = 0; i < le_connect_list_.size(); i++) { - if (le_connect_list_[i].address == addr && - le_connect_list_[i].address_type == addr_type) { - le_connect_list_.erase(le_connect_list_.begin() + i); - } - } - return ErrorCode::SUCCESS; -} - -ErrorCode LinkLayerController::LeResolvingListRemoveDevice( - Address addr, AddressType addr_type) { - if (ResolvingListBusy()) { - return ErrorCode::COMMAND_DISALLOWED; - } - for (size_t i = 0; i < le_resolving_list_.size(); i++) { - auto curr = le_resolving_list_[i]; - if (curr.address == addr && curr.address_type == addr_type) { - le_resolving_list_.erase(le_resolving_list_.begin() + i); - } - } - return ErrorCode::SUCCESS; -} - -bool LinkLayerController::LeFilterAcceptListContainsDevice( - Address addr, AddressType addr_type) { - for (size_t i = 0; i < le_connect_list_.size(); i++) { - if (le_connect_list_[i].address == addr && - le_connect_list_[i].address_type == addr_type) { - return true; - } - } - return false; -} - -bool LinkLayerController::LeResolvingListContainsDevice(Address addr, - AddressType addr_type) { - for (size_t i = 0; i < le_connect_list_.size(); i++) { - auto curr = le_connect_list_[i]; - if (curr.address == addr && curr.address_type == addr_type) { - return true; - } - } - return false; -} - -bool LinkLayerController::LeFilterAcceptListFull() { - return le_connect_list_.size() >= properties_.le_filter_accept_list_size; -} - -bool LinkLayerController::LeResolvingListFull() { - return le_resolving_list_.size() >= properties_.le_resolving_list_size; -} - void LinkLayerController::Reset() { connections_ = AclConnectionHandler(); - le_connect_list_.clear(); + le_filter_accept_list_.clear(); le_resolving_list_.clear(); le_resolving_list_enabled_ = false; le_connecting_rpa_ = Address(); @@ -4011,6 +4230,12 @@ void LinkLayerController::Reset() { last_inquiry_ = steady_clock::now(); page_scan_enable_ = false; inquiry_scan_enable_ = false; + + bluetooth::hci::Lap general_iac; + general_iac.lap_ = 0x33; // 0x9E8B33 + current_iac_lap_list_.clear(); + current_iac_lap_list_.emplace_back(general_iac); + #ifdef ROOTCANAL_LMP lm_.reset(link_manager_create(ops_)); #endif @@ -4055,7 +4280,7 @@ void LinkLayerController::Inquiry() { } SendLinkLayerPacket(model::packets::InquiryBuilder::Create( - GetAddress(), Address::kEmpty, inquiry_mode_)); + GetAddress(), Address::kEmpty, inquiry_mode_, inquiry_lap_)); last_inquiry_ = now; } @@ -4067,6 +4292,12 @@ void LinkLayerController::SetPageScanEnable(bool enable) { page_scan_enable_ = enable; } +uint16_t LinkLayerController::GetPageTimeout() { return page_timeout_; } + +void LinkLayerController::SetPageTimeout(uint16_t page_timeout) { + page_timeout_ = page_timeout; +} + ErrorCode LinkLayerController::AddScoConnection(uint16_t connection_handle, uint16_t packet_type) { if (!connections_.HasHandle(connection_handle)) { diff --git a/tools/rootcanal/model/controller/link_layer_controller.h b/tools/rootcanal/model/controller/link_layer_controller.h index 9a024767b9..9b1e83dffb 100644 --- a/tools/rootcanal/model/controller/link_layer_controller.h +++ b/tools/rootcanal/model/controller/link_layer_controller.h @@ -41,6 +41,7 @@ using ::bluetooth::hci::AddressType; using ::bluetooth::hci::AuthenticationEnable; using ::bluetooth::hci::ClassOfDevice; using ::bluetooth::hci::ErrorCode; +using ::bluetooth::hci::FilterAcceptListAddressType; using ::bluetooth::hci::OpCode; using ::bluetooth::hci::PageScanRepetitionMode; @@ -48,12 +49,6 @@ class LinkLayerController { public: static constexpr size_t kIrkSize = 16; - // HCI LE Set Random Address command (Vol 4, Part E § 7.8.4). - ErrorCode LeSetRandomAddress(Address random_address); - - // HCI LE Set Host Feature command (Vol 4, Part E § 7.8.115). - ErrorCode LeSetHostFeature(uint8_t bit_number, uint8_t bit_value); - LinkLayerController(const Address& address, const ControllerProperties& properties); @@ -109,6 +104,9 @@ class LinkLayerController { ErrorCode AuthenticationRequested(uint16_t handle); #endif /* ROOTCANAL_LMP */ + std::vector<bluetooth::hci::Lap> const& ReadCurrentIacLap() const; + void WriteCurrentIacLap(std::vector<bluetooth::hci::Lap> iac_lap); + ErrorCode AcceptConnectionRequest(const Address& addr, bool try_role_switch); void MakePeripheralConnection(const Address& addr, bool try_role_switch); ErrorCode RejectConnectionRequest(const Address& addr, uint8_t reason); @@ -205,28 +203,25 @@ class LinkLayerController { ErrorCode LeRemoteConnectionParameterRequestNegativeReply( uint16_t connection_handle, bluetooth::hci::ErrorCode reason); uint16_t HandleLeConnection(AddressWithType addr, AddressWithType own_addr, - uint8_t role, uint16_t connection_interval, + bluetooth::hci::Role role, + uint16_t connection_interval, uint16_t connection_latency, uint16_t supervision_timeout, bool send_le_channel_selection_algorithm_event); - bool ListBusy(uint16_t ignore_mask); - bool FilterAcceptListBusy(); - ErrorCode LeFilterAcceptListClear(); - ErrorCode LeFilterAcceptListAddDevice(Address addr, AddressType addr_type); - ErrorCode LeFilterAcceptListRemoveDevice(Address addr, AddressType addr_type); - bool LeFilterAcceptListContainsDevice(Address addr, AddressType addr_type); - bool LeFilterAcceptListFull(); + bool LeFilterAcceptListContainsDevice( + FilterAcceptListAddressType address_type, Address address); + bool LeFilterAcceptListContainsDevice(AddressType address_type, + Address address); + bool ResolvingListBusy(); - ErrorCode LeSetAddressResolutionEnable(bool enable); - ErrorCode LeResolvingListClear(); - ErrorCode LeResolvingListAddDevice(Address addr, AddressType addr_type, - std::array<uint8_t, kIrkSize> peerIrk, - std::array<uint8_t, kIrkSize> localIrk); - ErrorCode LeResolvingListRemoveDevice(Address addr, AddressType addr_type); - bool LeResolvingListContainsDevice(Address addr, AddressType addr_type); - bool LeResolvingListFull(); + bool LeResolvingListContainsDevice(AddressType peer_identity_address_type, + Address peer_identity_address); + bool LeResolvingListContainsDevice( + bluetooth::hci::PeerAddressType peer_identity_address_type, + Address peer_identity_address); + void LeSetPrivacyMode(AddressType address_type, Address addr, uint8_t mode); void LeReadIsoTxSync(uint16_t handle); @@ -300,7 +295,8 @@ class LinkLayerController { void SetLeScanWindow(uint16_t le_scan_window) { le_scan_window_ = le_scan_window; } - void SetLeScanFilterPolicy(uint8_t le_scan_filter_policy) { + void SetLeScanFilterPolicy( + bluetooth::hci::LeScanningFilterPolicy le_scan_filter_policy) { le_scan_filter_policy_ = le_scan_filter_policy; } void SetLeFilterDuplicates(uint8_t le_scan_filter_duplicates) { @@ -336,7 +332,8 @@ class LinkLayerController { void SetLeMaximumCeLength(uint16_t max) { le_connection_maximum_ce_length_ = max; } - void SetLeInitiatorFilterPolicy(uint8_t le_initiator_filter_policy) { + void SetLeInitiatorFilterPolicy( + bluetooth::hci::InitiatorFilterPolicy le_initiator_filter_policy) { le_initiator_filter_policy_ = le_initiator_filter_policy; } void SetLePeerAddressType(uint8_t peer_address_type) { @@ -355,9 +352,15 @@ class LinkLayerController { void SetInquiryMaxResponses(uint8_t max); void Inquiry(); + bool GetInquiryScanEnable() { return inquiry_scan_enable_; } void SetInquiryScanEnable(bool enable); + + bool GetPageScanEnable() { return page_scan_enable_; } void SetPageScanEnable(bool enable); + uint16_t GetPageTimeout(); + void SetPageTimeout(uint16_t page_timeout); + ErrorCode ChangeConnectionPacketType(uint16_t handle, uint16_t types); ErrorCode ChangeConnectionLinkKey(uint16_t handle); ErrorCode CentralLinkKey(uint8_t key_flag); @@ -401,6 +404,46 @@ class LinkLayerController { void HandleIso(bluetooth::hci::IsoView iso); + // LE Commands + + // HCI LE Set Random Address command (Vol 4, Part E § 7.8.4). + ErrorCode LeSetRandomAddress(Address random_address); + + // HCI LE Set Host Feature command (Vol 4, Part E § 7.8.115). + ErrorCode LeSetHostFeature(uint8_t bit_number, uint8_t bit_value); + + // LE Filter Accept List + + // HCI command LE_Clear_Filter_Accept_List (Vol 4, Part E § 7.8.15). + ErrorCode LeClearFilterAcceptList(); + + // HCI command LE_Add_Device_To_Filter_Accept_List (Vol 4, Part E § 7.8.16). + ErrorCode LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType address_type, Address address); + + // HCI command LE_Remove_Device_From_Filter_Accept_List (Vol 4, Part E + // § 7.8.17). + ErrorCode LeRemoveDeviceFromFilterAcceptList( + FilterAcceptListAddressType address_type, Address address); + + // LE Address Resolving + + // HCI command LE_Add_Device_To_Resolving_List (Vol 4, Part E § 7.8.38). + ErrorCode LeAddDeviceToResolvingList(AddressType peer_identity_address_type, + Address peer_address, + std::array<uint8_t, kIrkSize> peer_irk, + std::array<uint8_t, kIrkSize> local_irk); + + // HCI command LE_Remove_Device_From_Resolving_List (Vol 4, Part E § 7.8.39). + ErrorCode LeRemoveDeviceFromResolvingList( + AddressType peer_identity_address_type, Address peer_identity_address); + + // HCI command LE_Clear_Resolving_List (Vol 4, Part E § 7.8.40). + ErrorCode LeClearResolvingList(); + + // HCI command LE_Set_Address_Resolution_Enable (Vol 4, Part E § 7.8.44). + ErrorCode LeSetAddressResolutionEnable(bool enable); + protected: void SendLeLinkLayerPacketWithRssi( Address source, Address dest, uint8_t rssi, @@ -586,7 +629,8 @@ class LinkLayerController { uint16_t interval_min, uint16_t interval_max, bluetooth::hci::AdvertisingType advertising_type, uint8_t own_address_type, uint8_t peer_address_type, Address peer_address, - uint8_t channel_map, uint8_t filter_policy); + uint8_t channel_map, + bluetooth::hci::AdvertisingFilterPolicy filter_policy); void SetConnectionAcceptTimeout(uint16_t timeout) { connection_accept_timeout_ = timeout; } @@ -621,7 +665,7 @@ class LinkLayerController { return le_advertising_channel_map_; } - uint8_t GetLeAdvertisingFilterPolicy() const { + bluetooth::hci::AdvertisingFilterPolicy GetLeAdvertisingFilterPolicy() const { return le_advertising_filter_policy_; } @@ -698,6 +742,9 @@ class LinkLayerController { // Other configuration parameters. + // Current IAC LAP (Vol 4, Part E § 7.3.44). + std::vector<bluetooth::hci::Lap> current_iac_lap_list_{}; + // Min Encryption Key Size (Vol 4, Part E § 7.3.102). uint8_t min_encryption_key_size_{16}; @@ -727,8 +774,8 @@ class LinkLayerController { uint8_t le_advertising_peer_address_type_{0x0}; // Public Device Address Address le_advertising_peer_address_{}; uint8_t le_advertising_channel_map_{0x7}; // All channels enabled - uint8_t le_advertising_filter_policy_{0x0}; // Process scan and connection - // requests from all devices + bluetooth::hci::AdvertisingFilterPolicy le_advertising_filter_policy_{ + bluetooth::hci::AdvertisingFilterPolicy::ALL_DEVICES}; AclConnectionHandler connections_; @@ -755,18 +802,20 @@ class LinkLayerController { uint32_t oob_id_ = 1; uint32_t key_id_ = 1; - // LE state - struct ConnectListEntry { + struct FilterAcceptListEntry { + FilterAcceptListAddressType address_type; Address address; - AddressType address_type; }; - std::vector<ConnectListEntry> le_connect_list_; + + std::vector<FilterAcceptListEntry> le_filter_accept_list_; + struct ResolvingListEntry { - Address address; - AddressType address_type; + AddressType peer_identity_address_type; + Address peer_identity_address; std::array<uint8_t, kIrkSize> peer_irk; std::array<uint8_t, kIrkSize> local_irk; }; + std::vector<ResolvingListEntry> le_resolving_list_; bool le_resolving_list_enabled_{false}; @@ -778,7 +827,7 @@ class LinkLayerController { uint8_t le_scan_type_{}; uint16_t le_scan_interval_{}; uint16_t le_scan_window_{}; - uint8_t le_scan_filter_policy_{}; + bluetooth::hci::LeScanningFilterPolicy le_scan_filter_policy_{}; uint8_t le_scan_filter_duplicates_{}; bluetooth::hci::OwnAddressType le_address_type_{}; @@ -791,7 +840,7 @@ class LinkLayerController { uint16_t le_connection_supervision_timeout_{}; uint16_t le_connection_minimum_ce_length_{}; uint16_t le_connection_maximum_ce_length_{}; - uint8_t le_initiator_filter_policy_{}; + bluetooth::hci::InitiatorFilterPolicy le_initiator_filter_policy_{}; Address le_peer_address_{}; uint8_t le_peer_address_type_{}; @@ -803,6 +852,9 @@ class LinkLayerController { #else SecurityManager security_manager_{10}; #endif /* ROOTCANAL_LMP */ + + AsyncTaskId page_timeout_task_id_ = kInvalidTaskId; + std::chrono::steady_clock::time_point last_inquiry_; model::packets::InquiryType inquiry_mode_{ model::packets::InquiryType::STANDARD}; diff --git a/tools/rootcanal/packets/link_layer_packets.pdl b/tools/rootcanal/packets/link_layer_packets.pdl index 66101bbc7d..0f6ab02549 100644 --- a/tools/rootcanal/packets/link_layer_packets.pdl +++ b/tools/rootcanal/packets/link_layer_packets.pdl @@ -100,6 +100,7 @@ enum InquiryType : 8 { packet Inquiry : LinkLayerPacket (type = INQUIRY) { inquiry_type : InquiryType, + lap : 8, // The IAC is derived from the LAP } packet BasicInquiryResponse : LinkLayerPacket(type = INQUIRY_RESPONSE) { diff --git a/tools/rootcanal/test/controller/le/le_add_device_to_filter_accept_list_test.cc b/tools/rootcanal/test/controller/le/le_add_device_to_filter_accept_list_test.cc new file mode 100644 index 0000000000..93fe0cc838 --- /dev/null +++ b/tools/rootcanal/test/controller/le/le_add_device_to_filter_accept_list_test.cc @@ -0,0 +1,104 @@ +/* + * 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 <gtest/gtest.h> + +#include "model/controller/link_layer_controller.h" + +namespace rootcanal { + +using namespace bluetooth::hci; + +class LeAddDeviceToFilterAcceptListTest : public ::testing::Test { + public: + LeAddDeviceToFilterAcceptListTest() { + // Reduce the size of the filter accept list to simplify testing. + properties_.le_filter_accept_list_size = 2; + } + + ~LeAddDeviceToFilterAcceptListTest() override = default; + + protected: + Address address_{0}; + ControllerProperties properties_{}; + LinkLayerController controller_{address_, properties_}; +}; + +TEST_F(LeAddDeviceToFilterAcceptListTest, Success) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::RANDOM, Address{1}), + ErrorCode::SUCCESS); +} + +TEST_F(LeAddDeviceToFilterAcceptListTest, ListFull) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{2}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{3}), + ErrorCode::MEMORY_CAPACITY_EXCEEDED); +} + +TEST_F(LeAddDeviceToFilterAcceptListTest, ScanningActive) { + controller_.SetLeScanFilterPolicy( + LeScanningFilterPolicy::FILTER_ACCEPT_LIST_ONLY); + controller_.SetLeScanEnable(OpCode::LE_SET_SCAN_ENABLE); + + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeAddDeviceToFilterAcceptListTest, LegacyAdvertisingActive) { + controller_.SetLeAdvertisingParameters( + 0x0800, 0x0800, AdvertisingType::ADV_IND, 0, 0, Address::kEmpty, 0x7, + AdvertisingFilterPolicy::LISTED_SCAN); + ASSERT_EQ(controller_.SetLeAdvertisingEnable(1), ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeAddDeviceToFilterAcceptListTest, ExtendedAdvertisingActive) { + EnabledSet enabled_set; + enabled_set.advertising_handle_ = 1; + enabled_set.duration_ = 0; + ASSERT_EQ(controller_.SetLeExtendedAdvertisingParameters( + 1, 0, 0, LegacyAdvertisingEventProperties::ADV_IND, + OwnAddressType::PUBLIC_DEVICE_ADDRESS, + PeerAddressType::PUBLIC_DEVICE_OR_IDENTITY_ADDRESS, + Address::kEmpty, AdvertisingFilterPolicy::LISTED_SCAN, 0x70), + ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeExtendedAdvertisingEnable(Enable::ENABLED, + {enabled_set}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +} // namespace rootcanal diff --git a/tools/rootcanal/test/controller/le/le_add_device_to_resolving_list_test.cc b/tools/rootcanal/test/controller/le/le_add_device_to_resolving_list_test.cc new file mode 100644 index 0000000000..345a713803 --- /dev/null +++ b/tools/rootcanal/test/controller/le/le_add_device_to_resolving_list_test.cc @@ -0,0 +1,128 @@ +/* + * 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 <gtest/gtest.h> + +#include "model/controller/link_layer_controller.h" + +namespace rootcanal { + +using namespace bluetooth::hci; + +class LeAddDeviceToResolvingListTest : public ::testing::Test { + public: + LeAddDeviceToResolvingListTest() { + // Reduce the size of the resolving list to simplify testing. + properties_.le_resolving_list_size = 2; + } + + ~LeAddDeviceToResolvingListTest() override = default; + + protected: + Address address_{0}; + ControllerProperties properties_{}; + LinkLayerController controller_{address_, properties_}; +}; + +TEST_F(LeAddDeviceToResolvingListTest, Success) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::RANDOM_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{2}, std::array<uint8_t, 16>{2}), + ErrorCode::SUCCESS); +} + +TEST_F(LeAddDeviceToResolvingListTest, ListFull) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{2}, + std::array<uint8_t, 16>{2}, std::array<uint8_t, 16>{2}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{3}, + std::array<uint8_t, 16>{3}, std::array<uint8_t, 16>{3}), + ErrorCode::MEMORY_CAPACITY_EXCEEDED); +} + +TEST_F(LeAddDeviceToResolvingListTest, ScanningActive) { + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + controller_.SetLeScanEnable(OpCode::LE_SET_SCAN_ENABLE); + + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeAddDeviceToResolvingListTest, LegacyAdvertisingActive) { + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeAdvertisingEnable(1), ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeAddDeviceToResolvingListTest, ExtendedAdvertisingActive) { + EnabledSet enabled_set; + enabled_set.advertising_handle_ = 1; + enabled_set.duration_ = 0; + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeExtendedAdvertisingEnable(Enable::ENABLED, + {enabled_set}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeAddDeviceToResolvingListTest, PeerAddressDuplicate) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{2}, std::array<uint8_t, 16>{2}), + ErrorCode::INVALID_HCI_COMMAND_PARAMETERS); +} + +TEST_F(LeAddDeviceToResolvingListTest, PeerIrkDuplicate) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::RANDOM_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::INVALID_HCI_COMMAND_PARAMETERS); +} + +} // namespace rootcanal diff --git a/tools/rootcanal/test/controller/le/le_clear_filter_accept_list_test.cc b/tools/rootcanal/test/controller/le/le_clear_filter_accept_list_test.cc new file mode 100644 index 0000000000..cca1a78360 --- /dev/null +++ b/tools/rootcanal/test/controller/le/le_clear_filter_accept_list_test.cc @@ -0,0 +1,97 @@ +/* + * 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 <gtest/gtest.h> + +#include "model/controller/link_layer_controller.h" + +namespace rootcanal { + +using namespace bluetooth::hci; + +class LeClearFilterAcceptListTest : public ::testing::Test { + public: + LeClearFilterAcceptListTest() { + // Reduce the size of the filter accept list to simplify testing. + properties_.le_filter_accept_list_size = 2; + } + + ~LeClearFilterAcceptListTest() override = default; + + protected: + Address address_{0}; + ControllerProperties properties_{}; + LinkLayerController controller_{address_, properties_}; +}; + +TEST_F(LeClearFilterAcceptListTest, Success) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeClearFilterAcceptList(), ErrorCode::SUCCESS); +} + +TEST_F(LeClearFilterAcceptListTest, ScanningActive) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + controller_.SetLeScanFilterPolicy( + LeScanningFilterPolicy::FILTER_ACCEPT_LIST_ONLY); + controller_.SetLeScanEnable(OpCode::LE_SET_SCAN_ENABLE); + + ASSERT_EQ(controller_.LeClearFilterAcceptList(), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeClearFilterAcceptListTest, LegacyAdvertisingActive) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + controller_.SetLeAdvertisingParameters( + 0x0800, 0x0800, AdvertisingType::ADV_IND, 0, 0, Address::kEmpty, 0x7, + AdvertisingFilterPolicy::LISTED_SCAN); + ASSERT_EQ(controller_.SetLeAdvertisingEnable(1), ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeClearFilterAcceptList(), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeClearFilterAcceptListTest, ExtendedAdvertisingActive) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + EnabledSet enabled_set; + enabled_set.advertising_handle_ = 1; + enabled_set.duration_ = 0; + ASSERT_EQ(controller_.SetLeExtendedAdvertisingParameters( + 1, 0, 0, LegacyAdvertisingEventProperties::ADV_IND, + OwnAddressType::PUBLIC_DEVICE_ADDRESS, + PeerAddressType::PUBLIC_DEVICE_OR_IDENTITY_ADDRESS, + Address::kEmpty, AdvertisingFilterPolicy::LISTED_SCAN, 0x70), + ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeExtendedAdvertisingEnable(Enable::ENABLED, + {enabled_set}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeClearFilterAcceptList(), + ErrorCode::COMMAND_DISALLOWED); +} + +} // namespace rootcanal diff --git a/tools/rootcanal/test/controller/le/le_clear_resolving_list_test.cc b/tools/rootcanal/test/controller/le/le_clear_resolving_list_test.cc new file mode 100644 index 0000000000..420a611316 --- /dev/null +++ b/tools/rootcanal/test/controller/le/le_clear_resolving_list_test.cc @@ -0,0 +1,90 @@ +/* + * 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 <gtest/gtest.h> + +#include "model/controller/link_layer_controller.h" + +namespace rootcanal { + +using namespace bluetooth::hci; + +class LeClearResolvingListTest : public ::testing::Test { + public: + LeClearResolvingListTest() { + // Reduce the size of the resolving list to simplify testing. + properties_.le_resolving_list_size = 2; + } + + ~LeClearResolvingListTest() override = default; + + protected: + Address address_{0}; + ControllerProperties properties_{}; + LinkLayerController controller_{address_, properties_}; +}; + +TEST_F(LeClearResolvingListTest, Success) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeClearResolvingList(), ErrorCode::SUCCESS); +} + +TEST_F(LeClearResolvingListTest, ScanningActive) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + controller_.SetLeScanEnable(OpCode::LE_SET_SCAN_ENABLE); + + ASSERT_EQ(controller_.LeClearResolvingList(), ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeClearResolvingListTest, LegacyAdvertisingActive) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeAdvertisingEnable(1), ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeClearResolvingList(), ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeClearResolvingListTest, ExtendedAdvertisingActive) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + EnabledSet enabled_set; + enabled_set.advertising_handle_ = 1; + enabled_set.duration_ = 0; + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeExtendedAdvertisingEnable(Enable::ENABLED, + {enabled_set}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeClearResolvingList(), ErrorCode::COMMAND_DISALLOWED); +} + +} // namespace rootcanal diff --git a/tools/rootcanal/test/controller/le/le_remove_device_from_filter_accept_list_test.cc b/tools/rootcanal/test/controller/le/le_remove_device_from_filter_accept_list_test.cc new file mode 100644 index 0000000000..2970ac7965 --- /dev/null +++ b/tools/rootcanal/test/controller/le/le_remove_device_from_filter_accept_list_test.cc @@ -0,0 +1,112 @@ +/* + * 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 <gtest/gtest.h> + +#include "model/controller/link_layer_controller.h" + +namespace rootcanal { + +using namespace bluetooth::hci; + +class LeRemoveDeviceFromFilterAcceptListTest : public ::testing::Test { + public: + LeRemoveDeviceFromFilterAcceptListTest() { + // Reduce the size of the resolving list to simplify testing. + properties_.le_resolving_list_size = 2; + } + + ~LeRemoveDeviceFromFilterAcceptListTest() override = default; + + protected: + Address address_{0}; + ControllerProperties properties_{}; + LinkLayerController controller_{address_, properties_}; +}; + +TEST_F(LeRemoveDeviceFromFilterAcceptListTest, Success) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeRemoveDeviceFromFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); +} + +TEST_F(LeRemoveDeviceFromFilterAcceptListTest, NotFound) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeRemoveDeviceFromFilterAcceptList( + FilterAcceptListAddressType::RANDOM, Address{1}), + ErrorCode::SUCCESS); +} + +TEST_F(LeRemoveDeviceFromFilterAcceptListTest, ScanningActive) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + controller_.SetLeScanFilterPolicy( + LeScanningFilterPolicy::FILTER_ACCEPT_LIST_ONLY); + controller_.SetLeScanEnable(OpCode::LE_SET_SCAN_ENABLE); + + ASSERT_EQ(controller_.LeRemoveDeviceFromFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeRemoveDeviceFromFilterAcceptListTest, LegacyAdvertisingActive) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + controller_.SetLeAdvertisingParameters( + 0x0800, 0x0800, AdvertisingType::ADV_IND, 0, 0, Address::kEmpty, 0x7, + AdvertisingFilterPolicy::LISTED_SCAN); + ASSERT_EQ(controller_.SetLeAdvertisingEnable(1), ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeRemoveDeviceFromFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeRemoveDeviceFromFilterAcceptListTest, ExtendedAdvertisingActive) { + ASSERT_EQ(controller_.LeAddDeviceToFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::SUCCESS); + + EnabledSet enabled_set; + enabled_set.advertising_handle_ = 1; + enabled_set.duration_ = 0; + ASSERT_EQ(controller_.SetLeExtendedAdvertisingParameters( + 1, 0, 0, LegacyAdvertisingEventProperties::ADV_IND, + OwnAddressType::PUBLIC_DEVICE_ADDRESS, + PeerAddressType::PUBLIC_DEVICE_OR_IDENTITY_ADDRESS, + Address::kEmpty, AdvertisingFilterPolicy::LISTED_SCAN, 0x70), + ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeExtendedAdvertisingEnable(Enable::ENABLED, + {enabled_set}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeRemoveDeviceFromFilterAcceptList( + FilterAcceptListAddressType::PUBLIC, Address{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +} // namespace rootcanal diff --git a/tools/rootcanal/test/controller/le/le_remove_device_from_resolving_list_test.cc b/tools/rootcanal/test/controller/le/le_remove_device_from_resolving_list_test.cc new file mode 100644 index 0000000000..ce3745b1e1 --- /dev/null +++ b/tools/rootcanal/test/controller/le/le_remove_device_from_resolving_list_test.cc @@ -0,0 +1,109 @@ +/* + * 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 <gtest/gtest.h> + +#include "model/controller/link_layer_controller.h" + +namespace rootcanal { + +using namespace bluetooth::hci; + +class LeRemoveDeviceFromResolvingListTest : public ::testing::Test { + public: + LeRemoveDeviceFromResolvingListTest() { + // Reduce the size of the resolving list to simplify testing. + properties_.le_resolving_list_size = 2; + } + + ~LeRemoveDeviceFromResolvingListTest() override = default; + + protected: + Address address_{0}; + ControllerProperties properties_{}; + LinkLayerController controller_{address_, properties_}; +}; + +TEST_F(LeRemoveDeviceFromResolvingListTest, Success) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeRemoveDeviceFromResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}), + ErrorCode::SUCCESS); +} + +TEST_F(LeRemoveDeviceFromResolvingListTest, NotFound) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeRemoveDeviceFromResolvingList( + AddressType::RANDOM_DEVICE_ADDRESS, Address{1}), + ErrorCode::UNKNOWN_CONNECTION); +} + +TEST_F(LeRemoveDeviceFromResolvingListTest, ScanningActive) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + controller_.SetLeScanEnable(OpCode::LE_SET_SCAN_ENABLE); + + ASSERT_EQ(controller_.LeRemoveDeviceFromResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeRemoveDeviceFromResolvingListTest, LegacyAdvertisingActive) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeAdvertisingEnable(1), ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeRemoveDeviceFromResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeRemoveDeviceFromResolvingListTest, ExtendedAdvertisingActive) { + ASSERT_EQ(controller_.LeAddDeviceToResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}, + std::array<uint8_t, 16>{1}, std::array<uint8_t, 16>{1}), + ErrorCode::SUCCESS); + + EnabledSet enabled_set; + enabled_set.advertising_handle_ = 1; + enabled_set.duration_ = 0; + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeExtendedAdvertisingEnable(Enable::ENABLED, + {enabled_set}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeRemoveDeviceFromResolvingList( + AddressType::PUBLIC_DEVICE_ADDRESS, Address{1}), + ErrorCode::COMMAND_DISALLOWED); +} + +} // namespace rootcanal diff --git a/tools/rootcanal/test/controller/le/le_set_address_resolution_enable_test.cc b/tools/rootcanal/test/controller/le/le_set_address_resolution_enable_test.cc new file mode 100644 index 0000000000..19976c4d90 --- /dev/null +++ b/tools/rootcanal/test/controller/le/le_set_address_resolution_enable_test.cc @@ -0,0 +1,73 @@ +/* + * 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 <gtest/gtest.h> + +#include "model/controller/link_layer_controller.h" + +namespace rootcanal { + +using namespace bluetooth::hci; + +class LeSetAddressResolutionEnableTest : public ::testing::Test { + public: + LeSetAddressResolutionEnableTest() = default; + ~LeSetAddressResolutionEnableTest() override = default; + + protected: + Address address_{0}; + ControllerProperties properties_{}; + LinkLayerController controller_{address_, properties_}; +}; + +TEST_F(LeSetAddressResolutionEnableTest, Success) { + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(false), + ErrorCode::SUCCESS); +} + +TEST_F(LeSetAddressResolutionEnableTest, ScanningActive) { + controller_.SetLeScanEnable(OpCode::LE_SET_SCAN_ENABLE); + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), + ErrorCode::COMMAND_DISALLOWED); + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(false), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeSetAddressResolutionEnableTest, LegacyAdvertisingActive) { + ASSERT_EQ(controller_.SetLeAdvertisingEnable(1), ErrorCode::SUCCESS); + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), + ErrorCode::COMMAND_DISALLOWED); + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(false), + ErrorCode::COMMAND_DISALLOWED); +} + +TEST_F(LeSetAddressResolutionEnableTest, ExtendedAdvertisingActive) { + EnabledSet enabled_set; + enabled_set.advertising_handle_ = 1; + enabled_set.duration_ = 0; + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), ErrorCode::SUCCESS); + ASSERT_EQ(controller_.SetLeExtendedAdvertisingEnable(Enable::ENABLED, + {enabled_set}), + ErrorCode::SUCCESS); + + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(true), + ErrorCode::COMMAND_DISALLOWED); + ASSERT_EQ(controller_.LeSetAddressResolutionEnable(false), + ErrorCode::COMMAND_DISALLOWED); +} + +} // namespace rootcanal |