diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-04-09 23:09:28 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-04-09 23:09:28 +0000 |
commit | 4b17cbcccf3c79b9839b32d055883b4fa12dac6c (patch) | |
tree | 671509816cac7f79d56c4515300ace3e6a1c33c4 | |
parent | c7d896b3b3e5aa58ec83546790cdd2f81ca32314 (diff) | |
parent | d76065776b2690ee6c1595e46b10115ae0d32a42 (diff) |
Snap for 8431966 from d76065776b2690ee6c1595e46b10115ae0d32a42 to tm-release
Change-Id: I0af90a0b5ca04edd75f394b617297a5f082d05f4
37 files changed, 1462 insertions, 231 deletions
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 d616ffcd83..b6b946a8a3 100644 --- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java @@ -59,6 +59,7 @@ import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.bluetooth.mcp.McpService; import com.android.bluetooth.vc.VolumeControlService; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.SynchronousResultReceiver; @@ -110,9 +111,10 @@ public class LeAudioService extends ProfileService { private AdapterService mAdapterService; private DatabaseManager mDatabaseManager; private HandlerThread mStateMachinesThread; - private BluetoothDevice mActiveAudioOutDevice; - private BluetoothDevice mActiveAudioInDevice; + private volatile BluetoothDevice mActiveAudioOutDevice; + private volatile BluetoothDevice mActiveAudioInDevice; private LeAudioCodecConfig mLeAudioCodecConfig; + private Object mGroupLock = new Object(); ServiceFactory mServiceFactory = new ServiceFactory(); LeAudioNativeInterface mLeAudioNativeInterface; @@ -143,9 +145,11 @@ public class LeAudioService extends ProfileService { List<BluetoothLeAudioCodecConfig> mInputLocalCodecCapabilities = new ArrayList<>(); List<BluetoothLeAudioCodecConfig> mOutputLocalCodecCapabilities = new ArrayList<>(); + @GuardedBy("mGroupLock") private final Map<Integer, LeAudioGroupDescriptor> mGroupDescriptors = new LinkedHashMap<>(); private final Map<BluetoothDevice, LeAudioStateMachine> mStateMachines = new LinkedHashMap<>(); + @GuardedBy("mGroupLock") private final Map<BluetoothDevice, Integer> mDeviceGroupIdMap = new ConcurrentHashMap<>(); private final Map<BluetoothDevice, Integer> mDeviceAudioLocationMap = new ConcurrentHashMap<>(); @@ -210,13 +214,15 @@ public class LeAudioService extends ProfileService { mStateMachinesThread = new HandlerThread("LeAudioService.StateMachines"); mStateMachinesThread.start(); - mDeviceGroupIdMap.clear(); mDeviceAudioLocationMap.clear(); mBroadcastStateMap.clear(); mBroadcastMetadataList.clear(); mBroadcastsPlaybackMap.clear(); - mGroupDescriptors.clear(); + synchronized (mGroupLock) { + mDeviceGroupIdMap.clear(); + mGroupDescriptors.clear(); + } // Setup broadcast receivers IntentFilter filter = new IntentFilter(); @@ -247,12 +253,19 @@ public class LeAudioService extends ProfileService { // Delay the call to init by posting it. This ensures TBS and MCS are fully initialized // before we start accepting connections - mHandler.post(() -> - mLeAudioNativeInterface.init(mLeAudioCodecConfig.getCodecConfigOffloading())); + mHandler.post(this::init); return true; } + private void init() { + LeAudioNativeInterface nativeInterface = mLeAudioNativeInterface; + if (nativeInterface == null) { + Log.w(TAG, "the service is stopped. ignore init()"); + } + nativeInterface.init(mLeAudioCodecConfig.getCodecConfigOffloading()); + } + @Override protected boolean stop() { Log.i(TAG, "stop()"); @@ -264,15 +277,19 @@ public class LeAudioService extends ProfileService { setActiveDevice(null); //Don't wait for async call with INACTIVE group status, clean active //device for active group. - for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) { - LeAudioGroupDescriptor descriptor = entry.getValue(); - Integer group_id = entry.getKey(); - if (descriptor.mIsActive) { - descriptor.mIsActive = false; - updateActiveDevices(group_id, descriptor.mActiveContexts, - ACTIVE_CONTEXTS_NONE, descriptor.mIsActive); - break; + synchronized (mGroupLock) { + for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) { + LeAudioGroupDescriptor descriptor = entry.getValue(); + Integer group_id = entry.getKey(); + if (descriptor.mIsActive) { + descriptor.mIsActive = false; + updateActiveDevices(group_id, descriptor.mActiveContexts, + ACTIVE_CONTEXTS_NONE, descriptor.mIsActive); + break; + } } + mDeviceGroupIdMap.clear(); + mGroupDescriptors.clear(); } // Cleanup native interfaces @@ -297,9 +314,7 @@ public class LeAudioService extends ProfileService { mStateMachines.clear(); } - mDeviceGroupIdMap.clear(); mDeviceAudioLocationMap.clear(); - mGroupDescriptors.clear(); if (mBroadcastCallbacks != null) { mBroadcastCallbacks.kill(); @@ -328,7 +343,6 @@ public class LeAudioService extends ProfileService { } } - mAudioManager = null; mAdapterService = null; mAudioManager = null; @@ -423,7 +437,7 @@ public class LeAudioService extends ProfileService { } } - public BluetoothDevice getConnectedGroupLeadDevice(int groupId) { + BluetoothDevice getConnectedGroupLeadDevice(int groupId) { return getFirstDeviceFromGroup(groupId); } @@ -499,7 +513,7 @@ public class LeAudioService extends ProfileService { * @param device the active device * @return true on success, otherwise false */ - public boolean groupAddNode(int groupId, BluetoothDevice device) { + boolean groupAddNode(int groupId, BluetoothDevice device) { return mLeAudioNativeInterface.groupAddNode(groupId, device); } @@ -509,7 +523,7 @@ public class LeAudioService extends ProfileService { * @param device the active device * @return true on success, otherwise false */ - public boolean groupRemoveNode(int groupId, BluetoothDevice device) { + boolean groupRemoveNode(int groupId, BluetoothDevice device) { return mLeAudioNativeInterface.groupRemoveNode(groupId, device); } @@ -519,23 +533,25 @@ public class LeAudioService extends ProfileService { * @return true given group exists, otherwise false */ public boolean isValidDeviceGroup(int group_id) { - return (group_id != LE_AUDIO_GROUP_ID_INVALID) ? - mDeviceGroupIdMap.containsValue(group_id) : - false; + return group_id != LE_AUDIO_GROUP_ID_INVALID && mDeviceGroupIdMap.containsValue(group_id); } /** * Get all the devices within a given group. - * @param group_id group Id to verify + * @param groupId group id to get devices * @return all devices within a given group or empty list */ - public List<BluetoothDevice> getGroupDevices(int group_id) { + public List<BluetoothDevice> getGroupDevices(int groupId) { List<BluetoothDevice> result = new ArrayList<>(); - if (group_id != LE_AUDIO_GROUP_ID_INVALID) { - for (BluetoothDevice storedDevice : mDeviceGroupIdMap.keySet()) { - if (getGroupId(storedDevice) == group_id) { - result.add(storedDevice); + if (groupId == LE_AUDIO_GROUP_ID_INVALID) { + return result; + } + + synchronized (mGroupLock) { + for (Map.Entry<BluetoothDevice, Integer> entry : mDeviceGroupIdMap.entrySet()) { + if (entry.getValue() == groupId) { + result.add(entry.getKey()); } } } @@ -545,11 +561,11 @@ public class LeAudioService extends ProfileService { /** * Get supported group audio direction from available context. * - * @param activeContext bitset of active context to be matched with possible audio direction + * @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 + * {@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; @@ -565,10 +581,12 @@ public class LeAudioService extends ProfileService { } private Integer getActiveGroupId() { - for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) { - LeAudioGroupDescriptor descriptor = entry.getValue(); - if (descriptor.mIsActive) { - return entry.getKey(); + synchronized (mGroupLock) { + for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) { + LeAudioGroupDescriptor descriptor = entry.getValue(); + if (descriptor.mIsActive) { + return entry.getKey(); + } } } return LE_AUDIO_GROUP_ID_INVALID; @@ -669,21 +687,21 @@ public class LeAudioService extends ProfileService { } private BluetoothDevice getFirstDeviceFromGroup(Integer groupId) { - if (groupId != LE_AUDIO_GROUP_ID_INVALID) { - for(Map.Entry<BluetoothDevice, Integer> entry : mDeviceGroupIdMap.entrySet()) { + if (groupId == LE_AUDIO_GROUP_ID_INVALID) { + return null; + } + synchronized (mGroupLock) { + for (Map.Entry<BluetoothDevice, Integer> entry : mDeviceGroupIdMap.entrySet()) { if (entry.getValue() != groupId) { continue; } - LeAudioStateMachine sm = mStateMachines.get(entry.getKey()); if (sm == null || sm.getConnectionState() != BluetoothProfile.STATE_CONNECTED) { continue; } - return entry.getKey(); } } - return null; } @@ -720,8 +738,10 @@ public class LeAudioService extends ProfileService { } } else if (previousGroupId != LE_AUDIO_GROUP_ID_INVALID) { /* Mark old group as no active */ - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(previousGroupId); - descriptor.mIsActive = false; + LeAudioGroupDescriptor descriptor = getGroupDescriptor(previousGroupId); + if (descriptor != null) { + descriptor.mIsActive = false; + } } } @@ -784,8 +804,10 @@ public class LeAudioService extends ProfileService { } else if (previousGroupId != LE_AUDIO_GROUP_ID_INVALID) { Log.i(TAG, " Switching active group from " + previousGroupId + " to " + groupId); /* Mark old group as no active */ - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(previousGroupId); - descriptor.mIsActive = false; + LeAudioGroupDescriptor descriptor = getGroupDescriptor(previousGroupId); + if (descriptor != null) { + descriptor.mIsActive = false; + } } } @@ -851,9 +873,8 @@ public class LeAudioService extends ProfileService { /** * Set the active device group. - * @param groupId group Id to set active */ - private void setActiveDeviceGroup(BluetoothDevice device) { + private void setActiveGroupWithDevice(BluetoothDevice device) { int groupId = LE_AUDIO_GROUP_ID_INVALID; if (device != null) { @@ -862,9 +883,9 @@ public class LeAudioService extends ProfileService { int currentlyActiveGroupId = getActiveGroupId(); if (DBG) { - Log.d(TAG, "setActiveDeviceGroup = " + groupId + - ", currentlyActiveGroupId = " + currentlyActiveGroupId + - ", device: " + device); + Log.d(TAG, "setActiveGroupWithDevice = " + groupId + + ", currentlyActiveGroupId = " + currentlyActiveGroupId + + ", device: " + device); } if (groupId == currentlyActiveGroupId) { @@ -882,24 +903,22 @@ public class LeAudioService extends ProfileService { * @return true on success, otherwise false */ public boolean setActiveDevice(BluetoothDevice device) { - synchronized (mStateMachines) { - /* Clear active group */ - if (device == null) { - setActiveDeviceGroup(device); - return true; - } - if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { - Log.e(TAG, "setActiveDevice(" + device + "): failed because group device is not " + - "connected"); - return false; - } - setActiveDeviceGroup(device); + /* Clear active group */ + if (device == null) { + setActiveGroupWithDevice(device); return true; } + if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { + Log.e(TAG, "setActiveDevice(" + device + "): failed because group device is not " + + "connected"); + return false; + } + setActiveGroupWithDevice(device); + return true; } /** - * Get the active LE audio device. + * Get the active LE audio devices. * * Note: When LE audio group is active, one of the Bluetooth device address * which belongs to the group, represents the active LE audio group. @@ -912,17 +931,16 @@ public class LeAudioService extends ProfileService { if (DBG) { Log.d(TAG, "getActiveDevices"); } - ArrayList<BluetoothDevice> activeDevices = new ArrayList<>(); + ArrayList<BluetoothDevice> activeDevices = new ArrayList<>(2); activeDevices.add(null); activeDevices.add(null); - synchronized (mStateMachines) { - int currentlyActiveGroupId = getActiveGroupId(); - if (currentlyActiveGroupId == LE_AUDIO_GROUP_ID_INVALID) { - return activeDevices; - } - activeDevices.add(0, mActiveAudioOutDevice); - activeDevices.add(1, mActiveAudioInDevice); + + int currentlyActiveGroupId = getActiveGroupId(); + if (currentlyActiveGroupId == LE_AUDIO_GROUP_ID_INVALID) { + return activeDevices; } + activeDevices.set(0, mActiveAudioOutDevice); + activeDevices.set(1, mActiveAudioInDevice); return activeDevices; } @@ -957,9 +975,8 @@ public class LeAudioService extends ProfileService { continue; } sm.sendMessage(LeAudioStateMachine.CONNECT); - } - } - + } + } } // Suppressed since this is part of a local process @@ -995,27 +1012,18 @@ public class LeAudioService extends ProfileService { return; } } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_GROUP_NODE_STATUS_CHANGED) { - int group_id = stackEvent.valueInt1; - int node_status = stackEvent.valueInt2; + int groupId = stackEvent.valueInt1; + int nodeStatus = stackEvent.valueInt2; Objects.requireNonNull(stackEvent.device, "Device should never be null, event: " + stackEvent); - switch (node_status) { + switch (nodeStatus) { case LeAudioStackEvent.GROUP_NODE_ADDED: - mDeviceGroupIdMap.put(device, group_id); - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(group_id); - if (descriptor == null) { - mGroupDescriptors.put(group_id, new LeAudioGroupDescriptor()); - } - notifyGroupNodeAdded(device, group_id); + handleGroupNodeAdded(device, groupId); break; case LeAudioStackEvent.GROUP_NODE_REMOVED: - mDeviceGroupIdMap.remove(device); - if (mDeviceGroupIdMap.containsValue(group_id) == false) { - mGroupDescriptors.remove(group_id); - } - notifyGroupNodeRemoved(device, group_id); + handleGroupNodeRemoved(device, groupId); break; default: break; @@ -1027,7 +1035,7 @@ public class LeAudioService extends ProfileService { } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_AUDIO_GROUP_CODEC_CONFIG_CHANGED) { int groupId = stackEvent.valueInt1; - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(groupId); + LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); if (descriptor == null) { Log.e(TAG, " Group not found " + groupId); return; @@ -1050,27 +1058,26 @@ public class LeAudioService extends ProfileService { descriptor.mCodecStatus = status; notifyUnicastCodecConfigChanged(groupId, status); - } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED) { int direction = stackEvent.valueInt1; - int group_id = stackEvent.valueInt2; + int groupId = stackEvent.valueInt2; int snk_audio_location = stackEvent.valueInt3; int src_audio_location = stackEvent.valueInt4; int available_contexts = stackEvent.valueInt5; - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(group_id); + LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); if (descriptor != null) { if (descriptor.mIsActive) { descriptor.mIsActive = - updateActiveDevices(group_id, descriptor.mActiveContexts, + updateActiveDevices(groupId, descriptor.mActiveContexts, available_contexts, descriptor.mIsActive); if (!descriptor.mIsActive) { - notifyGroupStatusChanged(group_id, BluetoothLeAudio.GROUP_STATUS_INACTIVE); + notifyGroupStatusChanged(groupId, BluetoothLeAudio.GROUP_STATUS_INACTIVE); } } descriptor.mActiveContexts = available_contexts; } else { - Log.e(TAG, "no descriptors for group: " + group_id); + Log.e(TAG, "no descriptors for group: " + groupId); } } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_SINK_AUDIO_LOCATION_AVAILABLE) { Objects.requireNonNull(stackEvent.device, @@ -1084,36 +1091,36 @@ public class LeAudioService extends ProfileService { + " audio location:" + sink_audio_location); } } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED) { - int group_id = stackEvent.valueInt1; - int group_status = stackEvent.valueInt2; + int groupId = stackEvent.valueInt1; + int groupStatus = stackEvent.valueInt2; boolean notifyGroupStatus = false; - switch (group_status) { + switch (groupStatus) { case LeAudioStackEvent.GROUP_STATUS_ACTIVE: { - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(group_id); + LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); if (descriptor != null) { if (!descriptor.mIsActive) { - descriptor.mIsActive = updateActiveDevices(group_id, + descriptor.mIsActive = updateActiveDevices(groupId, ACTIVE_CONTEXTS_NONE, descriptor.mActiveContexts, true); notifyGroupStatus = descriptor.mIsActive; } } else { - Log.e(TAG, "no descriptors for group: " + group_id); + Log.e(TAG, "no descriptors for group: " + groupId); } break; } case LeAudioStackEvent.GROUP_STATUS_INACTIVE: { - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(group_id); + LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); if (descriptor != null) { if (descriptor.mIsActive) { descriptor.mIsActive = false; - updateActiveDevices(group_id, descriptor.mActiveContexts, + updateActiveDevices(groupId, descriptor.mActiveContexts, ACTIVE_CONTEXTS_NONE, descriptor.mIsActive); notifyGroupStatus = true; } } else { - Log.e(TAG, "no descriptors for group: " + group_id); + Log.e(TAG, "no descriptors for group: " + groupId); } break; } @@ -1122,7 +1129,7 @@ public class LeAudioService extends ProfileService { } if (notifyGroupStatus) { - notifyGroupStatusChanged(group_id, group_status); + notifyGroupStatusChanged(groupId, groupStatus); } } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_BROADCAST_CREATED) { @@ -1340,7 +1347,7 @@ public class LeAudioService extends ProfileService { // BluetoothMetricsProto.ProfileId.LE_AUDIO); } - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(myGroupId); + LeAudioGroupDescriptor descriptor = getGroupDescriptor(myGroupId); if (descriptor != null) { descriptor.mIsConnected = true; /* HearingAid activates device after connection @@ -1372,7 +1379,7 @@ public class LeAudioService extends ProfileService { } int myGroupId = getGroupId(device); - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(myGroupId); + LeAudioGroupDescriptor descriptor = getGroupDescriptor(myGroupId); if (descriptor == null) { Log.e(TAG, "no descriptors for group: " + myGroupId); return; @@ -1520,7 +1527,9 @@ public class LeAudioService extends ProfileService { if (device == null) { return LE_AUDIO_GROUP_ID_INVALID; } - return mDeviceGroupIdMap.getOrDefault(device, LE_AUDIO_GROUP_ID_INVALID); + synchronized (mGroupLock) { + return mDeviceGroupIdMap.getOrDefault(device, LE_AUDIO_GROUP_ID_INVALID); + } } /** @@ -1544,6 +1553,23 @@ public class LeAudioService extends ProfileService { } } + private LeAudioGroupDescriptor getGroupDescriptor(int groupId) { + synchronized (mGroupLock) { + return mGroupDescriptors.get(groupId); + } + } + + private void handleGroupNodeAdded(BluetoothDevice device, int groupId) { + synchronized (mGroupLock) { + mDeviceGroupIdMap.put(device, groupId); + LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(groupId); + if (descriptor == null) { + mGroupDescriptors.put(groupId, new LeAudioGroupDescriptor()); + } + notifyGroupNodeAdded(device, groupId); + } + } + private void notifyGroupNodeAdded(BluetoothDevice device, int groupId) { if (mLeAudioCallbacks != null) { int n = mLeAudioCallbacks.beginBroadcast(); @@ -1558,6 +1584,16 @@ public class LeAudioService extends ProfileService { } } + private void handleGroupNodeRemoved(BluetoothDevice device, int groupId) { + synchronized (mGroupLock) { + mDeviceGroupIdMap.remove(device); + if (!mDeviceGroupIdMap.containsValue(groupId)) { + mGroupDescriptors.remove(groupId); + } + notifyGroupNodeRemoved(device, groupId); + } + } + private void notifyGroupNodeRemoved(BluetoothDevice device, int groupId) { if (mLeAudioCallbacks != null) { int n = mLeAudioCallbacks.beginBroadcast(); @@ -1741,7 +1777,7 @@ public class LeAudioService extends ProfileService { if (DBG) { Log.d(TAG, "getCodecStatus(" + groupId + ")"); } - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(groupId); + LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); if (descriptor != null) { return descriptor.mCodecStatus; } @@ -1764,7 +1800,7 @@ public class LeAudioService extends ProfileService { + Objects.toString(inputCodecConfig) + Objects.toString(outputCodecConfig)); } - LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(groupId); + LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); if (descriptor == null) { Log.e(TAG, "setCodecConfigPreference: Invalid groupId, " + groupId); return; @@ -1793,7 +1829,6 @@ public class LeAudioService extends ProfileService { inputCodecConfig, outputCodecConfig); } - /** * Binder object: must be a static class or memory leak may occur */ diff --git a/android/leaudio/app/src/main/AndroidManifest.xml b/android/leaudio/app/src/main/AndroidManifest.xml index 996dcee9ff..120142d543 100644 --- a/android/leaudio/app/src/main/AndroidManifest.xml +++ b/android/leaudio/app/src/main/AndroidManifest.xml @@ -36,6 +36,11 @@ android:excludeFromRecents="true" android:theme="@style/AppTheme.NoActionBar"> </activity> + <activity + android:name=".BroadcastScanActivity" + android:excludeFromRecents="true" + android:theme="@style/AppTheme.NoActionBar"> + </activity> </application> </manifest> diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java index 0e628ab3df..0b931adb0a 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java @@ -26,40 +26,45 @@ import android.content.IntentFilter; import android.os.ParcelUuid; import android.util.Log; +import androidx.annotation.NonNull; import androidx.core.util.Pair; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; -import com.android.bluetooth.leaudio.R; - public class BluetoothProxy { private static BluetoothProxy INSTANCE; private final Application application; private final BluetoothAdapter bluetoothAdapter; private BluetoothLeAudio bluetoothLeAudio = null; private BluetoothLeBroadcast mBluetoothLeBroadcast = null; + private BluetoothLeBroadcastAssistant mBluetoothLeBroadcastAssistant = null; + private Set<BluetoothDevice> mBroadcastScanOnBehalfDevices = new HashSet<>(); private BluetoothCsipSetCoordinator bluetoothCsis = null; private BluetoothVolumeControl bluetoothVolumeControl = null; private BluetoothHapClient bluetoothHapClient = null; private BluetoothProfile.ServiceListener profileListener = null; private BluetoothHapClient.Callback hapCallback = null; + private OnBassEventListener mBassEventListener; + private OnLocalBroadcastEventListener mLocalBroadcastEventListener; private final IntentFilter adapterIntentFilter; + private final IntentFilter bassIntentFilter; private IntentFilter intentFilter; private final ExecutorService mExecutor; @@ -290,6 +295,9 @@ public class BluetoothProxy { } mBroadcastAddedMutableLive.postValue(broadcastId); + if (mLocalBroadcastEventListener != null) { + mLocalBroadcastEventListener.onBroadcastStarted(broadcastId); + } } @Override @@ -301,6 +309,9 @@ public class BluetoothProxy { @Override public void onBroadcastStopped(int reason, int broadcastId) { mBroadcastRemovedMutableLive.postValue(new Pair<>(reason, broadcastId)); + if (mLocalBroadcastEventListener != null) { + mLocalBroadcastEventListener.onBroadcastStopped(broadcastId); + } } @Override @@ -323,6 +334,9 @@ public class BluetoothProxy { public void onBroadcastUpdated(int reason, int broadcastId) { mBroadcastStatusMutableLive.postValue("Broadcast " + broadcastId + "has been updated due to reason: " + reason); + if (mLocalBroadcastEventListener != null) { + mLocalBroadcastEventListener.onBroadcastUpdated(broadcastId); + } } @Override @@ -335,9 +349,147 @@ public class BluetoothProxy { public void onBroadcastMetadataChanged(int broadcastId, BluetoothLeBroadcastMetadata metadata) { mBroadcastUpdateMutableLive.postValue(metadata); + if (mLocalBroadcastEventListener != null) { + mLocalBroadcastEventListener.onBroadcastMetadataChanged( + broadcastId, metadata); + } } }; + // TODO: Add behaviors in empty methods if necessary. + private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + new BluetoothLeBroadcastAssistant.Callback() { + @Override + public void onSearchStarted(int reason) {} + + @Override + public void onSearchStartFailed(int reason) {} + + @Override + public void onSearchStopped(int reason) {} + + @Override + public void onSearchStopFailed(int reason) {} + + @Override + public void onSourceFound(BluetoothLeBroadcastMetadata source) { + if (mBassEventListener != null) { + mBassEventListener.onSourceFound(source); + } + } + + @Override + public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceAddFailed(BluetoothDevice sink, + BluetoothLeBroadcastMetadata source, int reason) {} + + @Override + public void onSourceModified(BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onReceiveStateChanged(BluetoothDevice sink, int sourceId, + BluetoothLeBroadcastReceiveState state) { + if (allLeAudioDevicesMutable.getValue() != null) { + Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable + .getValue().stream() + .filter(stateWrapper -> stateWrapper.device.getAddress().equals( + sink.getAddress())) + .findAny(); + + if (!valid_device_opt.isPresent()) + return; + + LeAudioDeviceStateWrapper valid_device = valid_device_opt.get(); + LeAudioDeviceStateWrapper.BassData svc_data = valid_device.bassData; + + // TODO: Is the receiver_id same with BluetoothLeBroadcastReceiveState.getSourceId()? + // If not, find getSourceId() usages and fix the issues. +// rstate.receiver_id = intent.getIntExtra( +// BluetoothBroadcastAudioScan.EXTRA_BASS_RECEIVER_ID, -1); + /** + * From "Introducing-Bluetooth-LE-Audio-book" 8.6.3.1: + * + * The Source_ID is an Acceptor generated number which is used to identify a + * specific set of + * broadcast device and BIG information. It is local to an Acceptor and used as a + * reference for + * a Broadcast Assistant. In the case of a Coordinated Set of Acceptors, such as + * a left and right + * earbud, the Source_IDs are not related and may be different, even if both are + * receiving the + * same BIS, as each Acceptor independently creates their own Source ID values + */ + + /** + * From BluetoothBroadcastAudioScan.EXTRA_BASS_RECEIVER_ID: + * + * Broadcast receiver's endpoint identifier. + */ + + HashMap<Integer, BluetoothLeBroadcastReceiveState> states = + svc_data.receiverStatesMutable.getValue(); + if (states == null) + states = new HashMap<>(); + states.put(state.getSourceId(), state); + + // Use SetValue instead of PostValue() since we want to make it + // synchronous due to getValue() we do here as well + // Otherwise we could miss the update and store only the last + // receiver ID + svc_data.receiverStatesMutable.setValue(states); + } + } + }; + + private final BroadcastReceiver bassIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothLeBroadcastAssistant.ACTION_CONNECTION_STATE_CHANGED)) { + final BluetoothDevice device = intent.getParcelableExtra( + BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class); + + if (allLeAudioDevicesMutable.getValue() != null) { + if (device != null) { + Optional<LeAudioDeviceStateWrapper> valid_device_opt = + allLeAudioDevicesMutable + .getValue().stream() + .filter(state -> state.device.getAddress().equals( + device.getAddress())) + .findAny(); + + if (valid_device_opt.isPresent()) { + LeAudioDeviceStateWrapper valid_device = valid_device_opt.get(); + LeAudioDeviceStateWrapper.BassData svc_data = valid_device.bassData; + + final int toState = intent + .getIntExtra(BluetoothProfile.EXTRA_STATE, -1); + if (toState == BluetoothProfile.STATE_CONNECTED + || toState == BluetoothProfile.STATE_DISCONNECTED) + svc_data.isConnectedMutable.postValue( + toState == BluetoothProfile.STATE_CONNECTED); + } + } + } + } + // TODO: Remove this if unnecessary. +// case BluetoothBroadcastAudioScan.ACTION_BASS_BROADCAST_ANNONCEMENT_AVAILABLE: +// // FIXME: Never happen since there is no valid device with this intent +// break; + } + }; + private BluetoothProxy(Application application) { this.application = application; bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); @@ -360,6 +512,10 @@ public class BluetoothProxy { adapterIntentFilter = new IntentFilter(); adapterIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); application.registerReceiver(adapterIntentReceiver, adapterIntentFilter); + + bassIntentFilter = new IntentFilter(); + bassIntentFilter.addAction(BluetoothLeBroadcastAssistant.ACTION_CONNECTION_STATE_CHANGED); + application.registerReceiver(bassIntentReceiver, bassIntentFilter); } // Lazy constructing Singleton acquire method @@ -512,6 +668,12 @@ public class BluetoothProxy { mBluetoothLeBroadcast = (BluetoothLeBroadcast) bluetoothProfile; mBluetoothLeBroadcast.registerCallback(mExecutor, mBroadcasterCallback); break; + case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT: + mBluetoothLeBroadcastAssistant = (BluetoothLeBroadcastAssistant) + bluetoothProfile; + mBluetoothLeBroadcastAssistant.registerCallback(mExecutor, + mBroadcastAssistantCallback); + break; } queryLeAudioDevices(); } @@ -525,6 +687,7 @@ public class BluetoothProxy { initVolumeControlProxy(); initHapProxy(); initLeAudioBroadcastProxy(); + initBassProxy(); } public void cleanupProfiles() { @@ -535,6 +698,7 @@ public class BluetoothProxy { cleanupVolumeControlProxy(); cleanupHapProxy(); cleanupLeAudioBroadcastProxy(); + cleanupBassProxy(); profileListener = null; } @@ -605,6 +769,19 @@ public class BluetoothProxy { } } + private void initBassProxy() { + bluetoothAdapter.getProfileProxy(this.application, profileListener, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + } + + private void cleanupBassProxy() { + if (mBluetoothLeBroadcastAssistant != null) { + mBluetoothLeBroadcastAssistant.unregisterCallback(mBroadcastAssistantCallback); + bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, + mBluetoothLeBroadcastAssistant); + } + } + private Boolean checkForEnabledBluetooth() { Boolean current_state = bluetoothAdapter.isEnabled(); @@ -703,6 +880,21 @@ public class BluetoothProxy { } } + if (Arrays.asList(dev.getUuids() != null ? dev.getUuids() : new ParcelUuid[0]) + .contains(ParcelUuid.fromString( + application.getString(R.string.svc_uuid_broadcast_audio)))) { + if (state_wrapper.bassData == null) + state_wrapper.bassData = new LeAudioDeviceStateWrapper.BassData(); + valid_device = true; + + if (mBluetoothLeBroadcastAssistant != null) { + boolean is_connected = mBluetoothLeBroadcastAssistant + .getConnectionState(dev) == BluetoothProfile.STATE_CONNECTED; + state_wrapper.bassData.isConnectedMutable.setValue(is_connected); + } + } + + if (valid_device) validDevices.add(state_wrapper); } @@ -822,6 +1014,88 @@ public class BluetoothProxy { } } + public void connectBass(BluetoothDevice device, boolean connect) { + if (mBluetoothLeBroadcastAssistant != null) { + if (connect) { + mBluetoothLeBroadcastAssistant.setConnectionPolicy(device, + BluetoothProfile.CONNECTION_POLICY_ALLOWED); + } else { + mBluetoothLeBroadcastAssistant.setConnectionPolicy(device, + BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); + } + } + } + + public boolean scanForBroadcasts(@NonNull BluetoothDevice onBehalfDevice, boolean scan) { + if (mBluetoothLeBroadcastAssistant != null) { + // Note: startSearchingForSources() does not support scanning on behalf of + // a specific device - it only searches for all BASS connected devices. + // Therefore, we manage the list of the devices and start/stop the scanning. + if (scan) { + mBroadcastScanOnBehalfDevices.add(onBehalfDevice); + mBluetoothLeBroadcastAssistant.startSearchingForSources(new ArrayList<>()); + if (mBassEventListener != null) { + mBassEventListener.onScanningStateChanged(true); + } + } else { + mBroadcastScanOnBehalfDevices.remove(onBehalfDevice); + if (mBroadcastScanOnBehalfDevices.isEmpty()) { + mBluetoothLeBroadcastAssistant.stopSearchingForSources(); + if (mBassEventListener != null) { + mBassEventListener.onScanningStateChanged(false); + } + } + } + return true; + } + return false; + } + + public boolean stopBroadcastObserving() { + if (mBluetoothLeBroadcastAssistant != null) { + mBroadcastScanOnBehalfDevices.clear(); + mBluetoothLeBroadcastAssistant.stopSearchingForSources(); + if (mBassEventListener != null) { + mBassEventListener.onScanningStateChanged(false); + } + return true; + } + return false; + } + + // TODO: Uncomment this method if necessary +// public boolean getBroadcastReceiverState(BluetoothDevice device, int receiver_id) { +// if (mBluetoothLeBroadcastAssistant != null) { +// return mBluetoothLeBroadcastAssistant.getBroadcastReceiverState(device, receiver_id); +// } +// return false; +// } + + public boolean addBroadcastSource(BluetoothDevice sink, BluetoothLeBroadcastMetadata sourceMetadata) { + if (mBluetoothLeBroadcastAssistant != null) { + mBluetoothLeBroadcastAssistant.addSource(sink, sourceMetadata, true /* isGroupOp */); + return true; + } + return false; + } + + public boolean modifyBroadcastSource(BluetoothDevice sink, int sourceId, + BluetoothLeBroadcastMetadata metadata) { + if (mBluetoothLeBroadcastAssistant != null) { + mBluetoothLeBroadcastAssistant.modifySource(sink, sourceId, metadata); + return true; + } + return false; + } + + public boolean removeBroadcastSource(BluetoothDevice sink, int sourceId) { + if (mBluetoothLeBroadcastAssistant != null) { + mBluetoothLeBroadcastAssistant.removeSource(sink, sourceId); + return true; + } + return false; + } + public void setVolume(BluetoothDevice device, int volume) { if (bluetoothLeAudio != null) { bluetoothLeAudio.setVolume(volume); @@ -843,7 +1117,7 @@ public class BluetoothProxy { BluetoothProfile.CONNECTION_POLICY_ALLOWED); } else { bluetoothHapClient.setConnectionPolicy(device, - BluetoothProfile.CONNECTION_POLICY_UNKNOWN); + BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); } } } @@ -1056,7 +1330,7 @@ public class BluetoothProxy { return true; } - public List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() { + public List<BluetoothLeBroadcastMetadata> getAllLocalBroadcasts() { if (mBluetoothLeBroadcast == null) return Collections.emptyList(); return mBluetoothLeBroadcast.getAllBroadcastMetadata(); } @@ -1086,4 +1360,27 @@ public class BluetoothProxy { return (bluetoothAdapter .isLeAudioBroadcastSourceSupported() == BluetoothStatusCodes.FEATURE_SUPPORTED); } + + public void setOnBassEventListener(OnBassEventListener listener) { + mBassEventListener = listener; + } + + // Used by BroadcastScanViewModel + public interface OnBassEventListener { + void onSourceFound(BluetoothLeBroadcastMetadata source); + void onScanningStateChanged(boolean isScanning); + } + + public void setOnLocalBroadcastEventListener(OnLocalBroadcastEventListener listener) { + mLocalBroadcastEventListener = listener; + } + + // Used by BroadcastScanViewModel + public interface OnLocalBroadcastEventListener { + // TODO: Add arguments in methods + void onBroadcastStarted(int broadcastId); + void onBroadcastStopped(int broadcastId); + void onBroadcastUpdated(int broadcastId); + void onBroadcastMetadataChanged(int broadcastId, BluetoothLeBroadcastMetadata metadata); + } } diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java index 215d71b7da..e98e0a9c87 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java @@ -34,12 +34,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import com.android.bluetooth.leaudio.R; - public class BroadcastItemsAdapter extends RecyclerView.Adapter<BroadcastItemsAdapter.BroadcastItemHolder> { - private List<BluetoothLeBroadcastMetadata> mBroadcastMetadata = new ArrayList<>(); - private final Map<Integer /* broadcastId */, Boolean /* isPlaying */> mBroadcastPlayback = + private List<BluetoothLeBroadcastMetadata> mBroadcastMetadataList = new ArrayList<>(); + private final Map<Integer /* broadcastId */, Boolean /* isPlaying */> mBroadcastPlaybackMap = new HashMap<>(); private OnItemClickListener mOnItemClickListener; @@ -57,8 +55,8 @@ public class BroadcastItemsAdapter @Override public void onBindViewHolder(@NonNull BroadcastItemHolder holder, int position) { - Integer broadcastId = (Integer) mBroadcastPlayback.keySet().toArray()[position]; - Boolean isPlaying = mBroadcastPlayback.get(broadcastId); + Integer broadcastId = (Integer) mBroadcastPlaybackMap.keySet().toArray()[position]; + Boolean isPlaying = mBroadcastPlaybackMap.get(broadcastId); // Set card color based on the playback state if (isPlaying) { @@ -76,33 +74,47 @@ public class BroadcastItemsAdapter @Override public int getItemCount() { - return mBroadcastPlayback.size(); + return mBroadcastPlaybackMap.size(); } public void updateBroadcastsMetadata(List<BluetoothLeBroadcastMetadata> broadcasts) { - mBroadcastMetadata = broadcasts; + mBroadcastMetadataList = broadcasts; notifyDataSetChanged(); } public void updateBroadcastMetadata(BluetoothLeBroadcastMetadata broadcast) { - mBroadcastMetadata.removeIf(bc -> (bc.getBroadcastId() == broadcast.getBroadcastId())); - mBroadcastMetadata.add(broadcast); + mBroadcastMetadataList.removeIf(bc -> (bc.getBroadcastId() == broadcast.getBroadcastId())); + mBroadcastMetadataList.add(broadcast); notifyDataSetChanged(); } public void addBroadcasts(Integer broadcastId) { - if (!mBroadcastPlayback.containsKey(broadcastId)) - mBroadcastPlayback.put(broadcastId, false); + if (!mBroadcastPlaybackMap.containsKey(broadcastId)) + mBroadcastPlaybackMap.put(broadcastId, false); } public void removeBroadcast(Integer broadcastId) { - mBroadcastMetadata.removeIf(bc -> (broadcastId.equals(bc.getBroadcastId()))); - mBroadcastPlayback.remove(broadcastId); + mBroadcastMetadataList.removeIf(bc -> (broadcastId.equals(bc.getBroadcastId()))); + mBroadcastPlaybackMap.remove(broadcastId); + notifyDataSetChanged(); + } + + public void setBroadcasts(List<BluetoothLeBroadcastMetadata> broadcasts) { + mBroadcastMetadataList.clear(); + mBroadcastMetadataList.addAll(broadcasts); + + for (BluetoothLeBroadcastMetadata b : broadcasts) { + int broadcastId = b.getBroadcastId(); + if (mBroadcastPlaybackMap.containsKey(broadcastId)) { + continue; + } + mBroadcastPlaybackMap.remove(broadcastId); + } notifyDataSetChanged(); } public void updateBroadcastPlayback(Integer broadcastId, boolean isPlaying) { - mBroadcastPlayback.put(broadcastId, isPlaying); + mBroadcastPlaybackMap.put(broadcastId, isPlaying); notifyDataSetChanged(); } @@ -125,7 +137,7 @@ public class BroadcastItemsAdapter int position = getAdapterPosition(); if (position != RecyclerView.NO_POSITION) { - Integer broadcastId = (Integer) mBroadcastPlayback.keySet().toArray()[position]; + Integer broadcastId = (Integer) mBroadcastPlaybackMap.keySet().toArray()[position]; listener.onItemClick(broadcastId); } }); diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java new file mode 100644 index 0000000000..17cf73893a --- /dev/null +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021 HIMSA II K/S - www.himsa.com. + * Represented by EHIMA - www.ehima.com + * + * 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.leaudio; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.Objects; + + +public class BroadcastScanActivity extends AppCompatActivity { + // Integer key used for sending/receiving receiver ID. + public static final String EXTRA_BASS_RECEIVER_ID = "receiver_id"; + + private static final int BIS_ALL = 0xFFFFFFFF; + + private BluetoothDevice device; + private BroadcastScanViewModel mViewModel; + private BroadcastItemsAdapter adapter; + private String mLocalBluetoothAddress; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.broadcast_scan_activity); + + RecyclerView recyclerView = findViewById(R.id.broadcast_recycler_view); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setHasFixedSize(true); + + mLocalBluetoothAddress = BluetoothAdapter.getDefaultAdapter().getAddress(); + + adapter = new BroadcastItemsAdapter(); + adapter.setOnItemClickListener(broadcastId -> { + mViewModel.scanForBroadcasts(device, false); + + BluetoothLeBroadcastMetadata broadcast = null; + for (BluetoothLeBroadcastMetadata b : mViewModel.getAllBroadcasts().getValue()) { + if (Objects.equals(b.getBroadcastId(), broadcastId)) { + broadcast = b; + break; + } + } + + if (broadcast == null) { + Toast.makeText(recyclerView.getContext(), "Matching broadcast not found." + + " broadcastId=" + broadcastId, Toast.LENGTH_SHORT).show(); + return; + } + + // TODO: Support selecting the subgroups instead of using all + mViewModel.addBroadcastSource(device, broadcast); + if (TextUtils.equals(mLocalBluetoothAddress, broadcast.getSourceDevice().getAddress())) { + Toast.makeText(recyclerView.getContext(), "Add local broadcast", + Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(recyclerView.getContext(), "Add remote broadcast", + Toast.LENGTH_SHORT).show(); + } + }); + recyclerView.setAdapter(adapter); + + mViewModel = ViewModelProviders.of(this).get(BroadcastScanViewModel.class); + mViewModel.getAllBroadcasts().observe(this, audioBroadcasts -> { + // Update Broadcast list in the adapter + adapter.setBroadcasts(audioBroadcasts); + }); + + Intent intent = getIntent(); + device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + } + + @Override + protected void onPause() { + super.onPause(); + + mViewModel.scanForBroadcasts(device, false); + } + + @Override + protected void onResume() { + super.onResume(); + + if (mViewModel.getAllBroadcasts().getValue() != null) + adapter.setBroadcasts(mViewModel.getAllBroadcasts().getValue()); + + mViewModel.scanForBroadcasts(device, true); + mViewModel.refreshBroadcasts(); + } + + @Override + public void onBackPressed() { + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + startActivity(intent); + finish(); + } +} diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanViewModel.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanViewModel.java new file mode 100644 index 0000000000..3e352e3963 --- /dev/null +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanViewModel.java @@ -0,0 +1,168 @@ +/* + * Copyright 2021 HIMSA II K/S - www.himsa.com. + * Represented by EHIMA - www.ehima.com + * + * 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.leaudio; + +import android.app.Application; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class BroadcastScanViewModel extends AndroidViewModel { + private final String TAG = "BroadcastScanViewModel"; + boolean mIsActivityScanning = false; + BluetoothDevice mOnBehalfDevice; + + // TODO: Remove these variables if they are unnecessary +// // AddBroadcast context +// BluetoothDevice mSetSrcTargetDevice; +// List<BluetoothBroadcastAudioScanBaseConfig> mSetSrcConfigs; +// boolean mSetSrcSyncPa; + + BluetoothProxy mBluetooth; + Application mApplication; + private MutableLiveData<List<BluetoothLeBroadcastMetadata>> mAllBroadcasts = new MutableLiveData<>(); + private HashMap<Integer, BluetoothLeBroadcastMetadata> mScanSessionBroadcasts = new HashMap<>(); + + private final BluetoothProxy.OnBassEventListener mBassEventListener = + new BluetoothProxy.OnBassEventListener() { + @Override + public void onSourceFound(BluetoothLeBroadcastMetadata source) { + mScanSessionBroadcasts.put(source.getBroadcastId(), source); + } + + @Override + public void onScanningStateChanged(boolean isScanning) { + if (!isScanning) { + // Update the live broadcast list and clear scan session results + List<BluetoothLeBroadcastMetadata> localSessionBroadcasts = + mBluetooth.getAllLocalBroadcasts(); + ArrayList<BluetoothLeBroadcastMetadata> new_arr; + if (localSessionBroadcasts != null) { + new_arr = new ArrayList<>(localSessionBroadcasts); + } else { + new_arr = new ArrayList<>(); + } + new_arr.addAll(mScanSessionBroadcasts.values()); + mAllBroadcasts.postValue(new_arr); + + // Continue as long as the main activity wants + if (mIsActivityScanning) { + if (mOnBehalfDevice != null) { + mBluetooth.scanForBroadcasts(mOnBehalfDevice, true); + } + } + } else { + // FIXME: Clear won't work - it would auto-update the mutable and clear it as + // mutable uses reference to its values + mScanSessionBroadcasts = new HashMap<>(); + } + } + }; + + private final BluetoothProxy.OnLocalBroadcastEventListener mLocalBroadcastEventListener = + new BluetoothProxy.OnLocalBroadcastEventListener() { + @Override + public void onBroadcastStarted(int broadcastId) { + // FIXME: We need a finer grain control over updating individual broadcast state + // and not just the entire list of broadcasts + refreshBroadcasts(); + } + + @Override + public void onBroadcastStopped(int broadcastId) { + refreshBroadcasts(); + } + + @Override + public void onBroadcastUpdated(int broadcastId) { + refreshBroadcasts(); + } + + @Override + public void onBroadcastMetadataChanged(int broadcastId, + BluetoothLeBroadcastMetadata metadata) { + refreshBroadcasts(); + } + }; + + public BroadcastScanViewModel(@NonNull Application application) { + super(application); + mApplication = application; + mBluetooth = BluetoothProxy.getBluetoothProxy(application); + + mBluetooth.setOnBassEventListener(mBassEventListener); + mBluetooth.setOnLocalBroadcastEventListener(mLocalBroadcastEventListener); + } + + @Override + public void onCleared() { + mBluetooth.setOnBassEventListener(null); + mBluetooth.setOnLocalBroadcastEventListener(null); + } + + public LiveData<List<BluetoothLeBroadcastMetadata>> getAllBroadcasts() { + return mAllBroadcasts; + } + + public void scanForBroadcasts(BluetoothDevice device, boolean scan) { + if (device == null) { + Log.e(TAG, "scanForBroadcasts: device is null. Ignoring."); + return; + } + + mIsActivityScanning = scan; + mOnBehalfDevice = scan ? device : null; + + // First update the live broadcast list + List<BluetoothLeBroadcastMetadata> localSessionBroadcasts = + mBluetooth.getAllLocalBroadcasts(); + ArrayList<BluetoothLeBroadcastMetadata> new_arr; + if (localSessionBroadcasts != null) { + new_arr = new ArrayList<>(localSessionBroadcasts); + } else { + new_arr = new ArrayList<>(); + } + new_arr.addAll(mScanSessionBroadcasts.values()); + mAllBroadcasts.postValue(new_arr); + + mBluetooth.scanForBroadcasts(device, scan); + } + + public void addBroadcastSource(BluetoothDevice sink, BluetoothLeBroadcastMetadata sourceMetadata) { + mBluetooth.addBroadcastSource(sink, sourceMetadata); + } + + public void refreshBroadcasts() { + // Concatenate local broadcasts to the scanned broadcast list + List<BluetoothLeBroadcastMetadata> localSessionBroadcasts = + mBluetooth.getAllLocalBroadcasts(); + ArrayList<BluetoothLeBroadcastMetadata> new_arr = new ArrayList<>( + localSessionBroadcasts); + new_arr.addAll(mScanSessionBroadcasts.values()); + mAllBroadcasts.postValue(new_arr); + } +} diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java index 94d4948930..bf22c8e73e 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java @@ -56,11 +56,11 @@ public class BroadcasterViewModel extends AndroidViewModel { } public List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() { - return mBluetooth.getAllBroadcastMetadata(); + return mBluetooth.getAllLocalBroadcasts(); } public int getBroadcastCount() { - return mBluetooth.getAllBroadcastMetadata().size(); + return mBluetooth.getAllLocalBroadcasts().size(); } public LiveData<BluetoothLeBroadcastMetadata> getBroadcastUpdateMetadataLive() { diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java index e94885684d..3183d6be3a 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java @@ -19,6 +19,8 @@ package com.android.bluetooth.leaudio; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapPresetInfo; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.BluetoothLeBroadcastSubgroup; import androidx.core.util.Pair; import androidx.lifecycle.MutableLiveData; @@ -100,16 +102,10 @@ public class LeAudioDeviceStateWrapper { public Object viewsData = null; } - public static class ReceiverState { - int receiver_id; - int state; - } - public static class BassData { - public MutableLiveData<Boolean> isValidBassDevice = new MutableLiveData<>(); public MutableLiveData<Boolean> isConnectedMutable = new MutableLiveData<>(); - public MutableLiveData<HashMap<Integer, ReceiverState>> receiverStatesMutable = - new MutableLiveData<>(); + public MutableLiveData<HashMap<Integer, BluetoothLeBroadcastReceiveState>> + receiverStatesMutable = new MutableLiveData<>(); public Object viewsData = null; } diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java index 6c8384b26a..74ae408528 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java @@ -17,17 +17,42 @@ package com.android.bluetooth.leaudio; +import static android.bluetooth.BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE; +import static android.bluetooth.BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_CODE_REQUIRED; +import static android.bluetooth.BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING; +import static android.bluetooth.BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_NOT_ENCRYPTED; +import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_FAILED_TO_SYNCHRONIZE; +import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE; +import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED; +import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCINFO_REQUEST; + +import static com.android.bluetooth.leaudio.BroadcastScanActivity.EXTRA_BASS_RECEIVER_ID; + import android.animation.ObjectAnimator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothLeAudio; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.content.Intent; import android.content.res.Configuration; +import android.content.res.Resources; import android.os.ParcelUuid; +import android.text.InputFilter; import android.text.InputType; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.*; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.NumberPicker; +import android.widget.SeekBar; +import android.widget.Spinner; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -39,14 +64,11 @@ import androidx.recyclerview.widget.RecyclerView; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; -import java.util.BitSet; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; -import com.android.bluetooth.leaudio.R; - public class LeAudioRecycleViewAdapter extends RecyclerView.Adapter<LeAudioRecycleViewAdapter.ViewHolder> { private final AppCompatActivity parent; @@ -99,6 +121,10 @@ public class LeAudioRecycleViewAdapter holder.itemView.findViewById(R.id.hap_switch).setEnabled( Arrays.asList(leAudioDeviceStateWrapper.device.getUuids()).contains( ParcelUuid.fromString(parent.getString(R.string.svc_uuid_has)))); + + holder.itemView.findViewById(R.id.bass_switch) + .setEnabled(Arrays.asList(leAudioDeviceStateWrapper.device.getUuids()) + .contains(ParcelUuid.fromString(parent.getString(R.string.svc_uuid_broadcast_audio)))); } } @@ -108,6 +134,7 @@ public class LeAudioRecycleViewAdapter setVolumeControlUiStateObservers(holder, leAudioDeviceStateWrapper); setBassStateObservers(holder, leAudioDeviceStateWrapper); setHasStateObservers(holder, leAudioDeviceStateWrapper); + setBassUiStateObservers(holder, leAudioDeviceStateWrapper); } private void setLeAudioStateObservers(@NonNull ViewHolder holder, @@ -626,6 +653,80 @@ public class LeAudioRecycleViewAdapter } } + private void setBassUiStateObservers(@NonNull ViewHolder holder, LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) { + if (leAudioDeviceStateWrapper.bassData == null) + return; + + ViewHolderBassPersistentData vData = (ViewHolderBassPersistentData)leAudioDeviceStateWrapper.bassData.viewsData; + if (vData == null) + return; + + if (vData.selectedReceiverPositionMutable.hasObservers()) + vData.selectedReceiverPositionMutable.removeObservers(this.parent); + + vData.selectedReceiverPositionMutable.observe(this.parent, aInteger -> { + int receiver_id = Integer.parseInt(holder.bassReceiverIdSpinner.getItemAtPosition(aInteger).toString()); + bassInteractionListener.onReceiverSelected(leAudioDeviceStateWrapper, receiver_id); + + Map<Integer, BluetoothLeBroadcastReceiveState> states = + leAudioDeviceStateWrapper.bassData.receiverStatesMutable.getValue(); + + if (states != null) { + if (states.containsKey(receiver_id)) { + BluetoothLeBroadcastReceiveState state = + states.get(holder.bassReceiverIdSpinner.getSelectedItem()); + final int paSyncState = state.getPaSyncState(); + final int bigEncryptionState = state.getBigEncryptionState(); + + Resources res = this.parent.getResources(); + String stateName = null; + + // Set the icon + if (paSyncState == PA_SYNC_STATE_IDLE) { + holder.bassScanButton.setImageResource(R.drawable.ic_cast_black_24dp); + stateName = res.getString(R.string.broadcast_state_idle); + } else if (paSyncState == PA_SYNC_STATE_FAILED_TO_SYNCHRONIZE) { + holder.bassScanButton.setImageResource(R.drawable.ic_warning_black_24dp); + stateName = res.getString(R.string.broadcast_state_sync_pa_failed); + } else if (paSyncState == PA_SYNC_STATE_SYNCHRONIZED) { + switch (bigEncryptionState) { + case BIG_ENCRYPTION_STATE_NOT_ENCRYPTED: + case BIG_ENCRYPTION_STATE_DECRYPTING: + holder.bassScanButton.setImageResource( + R.drawable.ic_bluetooth_searching_black_24dp); + stateName = res.getString(R.string.broadcast_state_receiving_broadcast); + break; + case BIG_ENCRYPTION_STATE_CODE_REQUIRED: + holder.bassScanButton.setImageResource( + R.drawable.ic_vpn_key_black_24dp); + stateName = res.getString(R.string.broadcast_state_code_required); + break; + case BIG_ENCRYPTION_STATE_BAD_CODE: + holder.bassScanButton.setImageResource(R.drawable.ic_warning_black_24dp); + stateName = res.getString(R.string.broadcast_state_code_invalid); + break; + } + } + + // TODO: Seems no appropriate state matching exists for RECEIVER_STATE_SYNCING + // and RECEIVER_STATE_SET_SOURCE_FAILED. + // What does "receiver source configuration has failed" mean? +// else if (state == BluetoothBroadcastAudioScan.RECEIVER_STATE_SYNCING) { +// holder.bassScanButton.setImageResource(R.drawable.ic_bluetooth_dots_black); +// stateName = res.getString(R.string.broadcast_state_syncing); +// } +// } else if (state == BluetoothBroadcastAudioScan.RECEIVER_STATE_SET_SOURCE_FAILED) { +// holder.bassScanButton.setImageResource(R.drawable.ic_refresh_black_24dp); +// stateName = res.getString(R.string.broadcast_state_set_source_failed); +// } + + holder.bassReceiverStateText.setText( + stateName != null ? stateName : res.getString(R.string.unknown)); + } + } + }); + } + @Override public long getItemId(int position) { return devices.get(position).device.getAddress().hashCode(); @@ -766,21 +867,6 @@ public class LeAudioRecycleViewAdapter int output_id, String description); } - public interface OnBassInteractionListener { - void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper); - - void onDisconnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper); - - void onReceiverSelected(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, - int receiver_id); - - void onStopSyncReq(BluetoothDevice device, int receiver_id); - - void onRemoveSourceReq(BluetoothDevice device, int receiver_id); - - void onStopObserving(); - } - public interface OnHapInteractionListener { void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper); @@ -801,6 +887,22 @@ public class LeAudioRecycleViewAdapter void onPreviousGroupPresetClicked(BluetoothDevice device); } + public interface OnBassInteractionListener { + void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper); + + void onDisconnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper); + + void onReceiverSelected(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int receiver_id); + + void onBroadcastCodeEntered(BluetoothDevice device, int receiver_id, byte[] broadcast_code); + + void onStopSyncReq(BluetoothDevice device, int receiver_id); + + void onRemoveSourceReq(BluetoothDevice device, int receiver_id); + + void onStopObserving(); + } + public class ViewHolder extends RecyclerView.ViewHolder { private final TextView deviceName; @@ -880,6 +982,7 @@ public class LeAudioRecycleViewAdapter private Switch bassConnectionSwitch; private Spinner bassReceiverIdSpinner; private TextView bassReceiverStateText; + private ImageButton bassScanButton; public ViewHolder(@NonNull View itemView) { super(itemView); @@ -888,6 +991,7 @@ public class LeAudioRecycleViewAdapter SetupLeAudioView(itemView); setupVcView(itemView); setupHapView(itemView); + setupBassView(itemView); // Notify viewmodel via parent's click listener itemView.setOnClickListener(view -> { @@ -1606,6 +1710,147 @@ public class LeAudioRecycleViewAdapter } }); } + + private void setupBassView(@NonNull View itemView) { + bassConnectionSwitch = itemView.findViewById(R.id.bass_switch); + bassConnectionSwitch.setActivated(true); + bassReceiverIdSpinner = itemView.findViewById(R.id.num_receiver_spinner); + bassReceiverStateText = itemView.findViewById(R.id.receiver_state_text); + bassScanButton = itemView.findViewById(R.id.broadcast_button); + + bassConnectionSwitch.setOnCheckedChangeListener((compoundButton, b) -> { + if (!compoundButton.isActivated()) + return; + + if (bassInteractionListener != null) { + if (b) + bassInteractionListener.onConnectClick( + devices.get(ViewHolder.this.getAdapterPosition())); + else + bassInteractionListener.onDisconnectClick( + devices.get(ViewHolder.this.getAdapterPosition())); + } + }); + + bassReceiverIdSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> adapterView, View view, int position, long l) { + LeAudioDeviceStateWrapper device = devices.get(ViewHolder.this.getAdapterPosition()); + ((ViewHolderBassPersistentData) device.bassData.viewsData).selectedReceiverPositionMutable.setValue(position); + } + + @Override + public void onNothingSelected(AdapterView<?> adapterView) { + // Nothing to do here + } + }); + + bassScanButton.setOnClickListener(view -> { + Resources res = view.getResources(); + + // TODO: Do not sync on the string value, but instead sync on the actual state value. + if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_idle))) { + AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext()); + alert.setTitle("Scan and add a source or remove the currently set one."); + + BluetoothDevice device = devices.get(ViewHolder.this.getAdapterPosition()).device; + int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString()); + + alert.setPositiveButton("Scan", (dialog, whichButton) -> { + // Scan for new announcements + Intent intent = new Intent(this.itemView.getContext(), BroadcastScanActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + intent.putExtra(EXTRA_BASS_RECEIVER_ID, receiver_id); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, devices.get(ViewHolder.this.getAdapterPosition()).device); + parent.startActivityForResult(intent, 666); + }); + alert.setNeutralButton("Cancel", (dialog, whichButton) -> { + // Do nothing + }); + alert.setNegativeButton("Remove", (dialog, whichButton) -> { + bassInteractionListener.onRemoveSourceReq(device, receiver_id); + }); + alert.show(); + + } else if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_code_required))) { + AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext()); + alert.setTitle("Please enter broadcast encryption code..."); + EditText pass_input_view = new EditText(itemView.getContext()); + pass_input_view.setFilters(new InputFilter[] { new InputFilter.LengthFilter(16) }); + alert.setView(pass_input_view); + + BluetoothDevice device = devices.get(ViewHolder.this.getAdapterPosition()).device; + int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString()); + + alert.setPositiveButton("Set", (dialog, whichButton) -> { + byte[] code = pass_input_view.getText().toString().getBytes(); + bassInteractionListener.onBroadcastCodeEntered(device, receiver_id, code); + }); + alert.setNegativeButton("Cancel", (dialog, whichButton) -> { + // Do nothing + }); + alert.show(); + + } else if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_receiving_broadcast))) { + AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext()); + alert.setTitle("Stop the synchronization?"); + + BluetoothDevice device = devices.get(ViewHolder.this.getAdapterPosition()).device; + int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString()); + + alert.setPositiveButton("Yes", (dialog, whichButton) -> { + bassInteractionListener.onRemoveSourceReq(device, receiver_id); + }); + // FIXME: To modify source we need the valid broadcaster_id context so we should start scan here again + // alert.setNeutralButton("Modify", (dialog, whichButton) -> { + // // TODO: Open the scan dialog to get the broadcast_id + // // bassInteractionListener.onStopSyncReq(device, receiver_id, broadcast_id); + // }); + alert.setNegativeButton("No", (dialog, whichButton) -> { + // Do nothing + }); + alert.show(); + + } else if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_set_source_failed)) + || bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_sync_pa_failed))) { + AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext()); + alert.setTitle("Retry broadcast audio announcement scan?"); + + alert.setPositiveButton("Yes", (dialog, whichButton) -> { + // Scan for new announcements + Intent intent = new Intent(view.getContext(), BroadcastScanActivity.class); + int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString()); + intent.putExtra(EXTRA_BASS_RECEIVER_ID, receiver_id); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, devices.get(ViewHolder.this.getAdapterPosition()).device); + parent.startActivityForResult(intent, 666); + }); + alert.setNegativeButton("No", (dialog, whichButton) -> { + // Do nothing + }); + alert.show(); + + } else if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_syncing))) { + AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext()); + alert.setTitle("Stop the synchronization?"); + + BluetoothDevice device = devices.get(ViewHolder.this.getAdapterPosition()).device; + int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString()); + + alert.setPositiveButton("Yes", (dialog, whichButton) -> { + bassInteractionListener.onRemoveSourceReq(device, receiver_id); + }); + // FIXME: To modify source we need the valid broadcaster_id context so we should start scan here again + // alert.setNeutralButton("Modify", (dialog, whichButton) -> { + // // TODO: Open the scan dialog to get the broadcast_id + // // bassInteractionListener.onStopSyncReq(device, receiver_id, broadcast_id); + // }); + alert.setNegativeButton("No", (dialog, whichButton) -> { + // Do nothing + }); + alert.show(); + } + }); + } } private class ViewHolderVcPersistentData { diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java index 3d6bf34fc8..ae31538b32 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java @@ -19,6 +19,7 @@ package com.android.bluetooth.leaudio; import android.app.Application; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; @@ -115,4 +116,37 @@ public class LeAudioViewModel extends AndroidViewModel { public boolean isLeAudioBroadcastSourceSupported() { return bluetoothProxy.isLeAudioBroadcastSourceSupported(); } + + public void connectBass(BluetoothDevice sink, boolean connect) { + bluetoothProxy.connectBass(sink, connect); + } + + public boolean stopBroadcastObserving() { + return bluetoothProxy.stopBroadcastObserving(); + } + + // TODO: Uncomment this method if necessary +// public boolean getBroadcastReceiverState(BluetoothDevice device, int receiver_id) { +// return bluetoothProxy.getBroadcastReceiverState(device, receiver_id); +// } + + // TODO: Uncomment this method if necessary +// public boolean modifyBroadcastSource(BluetoothDevice device, int receiver_id, boolean sync_pa, +// List<BluetoothBroadcastAudioScanBaseConfig> configs) { +// return bluetoothProxy.modifyBroadcastSource(device, receiver_id, sync_pa, configs); +// } + + public boolean removeBroadcastSource(BluetoothDevice sink, int receiver_id) { + // TODO: Find source ID from receiver_id. What is receiver_id? + int sourceId = 0; + return bluetoothProxy.removeBroadcastSource(sink, sourceId); + } + + public boolean setBroadcastCode(BluetoothDevice sink, int receiver_id, byte[] bcast_code) { + // TODO: Find source ID from receiver_id. What is receiver_id? + // TODO: Build BluetoothLeBroadcastMetadata with the new bcast_code. + int sourceId = 0; + BluetoothLeBroadcastMetadata metadata = null; + return bluetoothProxy.modifyBroadcastSource(sink, sourceId, metadata); + } } diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java index 33410073ee..fa7e8c45bc 100644 --- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java +++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java @@ -136,6 +136,21 @@ public class MainActivity extends AppCompatActivity { Intent intent = null; switch (item.getItemId()) { + case R.id.action_scan: + intent = new Intent(MainActivity.this, BroadcastScanActivity.class); + // TODO: Why does this pass no information? + //intent.putExtra(BluetoothBroadcastAudioScan.EXTRA_BASS_RECEIVER_ID, 0); + + // TODO: Change BluetoothAdapter.getDefaultAdapter() usages into BluetoothManager#getAdapter(). + BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter(); + + // What does this fake address mean? + byte[] address = {(byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff}; + BluetoothDevice dev = mAdapter.getRemoteDevice(address); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, dev); + startActivity(intent); + return true; + case R.id.action_broadcast: if (leAudioViewModel.getBluetoothEnabledLive().getValue() == null || !leAudioViewModel.getBluetoothEnabledLive().getValue()) { @@ -168,6 +183,9 @@ public class MainActivity extends AppCompatActivity { Toast.makeText(MainActivity.this, message + "(" + resultCode + ")", Toast.LENGTH_SHORT).show(); } + + // TODO: Depending on the resultCode we should either stop the sync or try the PAST + leAudioViewModel.stopBroadcastObserving(); } } @@ -548,12 +566,72 @@ public class MainActivity extends AppCompatActivity { .show(); } }); + + recyclerViewAdapter.setOnBassInteractionListener( + new LeAudioRecycleViewAdapter.OnBassInteractionListener() { + @Override + public void onConnectClick( + LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) { + Toast.makeText(MainActivity.this, + "Connecting BASS to " + leAudioDeviceStateWrapper.device.toString(), + Toast.LENGTH_SHORT).show(); + leAudioViewModel.connectBass(leAudioDeviceStateWrapper.device, true); + } + + @Override + public void onDisconnectClick( + LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) { + Toast.makeText(MainActivity.this, + "Disconnecting BASS from " + + leAudioDeviceStateWrapper.device.toString(), + Toast.LENGTH_SHORT).show(); + leAudioViewModel.connectBass(leAudioDeviceStateWrapper.device, false); + } + + @Override + public void onReceiverSelected( + LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int receiver_id) { + // Do nothing here, the UI is updated elsewhere and we already have the + // latest state value as well + } + + @Override + public void onBroadcastCodeEntered(BluetoothDevice device, int receiver_id, + byte[] broadcast_code) { + leAudioViewModel.setBroadcastCode(device, receiver_id, broadcast_code); + } + + @Override + public void onStopSyncReq(BluetoothDevice device, int receiver_id) { + // TODO: When is onStopSyncReq called? and what does below code do? + +// List<BluetoothBroadcastAudioScanBaseConfig> configs = new ArrayList<>(); +// // JT@CC: How come you can call this with null metadata when the +// // constructor has the @Nonull annotation for the param? +// BluetoothBroadcastAudioScanBaseConfig stop_config = +// new BluetoothBroadcastAudioScanBaseConfig(0, new byte[] {}); +// configs.add(stop_config); +// +// leAudioViewModel.modifyBroadcastSource(device, receiver_id, false, configs); + } + + @Override + public void onRemoveSourceReq(BluetoothDevice device, int receiver_id) { + leAudioViewModel.removeBroadcastSource(device, receiver_id); + } + + @Override + public void onStopObserving() { + leAudioViewModel.stopBroadcastObserving(); + } + }); } private void cleanupViewsProfileUiEventListeners() { recyclerViewAdapter.setOnLeAudioInteractionListener(null); recyclerViewAdapter.setOnVolumeControlInteractionListener(null); recyclerViewAdapter.setOnHapInteractionListener(null); + recyclerViewAdapter.setOnBassInteractionListener(null); } // This sets the initial values and set up the observers diff --git a/android/leaudio/app/src/main/res/layout/broadcast_scan_activity.xml b/android/leaudio/app/src/main/res/layout/broadcast_scan_activity.xml new file mode 100644 index 0000000000..0d66e0a374 --- /dev/null +++ b/android/leaudio/app/src/main/res/layout/broadcast_scan_activity.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".BroadcastScanActivity"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/scan_status_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="8dp" + android:layout_marginTop="8dp" + android:text="Scanning for broadcasts..." /> + + <ProgressBar + android:id="@+id/progressBar" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:indeterminate="true" + android:indeterminateBehavior="cycle" + android:indeterminateOnly="true" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/broadcast_recycler_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:listitem="@layout/broadcast_item" /> + </LinearLayout> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/android/leaudio/app/src/main/res/values/strings.xml b/android/leaudio/app/src/main/res/values/donottranslate_strings.xml index 45d0add8da..0b7a060d99 100644 --- a/android/leaudio/app/src/main/res/values/strings.xml +++ b/android/leaudio/app/src/main/res/values/donottranslate_strings.xml @@ -79,6 +79,13 @@ </string-array> <string name="group_locked">LOCKED</string> <string name="group_unlocked">UNLOCKED</string> + <string name="broadcast_state_idle">IDLE</string> + <string name="broadcast_state_set_source_failed">SET_SOURCE_FAILED</string> + <string name="broadcast_state_syncing">SYNCING</string> + <string name="broadcast_state_sync_pa_failed">SYNC_PA_FAILED</string> + <string name="broadcast_state_code_required">BROADCAST_CODE_REQUIRED</string> + <string name="broadcast_state_code_invalid">BROADCAST_CODE_INVALID</string> + <string name="broadcast_state_receiving_broadcast">RECEIVING_BROADCAST</string> <string-array name="tbs_call_states"> <item>Incoming</item> <item>Dialing</item> diff --git a/framework/java/android/bluetooth/BluetoothHapClient.java b/framework/java/android/bluetooth/BluetoothHapClient.java index 5416d73610..1d51670ffe 100644 --- a/framework/java/android/bluetooth/BluetoothHapClient.java +++ b/framework/java/android/bluetooth/BluetoothHapClient.java @@ -644,6 +644,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable public boolean setConnectionPolicy(@NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) { if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); + Objects.requireNonNull(device, "BluetoothDevice cannot be null"); final IBluetoothHapClient service = getService(); final boolean defaultValue = false; if (service == null) { @@ -673,7 +674,8 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable * {@link #CONNECTION_POLICY_UNKNOWN} * * @param device Bluetooth device - * @return connection policy of the device + * @return connection policy of the device or {@link #CONNECTION_POLICY_FORBIDDEN} if device is + * null * @hide */ @SystemApi diff --git a/service/java/com/android/server/bluetooth/BluetoothManagerService.java b/service/java/com/android/server/bluetooth/BluetoothManagerService.java index a1efcf3c63..4ee392f08d 100644 --- a/service/java/com/android/server/bluetooth/BluetoothManagerService.java +++ b/service/java/com/android/server/bluetooth/BluetoothManagerService.java @@ -1078,7 +1078,8 @@ public class BluetoothManagerService extends IBluetoothManager.Stub { public boolean enableBle(AttributionSource attributionSource, IBinder token) throws RemoteException { final String packageName = attributionSource.getPackageName(); - if (!checkBluetoothPermissions(attributionSource, "enableBle", false)) { + if (!checkBluetoothPermissions(attributionSource, "enableBle", false) + || isAirplaneModeOn()) { if (DBG) { Log.d(TAG, "enableBle(): bluetooth disallowed"); } diff --git a/system/blueberry/tests/gd/cert/matchers.py b/system/blueberry/tests/gd/cert/matchers.py index 95dbe4dcfc..df15d7d4e4 100644 --- a/system/blueberry/tests/gd/cert/matchers.py +++ b/system/blueberry/tests/gd/cert/matchers.py @@ -242,7 +242,7 @@ class NeighborMatchers(object): inquiry_view = hci_packets.InquiryResultView(hci_event) if inquiry_view is None: return False - results = inquiry_view.GetInquiryResults() + results = inquiry_view.GetResponses() return any((address == result.bd_addr for result in results)) @staticmethod @@ -257,7 +257,7 @@ class NeighborMatchers(object): inquiry_view = hci_packets.InquiryResultWithRssiView(hci_event) if inquiry_view is None: return False - results = inquiry_view.GetInquiryResults() + results = inquiry_view.GetResponses() return any((address == result.address for result in results)) @staticmethod diff --git a/system/bta/include/bta_api.h b/system/bta/include/bta_api.h index b3049c80ac..b98c8e4b5a 100644 --- a/system/bta/include/bta_api.h +++ b/system/bta/include/bta_api.h @@ -456,7 +456,7 @@ typedef struct { BD_NAME bd_name; /* Name of peer device. */ tBTA_SERVICE_MASK services; /* Services found on peer device. */ tBT_DEVICE_TYPE device_type; /* device type in case it is BLE device */ - uint32_t num_uuids; + size_t num_uuids; bluetooth::Uuid* p_uuid_list; tBTA_STATUS result; } tBTA_DM_DISC_RES; diff --git a/system/bta/le_audio/client.cc b/system/bta/le_audio/client.cc index fe0a4d06f0..0e46dc3443 100644 --- a/system/bta/le_audio/client.cc +++ b/system/bta/le_audio/client.cc @@ -37,6 +37,7 @@ #include "embdrv/lc3/include/lc3.h" #include "gatt/bta_gattc_int.h" #include "gd/common/strings.h" +#include "le_audio_set_configuration_provider.h" #include "le_audio_types.h" #include "osi/include/log.h" #include "osi/include/osi.h" @@ -3628,6 +3629,7 @@ void LeAudioClient::DebugDump(int fd) { LeAudioClientAudioSource::DebugDump(fd); LeAudioClientAudioSink::DebugDump(fd); + le_audio::AudioSetConfigurationProvider::Get()->DebugDump(fd); dprintf(fd, "\n"); } diff --git a/system/bta/le_audio/le_audio_set_configuration_provider.h b/system/bta/le_audio/le_audio_set_configuration_provider.h index 679135942e..7310868d68 100644 --- a/system/bta/le_audio/le_audio_set_configuration_provider.h +++ b/system/bta/le_audio/le_audio_set_configuration_provider.h @@ -28,6 +28,7 @@ class AudioSetConfigurationProvider { virtual ~AudioSetConfigurationProvider() = default; static AudioSetConfigurationProvider* Get(); static void Initialize(); + static void DebugDump(int fd); static void Cleanup(); virtual const set_configurations::AudioSetConfigurations* GetConfigurations( ::le_audio::types::LeAudioContextType content_type) const; diff --git a/system/bta/le_audio/le_audio_set_configuration_provider_json.cc b/system/bta/le_audio/le_audio_set_configuration_provider_json.cc index b2fa48156f..6e04c29f99 100644 --- a/system/bta/le_audio/le_audio_set_configuration_provider_json.cc +++ b/system/bta/le_audio/le_audio_set_configuration_provider_json.cc @@ -461,6 +461,36 @@ struct AudioSetConfigurationProvider::impl { bool IsRunning() { return config_provider_impl_ ? true : false; } + void Dump(int fd) { + std::stringstream stream; + + for (LeAudioContextType context : types::kLeAudioContextAllTypesArray) { + auto confs = Get()->GetConfigurations(context); + stream << " === Configurations for context type: " << (int)context + << ", num: " << (confs == nullptr ? 0 : confs->size()) << " \n"; + if (confs->size() > 0) { + for (const auto& conf : *confs) { + stream << " name: " << conf->name << " \n"; + for (const auto& ent : conf->confs) { + stream << " direction: " + << (ent.direction == types::kLeAudioDirectionSink + ? "Sink (speaker)\n" + : "Source (mic)\n") + << " number of devices: " << +ent.device_cnt << " \n" + << " number of ASEs: " << +ent.ase_cnt << " \n" + << " target latency: " << +ent.target_latency << " \n" + << " strategy: " << (int)(ent.strategy) << " \n" + << " qos->retransmission_number: " + << +ent.qos.retransmission_number << " \n" + << " qos->max_transport_latency: " + << +ent.qos.max_transport_latency << " \n"; + } + } + } + } + dprintf(fd, "%s", stream.str().c_str()); + } + const AudioSetConfigurationProvider& config_provider_; std::unique_ptr<AudioSetConfigurationProviderJson> config_provider_impl_; }; @@ -478,6 +508,21 @@ void AudioSetConfigurationProvider::Initialize() { config_provider->pimpl_->Initialize(); } +void AudioSetConfigurationProvider::DebugDump(int fd) { + if (!config_provider || !config_provider->pimpl_->IsRunning()) { + dprintf( + fd, + "\n AudioSetConfigurationProvider not initialized: config provider: " + "%d, pimpl: %d \n", + config_provider != nullptr, + (config_provider == nullptr ? 0 + : config_provider->pimpl_->IsRunning())); + return; + } + dprintf(fd, "\n AudioSetConfigurationProvider: \n"); + config_provider->pimpl_->Dump(fd); +} + void AudioSetConfigurationProvider::Cleanup() { if (!config_provider) return; if (config_provider->pimpl_->IsRunning()) config_provider->pimpl_->Cleanup(); diff --git a/system/gd/btaa/linux_generic/hci_processor.cc b/system/gd/btaa/linux_generic/hci_processor.cc index 60c5969bc9..28c1e70b19 100644 --- a/system/gd/btaa/linux_generic/hci_processor.cc +++ b/system/gd/btaa/linux_generic/hci_processor.cc @@ -74,7 +74,7 @@ void HciProcessor::process_special_event( if (!packet_view.IsValid()) { return; } - auto inquiry_results = packet_view.GetInquiryResults(); + auto inquiry_results = packet_view.GetResponses(); avg_byte_count = byte_count / inquiry_results.size(); for (auto& inquiry_result : inquiry_results) { btaa_hci_packets.push_back(BtaaHciPacket(Activity::SCAN, inquiry_result.bd_addr_, avg_byte_count)); diff --git a/system/gd/hci/facade/le_scanning_manager_facade.cc b/system/gd/hci/facade/le_scanning_manager_facade.cc index 8e583e218b..ea0d058510 100644 --- a/system/gd/hci/facade/le_scanning_manager_facade.cc +++ b/system/gd/hci/facade/le_scanning_manager_facade.cc @@ -124,8 +124,8 @@ class LeScanningManagerFacadeService : public LeScanningManagerFacade::Service, uint16_t periodic_advertising_interval, std::vector<uint8_t> advertising_data) { AdvertisingReportMsg advertising_report_msg; - std::vector<LeExtendedAdvertisingReport> advertisements; - LeExtendedAdvertisingReport le_extended_advertising_report; + std::vector<LeExtendedAdvertisingResponse> advertisements; + LeExtendedAdvertisingResponse le_extended_advertising_report; le_extended_advertising_report.address_type_ = (DirectAdvertisingAddressType)address_type; le_extended_advertising_report.address_ = address; le_extended_advertising_report.advertising_data_ = advertising_data; diff --git a/system/gd/hci/hci_packets.pdl b/system/gd/hci/hci_packets.pdl index 2051261fc0..ed0a5318e3 100644 --- a/system/gd/hci/hci_packets.pdl +++ b/system/gd/hci/hci_packets.pdl @@ -4959,7 +4959,7 @@ packet InquiryComplete : Event (event_code = INQUIRY_COMPLETE) { status : ErrorCode, } -struct InquiryResult { +struct InquiryResponse { bd_addr : Address, page_scan_repetition_mode : PageScanRepetitionMode, _reserved_ : 8, @@ -4970,8 +4970,8 @@ struct InquiryResult { } packet InquiryResult : Event (event_code = INQUIRY_RESULT) { - _count_(inquiry_results) : 8, - inquiry_results : InquiryResult[], + _count_(responses) : 8, + responses : InquiryResponse[], } enum LinkType : 8 { @@ -5197,7 +5197,7 @@ packet FlowSpecificationComplete : Event (event_code = FLOW_SPECIFICATION_COMPLE access_latency : 32, // Octets/s } -struct InquiryResultWithRssi { +struct InquiryResponseWithRssi { address : Address, page_scan_repetition_mode : PageScanRepetitionMode, _reserved_ : 8, @@ -5208,8 +5208,8 @@ struct InquiryResultWithRssi { } packet InquiryResultWithRssi : Event (event_code = INQUIRY_RESULT_WITH_RSSI) { - _count_(inquiry_results) : 8, - inquiry_results : InquiryResultWithRssi[], + _count_(responses) : 8, + responses : InquiryResponseWithRssi[], } packet ReadRemoteExtendedFeaturesComplete : Event (event_code = READ_REMOTE_EXTENDED_FEATURES_COMPLETE) { @@ -5403,7 +5403,7 @@ enum AdvertisingEventType : 8 { SCAN_RESPONSE = 0x04, } -struct LeAdvertisingReport { +struct LeAdvertisingResponse { event_type : AdvertisingEventType, address_type : AddressType, address : Address, @@ -5413,11 +5413,11 @@ struct LeAdvertisingReport { } packet LeAdvertisingReport : LeMetaEvent (subevent_code = ADVERTISING_REPORT) { - _count_(advertising_reports) : 8, - advertising_reports : LeAdvertisingReport[], + _count_(responses) : 8, + responses : LeAdvertisingResponse[], } -struct LeAdvertisingReportRaw { +struct LeAdvertisingResponseRaw { event_type : AdvertisingEventType, address_type : AddressType, address : Address, @@ -5427,8 +5427,8 @@ struct LeAdvertisingReportRaw { } packet LeAdvertisingReportRaw : LeMetaEvent (subevent_code = ADVERTISING_REPORT) { - _count_(advertising_reports) : 8, - advertising_reports : LeAdvertisingReportRaw[], + _count_(responses) : 8, + responses : LeAdvertisingResponseRaw[], } packet LeConnectionUpdateComplete : LeMetaEvent (subevent_code = CONNECTION_UPDATE_COMPLETE) { @@ -5514,7 +5514,7 @@ enum DirectAddressType : 8 { RANDOM_DEVICE_ADDRESS = 0x01, } -struct LeDirectedAdvertisingReport { +struct LeDirectedAdvertisingResponse { event_type : DirectAdvertisingEventType, address_type : DirectAdvertisingAddressType, address : Address, @@ -5524,8 +5524,8 @@ struct LeDirectedAdvertisingReport { } packet LeDirectedAdvertisingReport : LeMetaEvent (subevent_code = DIRECTED_ADVERTISING_REPORT) { - _count_(advertising_reports) : 8, - advertising_reports : LeDirectedAdvertisingReport[], + _count_(responses) : 8, + responses : LeDirectedAdvertisingResponse[], } packet LePhyUpdateComplete : LeMetaEvent (subevent_code = PHY_UPDATE_COMPLETE) { @@ -5543,7 +5543,7 @@ enum DataStatus : 2 { RESERVED = 0x3, } -struct LeExtendedAdvertisingReport { +struct LeExtendedAdvertisingResponse { connectable : 1, scannable : 1, directed : 1, @@ -5566,8 +5566,8 @@ struct LeExtendedAdvertisingReport { } packet LeExtendedAdvertisingReport : LeMetaEvent (subevent_code = EXTENDED_ADVERTISING_REPORT) { - _count_(advertising_reports) : 8, - advertising_reports : LeExtendedAdvertisingReport[], + _count_(responses) : 8, + responses : LeExtendedAdvertisingResponse[], } packet LePeriodicAdvertisingSyncEstablished : LeMetaEvent (subevent_code = PERIODIC_ADVERTISING_SYNC_ESTABLISHED) { diff --git a/system/gd/hci/le_scanning_manager.cc b/system/gd/hci/le_scanning_manager.cc index 5a742c8578..6ba1c42862 100644 --- a/system/gd/hci/le_scanning_manager.cc +++ b/system/gd/hci/le_scanning_manager.cc @@ -304,13 +304,13 @@ struct LeScanningManager::impl : public bluetooth::hci::LeAddressManagerCallback LOG_INFO("Dropping invalid advertising event"); return; } - std::vector<LeAdvertisingReport> reports = event_view.GetAdvertisingReports(); + std::vector<LeAdvertisingResponse> reports = event_view.GetResponses(); if (reports.empty()) { LOG_INFO("Zero results in advertising event"); return; } - for (LeAdvertisingReport report : reports) { + for (LeAdvertisingResponse report : reports) { uint16_t extended_event_type = 0; switch (report.event_type_) { case hci::AdvertisingEventType::ADV_IND: @@ -362,7 +362,7 @@ struct LeScanningManager::impl : public bluetooth::hci::LeAddressManagerCallback LOG_INFO("Dropping invalid advertising event"); return; } - std::vector<LeDirectedAdvertisingReport> reports = event_view.GetAdvertisingReports(); + std::vector<LeDirectedAdvertisingResponse> reports = event_view.GetResponses(); if (reports.empty()) { LOG_INFO("Zero results in advertising event"); return; @@ -377,13 +377,13 @@ struct LeScanningManager::impl : public bluetooth::hci::LeAddressManagerCallback LOG_INFO("Dropping invalid advertising event"); return; } - std::vector<LeExtendedAdvertisingReport> reports = event_view.GetAdvertisingReports(); + std::vector<LeExtendedAdvertisingResponse> reports = event_view.GetResponses(); if (reports.empty()) { LOG_INFO("Zero results in advertising event"); return; } - for (LeExtendedAdvertisingReport report : reports) { + for (LeExtendedAdvertisingResponse report : reports) { uint16_t event_type = report.connectable_ | (report.scannable_ << kScannableBit) | (report.directed_ << kDirectedBit) | (report.scan_response_ << kScanResponseBit) | (report.legacy_ << kLegacyBit) | ((uint16_t)report.data_status_ << kDataStatusBits); diff --git a/system/gd/hci/le_scanning_manager_test.cc b/system/gd/hci/le_scanning_manager_test.cc index f5e6449295..bcf0c0287c 100644 --- a/system/gd/hci/le_scanning_manager_test.cc +++ b/system/gd/hci/le_scanning_manager_test.cc @@ -382,7 +382,7 @@ TEST_F(LeScanningManagerTest, start_scan_test) { ASSERT_EQ(std::future_status::ready, result); test_hci_layer_->IncomingEvent(LeSetScanEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS)); - LeAdvertisingReport report{}; + LeAdvertisingResponse report{}; report.event_type_ = AdvertisingEventType::ADV_DIRECT_IND; report.address_type_ = AddressType::PUBLIC_DEVICE_ADDRESS; Address::FromString("12:34:56:78:9a:bc", report.address_); @@ -409,7 +409,7 @@ TEST_F(LeAndroidHciScanningManagerTest, start_scan_test) { ASSERT_EQ(std::future_status::ready, result); test_hci_layer_->IncomingEvent(LeSetScanEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS)); - LeAdvertisingReport report{}; + LeAdvertisingResponse report{}; report.event_type_ = AdvertisingEventType::ADV_DIRECT_IND; report.address_type_ = AddressType::PUBLIC_DEVICE_ADDRESS; Address::FromString("12:34:56:78:9a:bc", report.address_); @@ -527,7 +527,7 @@ TEST_F(LeExtendedScanningManagerTest, start_scan_test) { test_hci_layer_->IncomingEvent(LeSetScanEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS)); - LeExtendedAdvertisingReport report{}; + LeExtendedAdvertisingResponse report{}; report.connectable_ = 1; report.scannable_ = 0; report.address_type_ = DirectAdvertisingAddressType::PUBLIC_DEVICE_ADDRESS; diff --git a/system/gd/neighbor/inquiry.cc b/system/gd/neighbor/inquiry.cc index 9eceebb366..f814fd0286 100644 --- a/system/gd/neighbor/inquiry.cc +++ b/system/gd/neighbor/inquiry.cc @@ -208,14 +208,14 @@ void neighbor::InquiryModule::impl::OnEvent(hci::EventView view) { case hci::EventCode::INQUIRY_RESULT: { auto packet = hci::InquiryResultView::Create(view); ASSERT(packet.IsValid()); - LOG_INFO("Inquiry result size:%zd num_responses:%zu", packet.size(), packet.GetInquiryResults().size()); + LOG_INFO("Inquiry result size:%zd num_responses:%zu", packet.size(), packet.GetResponses().size()); inquiry_callbacks_.result(packet); } break; case hci::EventCode::INQUIRY_RESULT_WITH_RSSI: { auto packet = hci::InquiryResultWithRssiView::Create(view); ASSERT(packet.IsValid()); - LOG_INFO("Inquiry result with rssi num_responses:%zu", packet.GetInquiryResults().size()); + LOG_INFO("Inquiry result with rssi num_responses:%zu", packet.GetResponses().size()); inquiry_callbacks_.result_with_rssi(packet); } break; diff --git a/system/gd/packet/parser/doc/reference.md b/system/gd/packet/parser/doc/reference.md index 1e521372d3..6158faee57 100644 --- a/system/gd/packet/parser/doc/reference.md +++ b/system/gd/packet/parser/doc/reference.md @@ -54,6 +54,20 @@ packet Brew { } ``` +## Identifiers + +- Identifiers can denote a field; an enumeration tag; or a declared type. + +- Field identifiers declared in a [packet](#packet) (resp. [struct](#struct)) belong to the _scope_ that extends + to the packet (resp. struct), and all derived packets (resp. structs). + +- Field identifiers declared in a [group](#group) belong to the _scope_ that + extends to the packets declaring a [group field](#group_field) for this group. + +- Two fields may not be declared with the same identifier in any packet scope. + +- Two types may not be declared width the same identifier. + ## Declarations > declaration: {#declaration}\ diff --git a/system/gd/rust/linux/client/src/command_handler.rs b/system/gd/rust/linux/client/src/command_handler.rs index f2221cc5e8..266dd5f261 100644 --- a/system/gd/rust/linux/client/src/command_handler.rs +++ b/system/gd/rust/linux/client/src/command_handler.rs @@ -437,7 +437,7 @@ impl CommandHandler { return; } - enforce_arg_len(args, 2, "device <connect|disconnect|info> <address>", || { + enforce_arg_len(args, 2, "device <connect|disconnect|info|set-alias> <address>", || { match &args[0][0..] { "connect" => { let device = BluetoothDevice { @@ -519,6 +519,31 @@ impl CommandHandler { ) ); } + "set-alias" => { + if args.len() < 3 { + println!("usage: device set-alias <address> <new-alias>"); + return; + } + let new_alias = &args[2]; + let device = + BluetoothDevice { address: String::from(&args[1]), name: String::from("") }; + let old_alias = self + .context + .lock() + .unwrap() + .adapter_dbus + .as_ref() + .unwrap() + .get_remote_alias(device.clone()); + println!("Updating alias for {}: {} -> {}", &args[1], old_alias, new_alias); + self.context + .lock() + .unwrap() + .adapter_dbus + .as_mut() + .unwrap() + .set_remote_alias(device.clone(), new_alias.clone()); + } _ => { println!("Invalid argument '{}'", args[0]); } diff --git a/system/gd/rust/linux/client/src/dbus_iface.rs b/system/gd/rust/linux/client/src/dbus_iface.rs index 300316f8dd..a18729e5ea 100644 --- a/system/gd/rust/linux/client/src/dbus_iface.rs +++ b/system/gd/rust/linux/client/src/dbus_iface.rs @@ -400,6 +400,11 @@ impl IBluetooth for BluetoothDBus { dbus_generated!() } + #[dbus_method("SetRemoteAlias")] + fn set_remote_alias(&mut self, device: BluetoothDevice, new_alias: String) { + dbus_generated!() + } + #[dbus_method("GetRemoteClass")] fn get_remote_class(&self, device: BluetoothDevice) -> u32 { 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 37769b67b1..10229b2bbb 100644 --- a/system/gd/rust/linux/service/src/iface_bluetooth.rs +++ b/system/gd/rust/linux/service/src/iface_bluetooth.rs @@ -248,6 +248,11 @@ impl IBluetooth for IBluetoothDBus { dbus_generated!() } + #[dbus_method("SetRemoteAlias")] + fn set_remote_alias(&mut self, _device: BluetoothDevice, new_alias: String) { + dbus_generated!() + } + #[dbus_method("GetRemoteClass")] fn get_remote_class(&self, _device: BluetoothDevice) -> u32 { dbus_generated!() diff --git a/system/gd/rust/linux/service/src/main.rs b/system/gd/rust/linux/service/src/main.rs index 62b97f21f6..e749843e14 100644 --- a/system/gd/rust/linux/service/src/main.rs +++ b/system/gd/rust/linux/service/src/main.rs @@ -76,19 +76,6 @@ fn main() -> Result<(), Box<dyn Error>> { let adapter_index = get_adapter_index(&args); - // Hold locks and initialize all interfaces. - { - intf.lock().unwrap().initialize(get_bt_dispatcher(tx.clone()), args); - - bluetooth_media.lock().unwrap().set_adapter(bluetooth.clone()); - - let mut bluetooth = bluetooth.lock().unwrap(); - bluetooth.init_profiles(); - bluetooth.enable(); - - bluetooth_gatt.lock().unwrap().init_profiles(tx.clone()); - } - topstack::get_runtime().block_on(async { // Connect to D-Bus system bus. let (resource, conn) = connection::new_system_sync()?; @@ -138,7 +125,7 @@ fn main() -> Result<(), Box<dyn Error>> { make_object_name(adapter_index, "gatt"), conn.clone(), &mut cr, - bluetooth_gatt, + bluetooth_gatt.clone(), disconnect_watcher.clone(), ); @@ -146,7 +133,7 @@ fn main() -> Result<(), Box<dyn Error>> { make_object_name(adapter_index, "media"), conn.clone(), &mut cr, - bluetooth_media, + bluetooth_media.clone(), disconnect_watcher.clone(), ); @@ -158,6 +145,22 @@ fn main() -> Result<(), Box<dyn Error>> { disconnect_watcher.clone(), ); + // Hold locks and initialize all interfaces. This must be done AFTER DBus is + // initialized so DBus can properly enforce user policies. + { + intf.lock().unwrap().initialize(get_bt_dispatcher(tx.clone()), args); + + bluetooth_media.lock().unwrap().set_adapter(bluetooth.clone()); + + let mut bluetooth = bluetooth.lock().unwrap(); + bluetooth.init_profiles(); + bluetooth.enable(); + + bluetooth_gatt.lock().unwrap().init_profiles(tx.clone()); + } + + // Start listening on DBus after exporting interfaces and initializing + // all bluetooth objects. conn.start_receive( MatchRule::new_method_call(), Box::new(move |msg, conn| { diff --git a/system/gd/rust/linux/stack/src/bluetooth.rs b/system/gd/rust/linux/stack/src/bluetooth.rs index f2a610d83b..3728d1a04f 100644 --- a/system/gd/rust/linux/stack/src/bluetooth.rs +++ b/system/gd/rust/linux/stack/src/bluetooth.rs @@ -131,6 +131,9 @@ pub trait IBluetooth { /// Gets the alias of the remote device. fn get_remote_alias(&self, device: BluetoothDevice) -> String; + /// Sets the alias of the remote device. + fn set_remote_alias(&mut self, device: BluetoothDevice, new_alias: String); + /// Gets the class of the remote device. fn get_remote_class(&self, device: BluetoothDevice) -> u32; @@ -399,6 +402,16 @@ impl Bluetooth { self.bonded_devices.get(&device.address).or_else(|| self.found_devices.get(&device.address)) } + fn get_remote_device_if_found_mut( + &mut self, + device: &BluetoothDevice, + ) -> Option<&mut BluetoothDeviceContext> { + match self.bonded_devices.get_mut(&device.address) { + None => self.found_devices.get_mut(&device.address), + some => some, + } + } + fn get_remote_device_property( &self, device: &BluetoothDevice, @@ -407,6 +420,31 @@ impl Bluetooth { self.get_remote_device_if_found(&device) .and_then(|d| d.properties.get(property_type).and_then(|p| Some(p.clone()))) } + + fn set_remote_device_property( + &mut self, + device: &BluetoothDevice, + property_type: BtPropertyType, + property: BluetoothProperty, + ) -> Result<(), ()> { + let mut remote_device = match self.get_remote_device_if_found_mut(&device) { + Some(d) => d, + None => { + return Err(()); + } + }; + + let mut addr = RawAddress::from_string(device.address.clone()); + if addr.is_none() { + return Err(()); + } + let addr = addr.as_mut().unwrap(); + + // TODO: Determine why a callback isn't invoked to do this. + remote_device.properties.insert(property_type, property.clone()); + self.intf.lock().unwrap().set_remote_device_property(addr, property); + Ok(()) + } } #[btif_callbacks_dispatcher(Bluetooth, dispatch_base_callbacks, BaseCallbacks)] @@ -1115,6 +1153,14 @@ impl IBluetooth for Bluetooth { } } + fn set_remote_alias(&mut self, device: BluetoothDevice, new_alias: String) { + let _ = self.set_remote_device_property( + &device, + BtPropertyType::RemoteFriendlyName, + BluetoothProperty::RemoteFriendlyName(new_alias), + ); + } + fn get_remote_class(&self, device: BluetoothDevice) -> u32 { match self.get_remote_device_property(&device, &BtPropertyType::ClassOfDevice) { Some(BluetoothProperty::ClassOfDevice(class)) => return class, diff --git a/system/gd/rust/topshim/src/btif.rs b/system/gd/rust/topshim/src/btif.rs index 625f815b08..452d7a6e2d 100644 --- a/system/gd/rust/topshim/src/btif.rs +++ b/system/gd/rust/topshim/src/btif.rs @@ -382,7 +382,9 @@ impl BluetoothProperty { let len = self.get_len(); match &*self { BluetoothProperty::BdName(name) => { - data.copy_from_slice(&name.as_bytes()[0..len]); + let copy_len = len - 1; + data[0..copy_len].copy_from_slice(&name.as_bytes()[0..copy_len]); + data[copy_len] = 0; } BluetoothProperty::BdAddr(addr) => { data.copy_from_slice(&addr.val); @@ -408,11 +410,12 @@ impl BluetoothProperty { unsafe { &mut *(data.as_mut_ptr() as *mut bindings::bt_service_record_t) }; record.uuid = sr.uuid; record.channel = sr.channel; - let name_len = len - mem::size_of::<BtServiceRecord>(); - record.name.copy_from_slice( + let name_len = len - mem::size_of::<BtServiceRecord>() - 1; + record.name[0..name_len].copy_from_slice( &(sr.name.as_bytes().iter().map(|x| *x as c_char).collect::<Vec<c_char>>()) [0..name_len], ); + record.name[name_len] = 0; } BluetoothProperty::AdapterScanMode(sm) => { data.copy_from_slice(&BtScanMode::to_u32(sm).unwrap_or_default().to_ne_bytes()); @@ -428,7 +431,9 @@ impl BluetoothProperty { data.copy_from_slice(&timeout.to_ne_bytes()); } BluetoothProperty::RemoteFriendlyName(name) => { - data.copy_from_slice(&name.as_bytes()[0..len]); + let copy_len = len - 1; + data[0..copy_len].copy_from_slice(&name.as_bytes()[0..copy_len]); + data[copy_len] = 0; } BluetoothProperty::RemoteRssi(rssi) => { data[0] = *rssi as u8; @@ -1110,4 +1115,46 @@ mod tests { let expected: Vec<i32> = vec![1, 2, 3]; assert_eq!(expected, vec); } + + #[test] + fn test_property_with_string_conversions() { + { + let bdname = BluetoothProperty::BdName("FooBar".into()); + let prop_pair: (Box<[u8]>, bindings::bt_property_t) = bdname.into(); + let converted: BluetoothProperty = prop_pair.1.into(); + assert!(match converted { + BluetoothProperty::BdName(name) => "FooBar".to_string() == name, + _ => false, + }); + } + + { + let orig_record = BtServiceRecord { + uuid: Uuid { uu: [0; 16] }, + channel: 3, + name: "FooBar".to_string(), + }; + let service_record = BluetoothProperty::ServiceRecord(orig_record.clone()); + let prop_pair: (Box<[u8]>, bindings::bt_property_t) = service_record.into(); + let converted: BluetoothProperty = prop_pair.1.into(); + assert!(match converted { + BluetoothProperty::ServiceRecord(sr) => { + sr.uuid == orig_record.uuid + && sr.channel == orig_record.channel + && sr.name == orig_record.name + } + _ => false, + }); + } + + { + let rfname = BluetoothProperty::RemoteFriendlyName("FooBizz".into()); + let prop_pair: (Box<[u8]>, bindings::bt_property_t) = rfname.into(); + let converted: BluetoothProperty = prop_pair.1.into(); + assert!(match converted { + BluetoothProperty::RemoteFriendlyName(name) => "FooBizz".to_string() == name, + _ => false, + }); + } + } } diff --git a/system/main/shim/btm.cc b/system/main/shim/btm.cc index 3ffd99aad6..e28f16b566 100644 --- a/system/main/shim/btm.cc +++ b/system/main/shim/btm.cc @@ -174,7 +174,7 @@ Btm::Btm(os::Handler* handler, neighbor::InquiryModule* inquiry) } void Btm::OnInquiryResult(bluetooth::hci::InquiryResultView view) { - for (auto& response : view.GetInquiryResults()) { + for (auto& response : view.GetResponses()) { btm_api_process_inquiry_result( ToRawAddress(response.bd_addr_), static_cast<uint8_t>(response.page_scan_repetition_mode_), @@ -184,7 +184,7 @@ void Btm::OnInquiryResult(bluetooth::hci::InquiryResultView view) { void Btm::OnInquiryResultWithRssi( bluetooth::hci::InquiryResultWithRssiView view) { - for (auto& response : view.GetInquiryResults()) { + for (auto& response : view.GetResponses()) { btm_api_process_inquiry_result_with_rssi( ToRawAddress(response.address_), static_cast<uint8_t>(response.page_scan_repetition_mode_), diff --git a/system/stack/btm/btm_ble.cc b/system/stack/btm/btm_ble.cc index dfb136d90c..55d7097587 100644 --- a/system/stack/btm/btm_ble.cc +++ b/system/stack/btm/btm_ble.cc @@ -141,8 +141,8 @@ void BTM_SecAddBleKey(const RawAddress& bd_addr, tBTM_LE_KEY_VALUE* p_le_key, key_type); btm_sec_save_le_key(bd_addr, key_type, p_le_key, false); - - if (key_type == BTM_LE_KEY_PID || key_type == BTM_LE_KEY_LID) { + // Only set peer irk. Local irk is always the same. + if (key_type == BTM_LE_KEY_PID) { btm_ble_resolving_list_load_dev(*p_dev_rec); } } diff --git a/system/stack/btm/btm_ble_privacy.cc b/system/stack/btm/btm_ble_privacy.cc index 78e981c41b..f5e2b78f3f 100644 --- a/system/stack/btm/btm_ble_privacy.cc +++ b/system/stack/btm/btm_ble_privacy.cc @@ -647,10 +647,6 @@ static void btm_ble_ble_unsupported_resolving_list_load_dev( return; } -static bool is_local_identity_key_valid(const tBTM_SEC_DEV_REC& dev_rec) { - return dev_rec.ble.key_type & BTM_LE_KEY_LID; -} - static bool is_peer_identity_key_valid(const tBTM_SEC_DEV_REC& dev_rec) { return dev_rec.ble.key_type & BTM_LE_KEY_PID; } @@ -667,8 +663,8 @@ void btm_ble_resolving_list_load_dev(tBTM_SEC_DEV_REC& dev_rec) { return btm_ble_ble_unsupported_resolving_list_load_dev(&dev_rec); } - if (!is_local_identity_key_valid(dev_rec) && - !is_peer_identity_key_valid(dev_rec)) { + // No need to check for local identity key validity. It remains unchanged. + if (!is_peer_identity_key_valid(dev_rec)) { LOG_INFO("Peer is not an RPA enabled device:%s", PRIVATE_ADDRESS(dev_rec.ble.identity_address_with_type)); return; diff --git a/tools/rootcanal/model/controller/link_layer_controller.cc b/tools/rootcanal/model/controller/link_layer_controller.cc index b4764cb6bd..655dcd0a87 100644 --- a/tools/rootcanal/model/controller/link_layer_controller.cc +++ b/tools/rootcanal/model/controller/link_layer_controller.cc @@ -799,7 +799,7 @@ void LinkLayerController::IncomingInquiryResponsePacket( (bluetooth::hci::PageScanRepetitionMode) inquiry_response.GetPageScanRepetitionMode(); - std::vector<bluetooth::hci::InquiryResult> responses; + std::vector<bluetooth::hci::InquiryResponse> responses; responses.emplace_back(); responses.back().bd_addr_ = inquiry_response.GetSourceAddress(); responses.back().page_scan_repetition_mode_ = page_scan_repetition_mode; @@ -820,7 +820,7 @@ void LinkLayerController::IncomingInquiryResponsePacket( (bluetooth::hci::PageScanRepetitionMode) inquiry_response.GetPageScanRepetitionMode(); - std::vector<bluetooth::hci::InquiryResultWithRssi> responses; + std::vector<bluetooth::hci::InquiryResponseWithRssi> responses; responses.emplace_back(); responses.back().address_ = inquiry_response.GetSourceAddress(); responses.back().page_scan_repetition_mode_ = page_scan_repetition_mode; @@ -1825,7 +1825,7 @@ void LinkLayerController::IncomingLeScanResponsePacket( if (adv_type != model::packets::AdvertisementType::SCAN_RESPONSE) { return; } - bluetooth::hci::LeAdvertisingReportRaw report; + bluetooth::hci::LeAdvertisingResponseRaw report; report.event_type_ = bluetooth::hci::AdvertisingEventType::SCAN_RESPONSE; report.address_ = incoming.GetSourceAddress(); report.address_type_ = @@ -1845,7 +1845,7 @@ void LinkLayerController::IncomingLeScanResponsePacket( properties_.IsUnmasked(EventCode::LE_META_EVENT) && properties_.GetLeEventSupported( bluetooth::hci::SubeventCode::EXTENDED_ADVERTISING_REPORT)) { - bluetooth::hci::LeExtendedAdvertisingReport report{}; + bluetooth::hci::LeExtendedAdvertisingResponse report{}; report.address_ = incoming.GetSourceAddress(); report.address_type_ = static_cast<bluetooth::hci::DirectAdvertisingAddressType>(address_type); |