diff options
188 files changed, 11143 insertions, 2426 deletions
diff --git a/TEST_MAPPING b/TEST_MAPPING index 5fc31de514..562f9717c8 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -79,6 +79,9 @@ "name": "bluetooth_le_audio_test" }, { + "name": "bluetooth_ras_test" + }, + { "name": "bluetooth_packet_parser_test" }, { @@ -275,6 +278,9 @@ "name": "bluetooth_le_audio_test" }, { + "name": "bluetooth_ras_test" + }, + { "name": "bluetooth_packet_parser_test" }, { diff --git a/android/app/aidl/android/bluetooth/IBluetoothA2dp.aidl b/android/app/aidl/android/bluetooth/IBluetoothA2dp.aidl index bcfc95585d..fa5d1362ac 100644 --- a/android/app/aidl/android/bluetooth/IBluetoothA2dp.aidl +++ b/android/app/aidl/android/bluetooth/IBluetoothA2dp.aidl @@ -53,7 +53,7 @@ interface IBluetoothA2dp { boolean isA2dpPlaying(in BluetoothDevice device, in AttributionSource attributionSource); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") List<BluetoothCodecType> getSupportedCodecTypes(); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED}, conditional=true)") BluetoothCodecStatus getCodecStatus(in BluetoothDevice device, in AttributionSource attributionSource); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED}, conditional=true)") oneway void setCodecConfigPreference(in BluetoothDevice device, in BluetoothCodecConfig codecConfig, in AttributionSource attributionSource); diff --git a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp index 805e22b0df..6df96370b1 100644 --- a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp +++ b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp @@ -2228,6 +2228,25 @@ static jboolean disconnectAllAclsNative(JNIEnv* /* env */, jobject /* obj */) { return (ret == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE; } +static jboolean disconnectAclNative(JNIEnv* env, jobject /* obj */, jbyteArray address, + jint transport) { + log::verbose(""); + + if (!sBluetoothInterface) { + return JNI_FALSE; + } + + jbyte* addr = env->GetByteArrayElements(address, nullptr); + if (addr == nullptr) { + jniThrowIOException(env, EINVAL); + return JNI_FALSE; + } + RawAddress addr_obj = {}; + addr_obj.FromOctets(reinterpret_cast<uint8_t*>(addr)); + + return sBluetoothInterface->disconnect_acl(addr_obj, transport); +} + static jboolean allowWakeByHidNative(JNIEnv* /* env */, jobject /* obj */) { log::verbose(""); @@ -2320,6 +2339,7 @@ int register_com_android_bluetooth_btservice_AdapterService(JNIEnv* env) { {"clearFilterAcceptListNative", "()Z", reinterpret_cast<void*>(clearFilterAcceptListNative)}, {"disconnectAllAclsNative", "()Z", reinterpret_cast<void*>(disconnectAllAclsNative)}, + {"disconnectAclNative", "([BI)Z", reinterpret_cast<void*>(disconnectAclNative)}, {"allowWakeByHidNative", "()Z", reinterpret_cast<void*>(allowWakeByHidNative)}, {"restoreFilterAcceptListNative", "()Z", reinterpret_cast<void*>(restoreFilterAcceptListNative)}, diff --git a/android/app/res/values/config.xml b/android/app/res/values/config.xml index 7418000e8e..0bb72b5ca8 100644 --- a/android/app/res/values/config.xml +++ b/android/app/res/values/config.xml @@ -138,12 +138,6 @@ <integer name="a2dp_source_codec_priority_lc3">6001</integer> <integer name="a2dp_source_codec_priority_opus">7001</integer> - <!-- For enabling the AVRCP Target Cover Artowrk feature--> - <bool name="avrcp_target_enable_cover_art">true</bool> - - <!-- Enable support for URI based images. Off by default due to increased memory usage --> - <bool name="avrcp_target_cover_art_uri_images">false</bool> - <!-- Package that is responsible for user interaction on pairing request, success or cancel. Receives: diff --git a/android/app/src/com/android/bluetooth/a2dp/A2dpService.java b/android/app/src/com/android/bluetooth/a2dp/A2dpService.java index 136a0d4621..100076127b 100644 --- a/android/app/src/com/android/bluetooth/a2dp/A2dpService.java +++ b/android/app/src/com/android/bluetooth/a2dp/A2dpService.java @@ -50,7 +50,6 @@ import android.media.BluetoothProfileConnectionInfo; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.HandlerThread; import android.os.Looper; import android.sysprop.BluetoothProperties; import android.util.Log; @@ -1515,12 +1514,14 @@ public class A2dpService extends ProfileService { @Override public BluetoothCodecStatus getCodecStatus( BluetoothDevice device, AttributionSource source) { + requireNonNull(device); A2dpService service = getServiceAndEnforceConnect(source); if (service == null) { return null; } - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + Utils.enforceCdmAssociationIfNotBluetoothPrivileged( + service, service.mCompanionDeviceManager, source, device); return service.getCodecStatus(device); } @@ -1530,6 +1531,7 @@ public class A2dpService extends ProfileService { BluetoothDevice device, BluetoothCodecConfig codecConfig, AttributionSource source) { + requireNonNull(device); A2dpService service = getServiceAndEnforceConnect(source); if (service == null) { return; diff --git a/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java b/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java index b04f684f34..e062814766 100644 --- a/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java +++ b/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java @@ -784,6 +784,10 @@ public class MediaPlayerList { } } + public boolean isVfsCoverArtEnabled() { + return Util.areUriImagesSupported(); + } + /** * Adds a {@link MediaController} to the {@link #mMediaPlayers} map and returns its ID. * @@ -985,6 +989,19 @@ public class MediaPlayerList { mActivePlayerId = playerId; + if (Utils.isPtsTestMode()) { + sendFolderUpdate(true, true, false); + } else if (Flags.setAddressedPlayer() && Flags.browsingRefactor()) { + // If the browsing refactor flag is not active, addressed player should always be 0. + // If the new active player has been set by Addressed player key event + // We don't send an addressed player update. + if (mActivePlayerId != mAddressedPlayerId) { + mAddressedPlayerId = mActivePlayerId; + Log.d(TAG, "setActivePlayer AddressedPlayer changed to " + mAddressedPlayerId); + sendFolderUpdate(false, true, false); + } + } + MediaPlayerWrapper player = getActivePlayer(); if (player == null) return; @@ -1002,19 +1019,6 @@ public class MediaPlayerList { return; } - if (Utils.isPtsTestMode()) { - sendFolderUpdate(true, true, false); - } else if (Flags.setAddressedPlayer() && Flags.browsingRefactor()) { - // If the browsing refactor flag is not active, addressed player should always be 0. - // If the new active player has been set by Addressed player key event - // We don't send an addressed player update. - if (mActivePlayerId != mAddressedPlayerId) { - mAddressedPlayerId = mActivePlayerId; - Log.d(TAG, "setActivePlayer AddressedPlayer changed to " + mAddressedPlayerId); - sendFolderUpdate(false, true, false); - } - } - MediaData data = player.getCurrentMediaData(); if (mAudioPlaybackIsActive) { data.state = mCurrMediaData.state; diff --git a/android/app/src/com/android/bluetooth/audio_util/helpers/Image.java b/android/app/src/com/android/bluetooth/audio_util/helpers/Image.java index fc106e9a95..d89a6afddf 100644 --- a/android/app/src/com/android/bluetooth/audio_util/helpers/Image.java +++ b/android/app/src/com/android/bluetooth/audio_util/helpers/Image.java @@ -60,7 +60,7 @@ public class Image { Bitmap bmp_album_art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); Bitmap bmp_icon = metadata.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON); - if (mContext != null && Util.areUriImagesSupported(mContext)) { + if (Util.areUriImagesSupported()) { uri_art = metadata.getString(MediaMetadata.METADATA_KEY_ART_URI); uri_album_art = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI); uri_icon = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI); @@ -92,7 +92,7 @@ public class Image { Bitmap bmp_album_art = bundle.getParcelable(MediaMetadata.METADATA_KEY_ALBUM_ART); Bitmap bmp_icon = bundle.getParcelable(MediaMetadata.METADATA_KEY_DISPLAY_ICON); - if (mContext != null && Util.areUriImagesSupported(mContext)) { + if (Util.areUriImagesSupported()) { uri_art = bundle.getString(MediaMetadata.METADATA_KEY_ART_URI); uri_album_art = bundle.getString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI); uri_icon = bundle.getString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI); diff --git a/android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java b/android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java index 02ca4fbd5c..63791acb29 100644 --- a/android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java +++ b/android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java @@ -23,8 +23,6 @@ import android.media.browse.MediaBrowser.MediaItem; import android.media.session.MediaSession; import android.os.Bundle; -import com.android.bluetooth.R; - import java.util.Objects; public class Metadata implements Cloneable { @@ -201,7 +199,7 @@ public class Metadata implements Cloneable { mMetadata.duration = "" + data.getLong(MediaMetadata.METADATA_KEY_DURATION); } if ((mContext != null - && Util.areUriImagesSupported(mContext) + && Util.areUriImagesSupported() && (data.containsKey(MediaMetadata.METADATA_KEY_ART_URI) || data.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART_URI) || data.containsKey( @@ -233,7 +231,7 @@ public class Metadata implements Cloneable { if (desc.getIconBitmap() != null) { mMetadata.image = new Image(mContext, desc.getIconBitmap()); } else if (mContext != null - && Util.areUriImagesSupported(mContext) + && Util.areUriImagesSupported() && desc.getIconUri() != null) { mMetadata.image = new Image(mContext, desc.getIconUri()); } @@ -281,7 +279,7 @@ public class Metadata implements Cloneable { mMetadata.duration = "" + bundle.getLong(MediaMetadata.METADATA_KEY_DURATION); } if ((mContext != null - && Util.areUriImagesSupported(mContext) + && Util.areUriImagesSupported() && (bundle.containsKey(MediaMetadata.METADATA_KEY_ART_URI) || bundle.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART_URI) || bundle.containsKey( @@ -296,13 +294,8 @@ public class Metadata implements Cloneable { /** Elect to use default values in the Metadata in place of any missing values */ public Builder useDefaults() { - if (mMetadata.mediaId == null) { - mMetadata.mediaId = EMPTY_MEDIA_ID; - } - if (mMetadata.title == null) { - mMetadata.title = - mContext != null ? mContext.getString(R.string.not_provided) : EMPTY_TITLE; - } + if (mMetadata.mediaId == null) mMetadata.mediaId = EMPTY_MEDIA_ID; + if (mMetadata.title == null) mMetadata.title = EMPTY_TITLE; if (mMetadata.artist == null) mMetadata.artist = EMPTY_ARTIST; if (mMetadata.album == null) mMetadata.album = EMPTY_ALBUM; if (mMetadata.trackNum == null) mMetadata.trackNum = EMPTY_TRACK_NUM; diff --git a/android/app/src/com/android/bluetooth/audio_util/helpers/Util.java b/android/app/src/com/android/bluetooth/audio_util/helpers/Util.java index 32597edc75..877a315c88 100644 --- a/android/app/src/com/android/bluetooth/audio_util/helpers/Util.java +++ b/android/app/src/com/android/bluetooth/audio_util/helpers/Util.java @@ -21,9 +21,10 @@ import android.content.pm.PackageManager; import android.media.MediaMetadata; import android.media.browse.MediaBrowser.MediaItem; import android.media.session.MediaSession; +import android.os.SystemProperties; import android.util.Log; -import com.android.bluetooth.R; +import androidx.annotation.VisibleForTesting; import java.util.ArrayList; import java.util.List; @@ -31,6 +32,13 @@ import java.util.List; class Util { public static String TAG = "audio_util.Util"; + private static final String VFS_COVER_ART_ENABLED_PROPERTY = + "bluetooth.profile.avrcp.target.vfs_coverart.enabled"; + + @VisibleForTesting + static Boolean sUriImagesSupport = + SystemProperties.getBoolean(VFS_COVER_ART_ENABLED_PROPERTY, false); + // TODO (apanicke): Remove this prefix later, for now it makes debugging easier. public static final String NOW_PLAYING_PREFIX = "NowPlayingId"; @@ -49,13 +57,12 @@ class Util { } /** - * Get whether or not Bluetooth is configured to support URI images or not. + * Get whether or not Bluetooth is configured to support URI images. * * <p>Note that creating URI images will dramatically increase memory usage. */ - public static boolean areUriImagesSupported(Context context) { - if (context == null) return false; - return context.getResources().getBoolean(R.bool.avrcp_target_cover_art_uri_images); + public static boolean areUriImagesSupported() { + return sUriImagesSupport.booleanValue(); } /** Translate a MediaItem to audio_util's Metadata */ diff --git a/android/app/src/com/android/bluetooth/avrcp/AvrcpCoverArtService.java b/android/app/src/com/android/bluetooth/avrcp/AvrcpCoverArtService.java index 26d7d3e48d..46805f404c 100644 --- a/android/app/src/com/android/bluetooth/avrcp/AvrcpCoverArtService.java +++ b/android/app/src/com/android/bluetooth/avrcp/AvrcpCoverArtService.java @@ -62,8 +62,9 @@ public class AvrcpCoverArtService { // Native interface private AvrcpNativeInterface mNativeInterface; - public AvrcpCoverArtService() { - mNativeInterface = AvrcpNativeInterface.getInstance(); + // The native interface must be a parameter here in order to be able to mock AvrcpTargetService + public AvrcpCoverArtService(AvrcpNativeInterface nativeInterface) { + mNativeInterface = nativeInterface; mAcceptThread = new SocketAcceptor(); mStorage = new AvrcpCoverArtStorage(COVER_ART_STORAGE_MAX_ITEMS); } diff --git a/android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java b/android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java index b6aa7cf8b6..068db0f024 100644 --- a/android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java +++ b/android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java @@ -36,8 +36,8 @@ import android.view.KeyEvent; import com.android.bluetooth.BluetoothEventLogger; import com.android.bluetooth.BluetoothMetricsProto; -import com.android.bluetooth.R; import com.android.bluetooth.a2dp.A2dpService; +import com.android.bluetooth.audio_util.ListItem; import com.android.bluetooth.audio_util.MediaData; import com.android.bluetooth.audio_util.MediaPlayerList; import com.android.bluetooth.audio_util.MediaPlayerWrapper; @@ -98,6 +98,8 @@ public class AvrcpTargetService extends ProfileService { private static AvrcpTargetService sInstance = null; + private final boolean mIsVfsCoverArtEnabled; + public AvrcpTargetService(AdapterService adapterService) { this( requireNonNull(adapterService), @@ -142,13 +144,11 @@ public class AvrcpTargetService extends ProfileService { mMediaPlayerList.init(new ListCallback()); } - if (!getResources().getBoolean(R.bool.avrcp_target_enable_cover_art)) { - mAvrcpCoverArtService = null; - } else if (!mAvrcpVersion.isAtleastVersion(AvrcpVersion.AVRCP_VERSION_1_6)) { + if (!mAvrcpVersion.isAtleastVersion(AvrcpVersion.AVRCP_VERSION_1_6)) { Log.e(TAG, "Please use AVRCP version 1.6 to enable cover art"); mAvrcpCoverArtService = null; } else { - AvrcpCoverArtService coverArtService = new AvrcpCoverArtService(); + AvrcpCoverArtService coverArtService = new AvrcpCoverArtService(mNativeInterface); if (coverArtService.start()) { mAvrcpCoverArtService = coverArtService; } else { @@ -157,6 +157,8 @@ public class AvrcpTargetService extends ProfileService { } } + mIsVfsCoverArtEnabled = mMediaPlayerList.isVfsCoverArtEnabled(); + mReceiver = new AvrcpBroadcastReceiver(); IntentFilter filter = new IntentFilter(); filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); @@ -399,8 +401,7 @@ public class AvrcpTargetService extends ProfileService { Metadata getCurrentSongInfo() { Metadata metadata = mMediaPlayerList.getCurrentSongInfo(); if (mAvrcpCoverArtService != null && metadata.image != null) { - String imageHandle = mAvrcpCoverArtService.storeImage(metadata.image); - if (imageHandle != null) metadata.image.setImageHandle(imageHandle); + metadata.image.setImageHandle(mAvrcpCoverArtService.storeImage(metadata.image)); } return metadata; } @@ -433,26 +434,20 @@ public class AvrcpTargetService extends ProfileService { List<Metadata> getNowPlayingList() { String currentMediaId = getCurrentMediaId(); Metadata currentTrack = null; - String imageHandle = null; List<Metadata> nowPlayingList = mMediaPlayerList.getNowPlayingList(); if (mAvrcpCoverArtService != null) { for (Metadata metadata : nowPlayingList) { if (TextUtils.equals(metadata.mediaId, currentMediaId)) { currentTrack = metadata; } else if (metadata.image != null) { - imageHandle = mAvrcpCoverArtService.storeImage(metadata.image); - if (imageHandle != null) { - metadata.image.setImageHandle(imageHandle); - } + metadata.image.setImageHandle(mAvrcpCoverArtService.storeImage(metadata.image)); } } // Always store the current item from the queue last so we know the image is in storage if (currentTrack != null) { - imageHandle = mAvrcpCoverArtService.storeImage(currentTrack.image); - if (imageHandle != null) { - currentTrack.image.setImageHandle(imageHandle); - } + currentTrack.image.setImageHandle( + mAvrcpCoverArtService.storeImage(currentTrack.image)); } } return nowPlayingList; @@ -491,7 +486,20 @@ public class AvrcpTargetService extends ProfileService { /** See {@link MediaPlayerList#getFolderItems}. */ void getFolderItems(int playerId, String mediaId, MediaPlayerList.GetFolderItemsCallback cb) { - mMediaPlayerList.getFolderItems(playerId, mediaId, cb); + mMediaPlayerList.getFolderItems( + playerId, + mediaId, + (id, results) -> { + if (mIsVfsCoverArtEnabled && mAvrcpCoverArtService != null) { + for (ListItem item : results) { + if (item != null && item.song != null && item.song.image != null) { + item.song.image.setImageHandle( + mAvrcpCoverArtService.storeImage(item.song.image)); + } + } + } + cb.run(id, results); + }); } /** See {@link MediaPlayerList#playItem}. */ diff --git a/android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpVersion.java b/android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpVersion.java index a3ab9192e9..c475db1ef1 100644 --- a/android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpVersion.java +++ b/android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpVersion.java @@ -18,6 +18,8 @@ package com.android.bluetooth.avrcp; import android.os.SystemProperties; +import com.android.bluetooth.flags.Flags; + /** A class to represent an AVRCP version */ final class AvrcpVersion { public static final AvrcpVersion AVRCP_VERSION_1_3 = new AvrcpVersion(1, 3); @@ -36,8 +38,12 @@ final class AvrcpVersion { public int minor; public static AvrcpVersion getCurrentSystemPropertiesValue() { - // Make sure this default version agrees with avrc_api.h's "AVRC_DEFAULT_VERSION" - String version = SystemProperties.get(AVRCP_VERSION_PROPERTY, AVRCP_VERSION_1_5_STRING); + // Make sure this default version agrees with AVRC_GetProfileVersion + + String defaultVersion = + Flags.avrcp16Default() ? AVRCP_VERSION_1_6_STRING : AVRCP_VERSION_1_5_STRING; + String version = SystemProperties.get(AVRCP_VERSION_PROPERTY, defaultVersion); + switch (version) { case AVRCP_VERSION_1_3_STRING: return AVRCP_VERSION_1_3; diff --git a/android/app/src/com/android/bluetooth/avrcp/helpers/CoverArt.java b/android/app/src/com/android/bluetooth/avrcp/helpers/CoverArt.java index 95388baf78..dd720077a0 100644 --- a/android/app/src/com/android/bluetooth/avrcp/helpers/CoverArt.java +++ b/android/app/src/com/android/bluetooth/avrcp/helpers/CoverArt.java @@ -40,7 +40,12 @@ import java.security.NoSuchAlgorithmException; */ public class CoverArt { private static final String TAG = CoverArt.class.getSimpleName(); - private static final BipPixel PIXEL_THUMBNAIL = BipPixel.createFixed(200, 200); + + // The size in pixels of the thumbnail sides. + private static final int THUMBNAIL_SIZE = 200; + + private static final BipPixel PIXEL_THUMBNAIL = + BipPixel.createFixed(THUMBNAIL_SIZE, THUMBNAIL_SIZE); private String mImageHandle = null; private Bitmap mImage = null; @@ -50,7 +55,7 @@ public class CoverArt { // Create a scaled version of the image for now, as consumers don't need // anything larger than this at the moment. Also makes each image gathered // the same dimensions for hashing purposes. - mImage = Bitmap.createScaledBitmap(image.getImage(), 200, 200, false); + mImage = Bitmap.createScaledBitmap(image.getImage(), THUMBNAIL_SIZE, THUMBNAIL_SIZE, false); } /** @@ -133,13 +138,15 @@ public class CoverArt { BipEncoding encoding = descriptor.getEncoding(); BipPixel pixel = descriptor.getPixel(); - if (encoding.getType() == BipEncoding.JPEG && PIXEL_THUMBNAIL.equals(pixel)) { + int encodingType = encoding.getType(); + if ((encodingType == BipEncoding.JPEG || encodingType == BipEncoding.PNG) + && PIXEL_THUMBNAIL.equals(pixel)) { return true; } return false; } - /** Get the cover artwork image bytes as a 200 x 200 JPEG thumbnail */ + /** Get the cover artwork image bytes as a THUMBNAIL_SIZE x THUMBNAIL_SIZE JPEG thumbnail */ public byte[] getThumbnail() { debug("GetImageThumbnail()"); if (mImage == null) return null; @@ -160,12 +167,19 @@ public class CoverArt { return null; } BipImageProperties.Builder builder = new BipImageProperties.Builder(); - BipEncoding encoding = new BipEncoding(BipEncoding.JPEG); - BipPixel pixel = BipPixel.createFixed(200, 200); - BipImageFormat format = BipImageFormat.createNative(encoding, pixel, -1); + + BipEncoding jpgEncoding = new BipEncoding(BipEncoding.JPEG); + BipEncoding pngEncoding = new BipEncoding(BipEncoding.PNG); + BipPixel jpgPixel = BipPixel.createFixed(THUMBNAIL_SIZE, THUMBNAIL_SIZE); + BipPixel pngPixel = BipPixel.createFixed(THUMBNAIL_SIZE, THUMBNAIL_SIZE); + + BipImageFormat jpgNativeFormat = BipImageFormat.createNative(jpgEncoding, jpgPixel, -1); + BipImageFormat pngVariantFormat = + BipImageFormat.createVariant(pngEncoding, pngPixel, THUMBNAIL_SIZE, null); builder.setImageHandle(mImageHandle); - builder.addNativeFormat(format); + builder.addNativeFormat(jpgNativeFormat); + builder.addVariantFormat(pngVariantFormat); BipImageProperties properties = builder.build(); return properties; diff --git a/android/app/src/com/android/bluetooth/bass_client/BassClientService.java b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java index 53657b16a6..0daef0175a 100644 --- a/android/app/src/com/android/bluetooth/bass_client/BassClientService.java +++ b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java @@ -2499,6 +2499,28 @@ public class BassClientService extends ProfileService { BluetoothDevice srcDevice = getDeviceForSyncHandle(syncHandle); mSyncHandleToDeviceMap.remove(syncHandle); int broadcastId = getBroadcastIdForSyncHandle(syncHandle); + if (leaudioMonitorUnicastSourceWhenManagedByBroadcastDelegator()) { + synchronized (mPendingSourcesToAdd) { + Iterator<AddSourceData> iterator = mPendingSourcesToAdd.iterator(); + while (iterator.hasNext()) { + AddSourceData pendingSourcesToAdd = iterator.next(); + if (pendingSourcesToAdd.mSourceMetadata.getBroadcastId() == broadcastId) { + iterator.remove(); + } + } + } + synchronized (mSinksWaitingForPast) { + Iterator<Map.Entry<BluetoothDevice, Pair<Integer, Integer>>> iterator = + mSinksWaitingForPast.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry<BluetoothDevice, Pair<Integer, Integer>> entry = iterator.next(); + int broadcastIdForPast = entry.getValue().first; + if (broadcastId == broadcastIdForPast) { + iterator.remove(); + } + } + } + } mSyncHandleToBroadcastIdMap.remove(syncHandle); if (srcDevice != null) { mPeriodicAdvertisementResultMap.get(srcDevice).remove(broadcastId); @@ -4054,7 +4076,10 @@ public class BassClientService extends ProfileService { mUnicastSourceStreamStatus = Optional.of(status); if (status == STATUS_LOCAL_STREAM_REQUESTED) { - if (areReceiversReceivingOnlyExternalBroadcast(getConnectedDevices())) { + if ((leaudioMonitorUnicastSourceWhenManagedByBroadcastDelegator() + && hasPrimaryDeviceManagedExternalBroadcast()) + || (!leaudioMonitorUnicastSourceWhenManagedByBroadcastDelegator() + && areReceiversReceivingOnlyExternalBroadcast(getConnectedDevices()))) { if (leaudioBroadcastAssistantPeripheralEntrustment()) { cacheSuspendingSources(BassConstants.INVALID_BROADCAST_ID); List<Pair<BluetoothLeBroadcastReceiveState, BluetoothDevice>> sourcesToStop = @@ -4067,11 +4092,13 @@ public class BassClientService extends ProfileService { suspendAllReceiversSourceSynchronization(); } } - for (Map.Entry<Integer, PauseType> entry : mPausedBroadcastIds.entrySet()) { - Integer broadcastId = entry.getKey(); - PauseType pauseType = entry.getValue(); - if (pauseType != PauseType.HOST_INTENTIONAL) { - suspendReceiversSourceSynchronization(broadcastId); + if (!leaudioMonitorUnicastSourceWhenManagedByBroadcastDelegator()) { + for (Map.Entry<Integer, PauseType> entry : mPausedBroadcastIds.entrySet()) { + Integer broadcastId = entry.getKey(); + PauseType pauseType = entry.getValue(); + if (pauseType != PauseType.HOST_INTENTIONAL) { + suspendReceiversSourceSynchronization(broadcastId); + } } } } else if (status == STATUS_LOCAL_STREAM_SUSPENDED) { @@ -4235,6 +4262,16 @@ public class BassClientService extends ProfileService { return activeSinks; } + /** Get sink devices synced to the broadcasts by broadcast id */ + public List<BluetoothDevice> getSyncedBroadcastSinks(int broadcastId) { + return getConnectedDevices().stream() + .filter( + device -> + getAllSources(device).stream() + .anyMatch(rs -> rs.getBroadcastId() == broadcastId)) + .toList(); + } + private boolean isSyncedToBroadcastStream(Long syncState) { return syncState != BassConstants.BCAST_RCVR_STATE_BIS_SYNC_NOT_SYNC_TO_BIS && syncState != BassConstants.BCAST_RCVR_STATE_BIS_SYNC_FAILED_SYNC_TO_BIG; diff --git a/android/app/src/com/android/bluetooth/btservice/AbstractionLayer.java b/android/app/src/com/android/bluetooth/btservice/AbstractionLayer.java index 981b2db0c7..9ebe10d181 100644 --- a/android/app/src/com/android/bluetooth/btservice/AbstractionLayer.java +++ b/android/app/src/com/android/bluetooth/btservice/AbstractionLayer.java @@ -50,6 +50,7 @@ public final class AbstractionLayer { static final int BT_PROPERTY_REMOTE_ASHA_TRUNCATED_HISYNCID = 0X16; static final int BT_PROPERTY_REMOTE_MODEL_NUM = 0x17; static final int BT_PROPERTY_LPP_OFFLOAD_FEATURES = 0x1B; + static final int BT_PROPERTY_UUIDS_LE = 0x1C; public static final int BT_DEVICE_TYPE_BREDR = 0x01; public static final int BT_DEVICE_TYPE_BLE = 0x02; diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterNativeInterface.java b/android/app/src/com/android/bluetooth/btservice/AdapterNativeInterface.java index 64b5a0dfae..244e132a66 100644 --- a/android/app/src/com/android/bluetooth/btservice/AdapterNativeInterface.java +++ b/android/app/src/com/android/bluetooth/btservice/AdapterNativeInterface.java @@ -316,6 +316,14 @@ public class AdapterNativeInterface { return disconnectAllAclsNative(); } + boolean disconnectAllAcls(BluetoothDevice device) { + return disconnectAcl(device, BluetoothDevice.TRANSPORT_AUTO); + } + + boolean disconnectAcl(BluetoothDevice device, int transport) { + return disconnectAclNative(Utils.getBytesFromAddress(device.getAddress()), transport); + } + boolean allowWakeByHid() { return allowWakeByHidNative(); } @@ -463,6 +471,8 @@ public class AdapterNativeInterface { private native boolean disconnectAllAclsNative(); + private native boolean disconnectAclNative(byte[] address, int transport); + private native boolean allowWakeByHidNative(); private native boolean restoreFilterAcceptListNative(); diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java index fe9cca3409..cba0af9ae6 100644 --- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java +++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java @@ -28,6 +28,7 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERA import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE; import static android.bluetooth.BluetoothDevice.BATTERY_LEVEL_UNKNOWN; import static android.bluetooth.BluetoothDevice.TRANSPORT_AUTO; +import static android.bluetooth.BluetoothProfile.getProfileName; import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static android.text.format.DateUtils.SECOND_IN_MILLIS; @@ -59,6 +60,7 @@ import android.bluetooth.BluetoothAdapter.ActiveDeviceUse; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice.BluetoothAddress; import android.bluetooth.BluetoothFrameworkInitializer; +import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothMap; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; @@ -116,6 +118,7 @@ import android.sysprop.BluetoothProperties; import android.text.TextUtils; import android.util.Base64; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import com.android.bluetooth.BluetoothMetricsProto; @@ -181,6 +184,7 @@ import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.time.Duration; +import java.time.Instant; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -290,6 +294,11 @@ public class AdapterService extends Service { private long mEnergyUsedTotalVoltAmpSecMicro; private HashSet<String> mLeAudioAllowDevices = new HashSet<>(); + /* List of pairs of gatt clients which controls AutoActiveMode on the device.*/ + @VisibleForTesting + final List<Pair<Integer, BluetoothDevice>> mLeGattClientsControllingAutoActiveMode = + new ArrayList<>(); + private BluetoothAdapter mAdapter; @VisibleForTesting AdapterProperties mAdapterProperties; private AdapterState mAdapterStateMachine; @@ -1546,46 +1555,45 @@ public class AdapterService extends Service { @VisibleForTesting void setProfileServiceState(int profileId, int state) { + Instant start = Instant.now(); + String logHdr = "setProfileServiceState(" + getProfileName(profileId) + ", " + state + "):"; + if (state == BluetoothAdapter.STATE_ON) { - if (!mStartedProfiles.containsKey(profileId)) { - ProfileService profileService = PROFILE_CONSTRUCTORS.get(profileId).apply(this); - mStartedProfiles.put(profileId, profileService); - addProfile(profileService); - profileService.start(); - profileService.setAvailable(true); - // With `Flags.scanManagerRefactor()` GattService initialization is pushed back to - // `ON` state instead of `BLE_ON`. Here we ensure mGattService is set prior - // to other Profiles using it. - if (profileId == BluetoothProfile.GATT && Flags.scanManagerRefactor()) { - mGattService = GattService.getGattService(); - } - onProfileServiceStateChanged(profileService, BluetoothAdapter.STATE_ON); - } else { - Log.e( - TAG, - "setProfileServiceState(" - + BluetoothProfile.getProfileName(profileId) - + ", STATE_ON): profile is already started"); + if (mStartedProfiles.containsKey(profileId)) { + Log.wtf(TAG, logHdr + " profile is already started"); + return; } + Log.d(TAG, logHdr + " starting profile"); + ProfileService profileService = PROFILE_CONSTRUCTORS.get(profileId).apply(this); + mStartedProfiles.put(profileId, profileService); + addProfile(profileService); + profileService.start(); + profileService.setAvailable(true); + // With `Flags.scanManagerRefactor()` GattService initialization is pushed back to + // `ON` state instead of `BLE_ON`. Here we ensure mGattService is set prior + // to other Profiles using it. + if (profileId == BluetoothProfile.GATT && Flags.scanManagerRefactor()) { + mGattService = GattService.getGattService(); + } + onProfileServiceStateChanged(profileService, BluetoothAdapter.STATE_ON); } else if (state == BluetoothAdapter.STATE_OFF) { ProfileService profileService = mStartedProfiles.remove(profileId); - if (profileService != null) { - profileService.setAvailable(false); - onProfileServiceStateChanged(profileService, BluetoothAdapter.STATE_OFF); - profileService.stop(); - removeProfile(profileService); - profileService.cleanup(); - if (profileService.getBinder() != null) { - profileService.getBinder().cleanup(); - } - } else { - Log.e( - TAG, - "setProfileServiceState(" - + BluetoothProfile.getProfileName(profileId) - + ", STATE_OFF): profile is already stopped"); + if (profileService == null) { + Log.wtf(TAG, logHdr + " profile is already stopped"); + return; + } + Log.d(TAG, logHdr + " stopping profile"); + profileService.setAvailable(false); + onProfileServiceStateChanged(profileService, BluetoothAdapter.STATE_OFF); + profileService.stop(); + removeProfile(profileService); + profileService.cleanup(); + if (profileService.getBinder() != null) { + profileService.getBinder().cleanup(); } } + Instant end = Instant.now(); + Log.d(TAG, logHdr + " completed in " + Duration.between(start, end).toMillis() + "ms"); } private void setAllProfileServiceStates(int[] profileIds, int state) { @@ -5153,6 +5161,192 @@ public class AdapterService extends Service { return getConnectionState(device) != BluetoothDevice.CONNECTION_STATE_DISCONNECTED; } + private void addGattClientToControlAutoActiveMode(int clientIf, BluetoothDevice device) { + if (!Flags.allowGattConnectFromTheAppsWithoutMakingLeaudioDeviceActive()) { + Log.i( + TAG, + "flag: allowGattConnectFromTheAppsWithoutMakingLeaudioDeviceActive is not" + + " enabled"); + return; + } + + /* When GATT client is connecting to LeAudio device, stack should not assume that + * LeAudio device should be automatically connected to Audio Framework. + * e.g. given LeAudio device might be busy with audio streaming from another device. + * LeAudio shall be automatically connected to Audio Framework when + * 1. Remote device expects that - Targeted Announcements are used + * 2. User is connecting device from Settings application. + * + * Above conditions are tracked by LeAudioService. In here, there is need to notify + * LeAudioService that connection is made for GATT purposes, so LeAudioService can + * disable AutoActiveMode and make sure to not make device Active just after connection + * is created. + * + * Note: AutoActiveMode is by default set to true and it means that LeAudio device is ready + * to streaming just after connection is created. That implies that device will be connected + * to Audio Framework (is made Active) when connection is created. + */ + + int groupId = mLeAudioService.getGroupId(device); + if (groupId == BluetoothLeAudio.GROUP_ID_INVALID) { + /* If this is not a LeAudio device, there is nothing to do here. */ + return; + } + + if (mLeAudioService.getConnectionPolicy(device) + != BluetoothProfile.CONNECTION_POLICY_ALLOWED) { + Log.d( + TAG, + "addGattClientToControlAutoActiveMode: " + + device + + " LeAudio connection policy is not allowed"); + return; + } + + Log.i( + TAG, + "addGattClientToControlAutoActiveMode: clientIf: " + + clientIf + + ", " + + device + + ", groupId: " + + groupId); + + synchronized (mLeGattClientsControllingAutoActiveMode) { + Pair newPair = new Pair<>(clientIf, device); + if (mLeGattClientsControllingAutoActiveMode.contains(newPair)) { + return; + } + + for (Pair<Integer, BluetoothDevice> pair : mLeGattClientsControllingAutoActiveMode) { + if (pair.second.equals(device) + || groupId == mLeAudioService.getGroupId(pair.second)) { + Log.i(TAG, "addGattClientToControlAutoActiveMode: adding new client"); + mLeGattClientsControllingAutoActiveMode.add(newPair); + return; + } + } + + if (mLeAudioService.setAutoActiveModeState(mLeAudioService.getGroupId(device), false)) { + Log.i( + TAG, + "addGattClientToControlAutoActiveMode: adding new client and notifying" + + " leAudioService"); + mLeGattClientsControllingAutoActiveMode.add(newPair); + } + } + } + + /** + * When this is called, AdapterService is aware of user doing GATT connection over LE. Adapter + * service will use this information to manage internal GATT services if needed. For now, + * AdapterService is using this information to control Auto Active Mode for LeAudio devices. + * + * @param clientIf clientIf ClientIf which was doing GATT connection attempt + * @param device device Remote device to connect + */ + public void notifyDirectLeGattClientConnect(int clientIf, BluetoothDevice device) { + if (mLeAudioService != null) { + addGattClientToControlAutoActiveMode(clientIf, device); + } + } + + private void removeGattClientFromControlAutoActiveMode(int clientIf, BluetoothDevice device) { + if (mLeGattClientsControllingAutoActiveMode.isEmpty()) { + return; + } + + int groupId = mLeAudioService.getGroupId(device); + if (groupId == BluetoothLeAudio.GROUP_ID_INVALID) { + /* If this is not a LeAudio device, there is nothing to do here. */ + return; + } + + /* Remember if auto active mode is still disabled. + * If it is disabled, it means, that either User or remote device did not make an + * action to make LeAudio device Active. + * That means, AdapterService should disconnect ACL when all the clients are disconnected + * from the group to which the device belongs. + */ + boolean isAutoActiveModeDisabled = !mLeAudioService.isAutoActiveModeEnabled(groupId); + + synchronized (mLeGattClientsControllingAutoActiveMode) { + Log.d( + TAG, + "removeGattClientFromControlAutoActiveMode: removing clientIf:" + + clientIf + + ", " + + device + + ", groupId: " + + groupId); + + mLeGattClientsControllingAutoActiveMode.remove(new Pair<>(clientIf, device)); + + if (!mLeGattClientsControllingAutoActiveMode.isEmpty()) { + for (Pair<Integer, BluetoothDevice> pair : + mLeGattClientsControllingAutoActiveMode) { + if (pair.second.equals(device) + || groupId == mLeAudioService.getGroupId(pair.second)) { + Log.d( + TAG, + "removeGattClientFromControlAutoActiveMode:" + + device + + " or groupId: " + + groupId + + " is still in use by clientif: " + + pair.first); + return; + } + } + } + + /* Back auto active mode to default. */ + mLeAudioService.setAutoActiveModeState(groupId, true); + } + + int leConnectedState = + BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE + | BluetoothDevice.CONNECTION_STATE_CONNECTED; + + /* If auto active mode was disabled for the given group and is still connected + * make sure to disconnected all the devices from the group + */ + if (isAutoActiveModeDisabled && ((getConnectionState(device) & leConnectedState) != 0)) { + for (BluetoothDevice dev : mLeAudioService.getGroupDevices(groupId)) { + /* Need to disconnect all the devices from the group as those might be connected + * as well especially those which migh keep the connection + */ + if ((getConnectionState(dev) & leConnectedState) != 0) { + mNativeInterface.disconnectAcl(dev, BluetoothDevice.TRANSPORT_LE); + } + } + } + } + + /** + * Notify AdapterService about failed GATT connection attempt. + * + * @param clientIf ClientIf which was doing GATT connection attempt + * @param device Remote device to which connection attpemt failed + */ + public void notifyGattClientConnectFailed(int clientIf, BluetoothDevice device) { + if (mLeAudioService != null) { + removeGattClientFromControlAutoActiveMode(clientIf, device); + } + } + + /** + * Notify AdapterService about GATT connection being disconnecting or disconnected. + * + * @param clientIf ClientIf which is disconnecting or is already disconnected + * @param device Remote device which is disconnecting or is disconnected + */ + public void notifyGattClientDisconnect(int clientIf, BluetoothDevice device) { + if (mLeAudioService != null) { + removeGattClientFromControlAutoActiveMode(clientIf, device); + } + } + public int getConnectionState(BluetoothDevice device) { final String address = device.getAddress(); if (Flags.apiGetConnectionStateUsingIdentityAddress()) { @@ -6577,6 +6771,12 @@ public class AdapterService extends Service { } writer.println(); + writer.println("LE Gatt clients controlling AutoActiveMode:"); + for (Pair<Integer, BluetoothDevice> pair : mLeGattClientsControllingAutoActiveMode) { + writer.println(" clientIf:" + pair.first + " " + pair.second); + } + writer.println(); + mAdapterStateMachine.dump(fd, writer, args); StringBuilder sb = new StringBuilder(); diff --git a/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java b/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java index 475da0dba1..47a5d9991f 100644 --- a/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java +++ b/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java @@ -274,6 +274,27 @@ public class PhonePolicy implements AdapterService.BluetoothStateCallback { && hap.getConnectionPolicy(device) != CONNECTION_POLICY_FORBIDDEN; } + private boolean shouldBlockBroadcastForHapDevice(BluetoothDevice device, ParcelUuid[] uuids) { + if (!Flags.leaudioDisableBroadcastForHapDevice()) { + Log.i(TAG, "disableBroadcastForHapDevice: Flag is disabled"); + return false; + } + + HapClientService hap = mFactory.getHapClientService(); + if (hap == null) { + Log.e(TAG, "shouldBlockBroadcastForHapDevice: No HapClientService"); + return false; + } + + if (!SystemProperties.getBoolean(SYSPROP_HAP_ENABLED, true)) { + Log.i(TAG, "shouldBlockBroadcastForHapDevice: SystemProperty is overridden to false"); + return false; + } + + return Utils.arrayContains(uuids, BluetoothUuid.HAS) + && hap.getConnectionPolicy(device) == CONNECTION_POLICY_ALLOWED; + } + // Policy implementation, all functions MUST be private private void processInitProfilePriorities(BluetoothDevice device, ParcelUuid[] uuids) { String log = "processInitProfilePriorities(" + device + "): "; @@ -518,7 +539,7 @@ public class PhonePolicy implements AdapterService.BluetoothStateCallback { if ((bcService != null) && Utils.arrayContains(uuids, BluetoothUuid.BASS) && (bcService.getConnectionPolicy(device) == CONNECTION_POLICY_UNKNOWN)) { - if (isLeAudioProfileAllowed) { + if (isLeAudioProfileAllowed && !shouldBlockBroadcastForHapDevice(device, uuids)) { Log.d(TAG, log + "Setting BASS priority"); if (mAutoConnectProfilesSupported) { bcService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED); @@ -531,7 +552,7 @@ public class PhonePolicy implements AdapterService.BluetoothStateCallback { CONNECTION_POLICY_ALLOWED); } } else { - Log.d(TAG, log + "LE_AUDIO is not allowed: Clear BASS priority"); + Log.d(TAG, log + "LE_AUDIO Broadcast is not allowed: Clear BASS priority"); mAdapterService .getDatabase() .setProfileConnectionPolicy( diff --git a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java index f0bad17e64..f297d905ac 100644 --- a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java +++ b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java @@ -345,7 +345,8 @@ public class RemoteDevices { private String mModelName; @VisibleForTesting int mBondState; @VisibleForTesting int mDeviceType; - @VisibleForTesting ParcelUuid[] mUuids; + @VisibleForTesting ParcelUuid[] mUuidsBrEdr; + @VisibleForTesting ParcelUuid[] mUuidsLe; private BluetoothSinkAudioPolicy mAudioPolicy; DeviceProperties() { @@ -506,20 +507,71 @@ public class RemoteDevices { } /** - * @return the mUuids + * @return the UUIDs on LE and Classic transport */ ParcelUuid[] getUuids() { synchronized (mObject) { - return mUuids; + /* When we bond dual mode device, and discover LE and Classic services, stack would + * return LE and Classic UUIDs separately, but Java apps expect them merged. + */ + int combinedUuidsLength = + (mUuidsBrEdr != null ? mUuidsBrEdr.length : 0) + + (mUuidsLe != null ? mUuidsLe.length : 0); + if (!Flags.separateServiceStorage() || combinedUuidsLength == 0) { + return mUuidsBrEdr; + } + + java.util.LinkedHashSet<ParcelUuid> result = + new java.util.LinkedHashSet<ParcelUuid>(); + if (mUuidsBrEdr != null) { + for (ParcelUuid uuid : mUuidsBrEdr) { + result.add(uuid); + } + } + + if (mUuidsLe != null) { + for (ParcelUuid uuid : mUuidsLe) { + result.add(uuid); + } + } + + return result.toArray(new ParcelUuid[combinedUuidsLength]); + } + } + + /** + * @return just classic transport UUIDS + */ + ParcelUuid[] getUuidsBrEdr() { + synchronized (mObject) { + return mUuidsBrEdr; + } + } + + /** + * @param uuids the mUuidsBrEdr to set + */ + void setUuidsBrEdr(ParcelUuid[] uuids) { + synchronized (mObject) { + this.mUuidsBrEdr = uuids; + } + } + + /** + * @return the mUuidsLe + */ + ParcelUuid[] getUuidsLe() { + synchronized (mObject) { + return mUuidsLe; } } /** - * @param uuids the mUuids to set + * @param uuids the mUuidsLe to set */ - void setUuids(ParcelUuid[] uuids) { + void setUuidsLe(ParcelUuid[] uuids) { synchronized (mObject) { - this.mUuids = uuids; + this.mUuidsLe = uuids; } } @@ -636,7 +688,8 @@ public class RemoteDevices { cachedBluetoothDevice issued a connect using the local cached copy of uuids, without waiting for the ACTION_UUID intent. This was resulting in multiple calls to connect().*/ - mUuids = null; + mUuidsBrEdr = null; + mUuidsLe = null; mAlias = null; } } @@ -988,147 +1041,168 @@ public class RemoteDevices { return; } + boolean uuids_updated = false; + for (int j = 0; j < types.length; j++) { type = types[j]; val = values[j]; - if (val.length > 0) { - synchronized (mObject) { - debugLog("Update property, device=" + bdDevice + ", type: " + type); - switch (type) { - case AbstractionLayer.BT_PROPERTY_BDNAME: - final String newName = new String(val); - if (newName.equals(deviceProperties.getName())) { - debugLog("Skip name update for " + bdDevice); - break; - } - deviceProperties.setName(newName); - List<String> wordBreakdownList = - MetricsLogger.getInstance().getWordBreakdownList(newName); - if (SdkLevel.isAtLeastU()) { - MetricsLogger.getInstance() - .uploadRestrictedBluetothDeviceName(wordBreakdownList); - } - intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED); - intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice); - intent.putExtra(BluetoothDevice.EXTRA_NAME, deviceProperties.getName()); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - mAdapterService.sendBroadcast( - intent, - BLUETOOTH_CONNECT, - Utils.getTempBroadcastOptions().toBundle()); - debugLog("Remote device name is: " + deviceProperties.getName()); - break; - case AbstractionLayer.BT_PROPERTY_REMOTE_FRIENDLY_NAME: - deviceProperties.setAlias(bdDevice, new String(val)); - debugLog("Remote device alias is: " + deviceProperties.getAlias()); - break; - case AbstractionLayer.BT_PROPERTY_BDADDR: - deviceProperties.setAddress(val); - debugLog( - "Remote Address is:" - + Utils.getRedactedAddressStringFromByte(val)); + if (val.length == 0) { + continue; + } + + synchronized (mObject) { + debugLog("Update property, device=" + bdDevice + ", type: " + type); + switch (type) { + case AbstractionLayer.BT_PROPERTY_BDNAME: + final String newName = new String(val); + if (newName.equals(deviceProperties.getName())) { + debugLog("Skip name update for " + bdDevice); break; - case AbstractionLayer.BT_PROPERTY_CLASS_OF_DEVICE: - final int newBluetoothClass = Utils.byteArrayToInt(val); - if (newBluetoothClass == deviceProperties.getBluetoothClass()) { - debugLog( - "Skip class update, device=" - + bdDevice - + ", cod=0x" - + Integer.toHexString(newBluetoothClass)); - break; - } - deviceProperties.setBluetoothClass(newBluetoothClass); - intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED); - intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice); - intent.putExtra( - BluetoothDevice.EXTRA_CLASS, - new BluetoothClass(deviceProperties.getBluetoothClass())); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - mAdapterService.sendBroadcast( - intent, - BLUETOOTH_CONNECT, - Utils.getTempBroadcastOptions().toBundle()); + } + deviceProperties.setName(newName); + List<String> wordBreakdownList = + MetricsLogger.getInstance().getWordBreakdownList(newName); + if (SdkLevel.isAtLeastU()) { + MetricsLogger.getInstance() + .uploadRestrictedBluetothDeviceName(wordBreakdownList); + } + intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice); + intent.putExtra(BluetoothDevice.EXTRA_NAME, deviceProperties.getName()); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + mAdapterService.sendBroadcast( + intent, + BLUETOOTH_CONNECT, + Utils.getTempBroadcastOptions().toBundle()); + debugLog("Remote device name is: " + deviceProperties.getName()); + break; + case AbstractionLayer.BT_PROPERTY_REMOTE_FRIENDLY_NAME: + deviceProperties.setAlias(bdDevice, new String(val)); + debugLog("Remote device alias is: " + deviceProperties.getAlias()); + break; + case AbstractionLayer.BT_PROPERTY_BDADDR: + deviceProperties.setAddress(val); + debugLog( + "Remote Address is:" + Utils.getRedactedAddressStringFromByte(val)); + break; + case AbstractionLayer.BT_PROPERTY_CLASS_OF_DEVICE: + final int newBluetoothClass = Utils.byteArrayToInt(val); + if (newBluetoothClass == deviceProperties.getBluetoothClass()) { debugLog( - "Remote class update, device=" + "Skip class update, device=" + bdDevice + ", cod=0x" + Integer.toHexString(newBluetoothClass)); break; - case AbstractionLayer.BT_PROPERTY_UUIDS: + } + deviceProperties.setBluetoothClass(newBluetoothClass); + intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice); + intent.putExtra( + BluetoothDevice.EXTRA_CLASS, + new BluetoothClass(deviceProperties.getBluetoothClass())); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + mAdapterService.sendBroadcast( + intent, + BLUETOOTH_CONNECT, + Utils.getTempBroadcastOptions().toBundle()); + debugLog( + "Remote class update, device=" + + bdDevice + + ", cod=0x" + + Integer.toHexString(newBluetoothClass)); + break; + case AbstractionLayer.BT_PROPERTY_UUIDS: + case AbstractionLayer.BT_PROPERTY_UUIDS_LE: + if (type == AbstractionLayer.BT_PROPERTY_UUIDS) { final ParcelUuid[] newUuids = Utils.byteArrayToUuid(val); - if (areUuidsEqual(newUuids, deviceProperties.getUuids())) { + if (areUuidsEqual(newUuids, deviceProperties.getUuidsBrEdr())) { // SDP Skip adding UUIDs to property cache if equal debugLog("Skip uuids update for " + bdDevice.getAddress()); MetricsLogger.getInstance() .cacheCount(BluetoothProtoEnums.SDP_UUIDS_EQUAL_SKIP, 1); break; } - deviceProperties.setUuids(newUuids); - if (mAdapterService.getState() == BluetoothAdapter.STATE_ON) { - // SDP Adding UUIDs to property cache and sending intent - MetricsLogger.getInstance() - .cacheCount( - BluetoothProtoEnums.SDP_ADD_UUID_WITH_INTENT, 1); - mAdapterService.deviceUuidUpdated(bdDevice); - sendUuidIntent(bdDevice, deviceProperties, true); - } else if (mAdapterService.getState() - == BluetoothAdapter.STATE_BLE_ON) { - // SDP Adding UUIDs to property cache but with no intent - MetricsLogger.getInstance() - .cacheCount( - BluetoothProtoEnums.SDP_ADD_UUID_WITH_NO_INTENT, 1); - mAdapterService.deviceUuidUpdated(bdDevice); - } else { - // SDP Silently dropping UUIDs and with no intent + deviceProperties.setUuidsBrEdr(newUuids); + } else if (type == AbstractionLayer.BT_PROPERTY_UUIDS_LE) { + final ParcelUuid[] newUuidsLe = Utils.byteArrayToUuid(val); + if (areUuidsEqual(newUuidsLe, deviceProperties.getUuidsLe())) { + // SDP Skip adding UUIDs to property cache if equal + debugLog("Skip LE uuids update for " + bdDevice.getAddress()); MetricsLogger.getInstance() - .cacheCount(BluetoothProtoEnums.SDP_DROP_UUID, 1); - } - break; - case AbstractionLayer.BT_PROPERTY_TYPE_OF_DEVICE: - if (deviceProperties.isConsolidated()) { + .cacheCount(BluetoothProtoEnums.SDP_UUIDS_EQUAL_SKIP, 1); break; } - // The device type from hal layer, defined in bluetooth.h, - // matches the type defined in BluetoothDevice.java - deviceProperties.setDeviceType(Utils.byteArrayToInt(val)); - break; - case AbstractionLayer.BT_PROPERTY_REMOTE_RSSI: - // RSSI from hal is in one byte - deviceProperties.setRssi(val[0]); - break; - case AbstractionLayer.BT_PROPERTY_REMOTE_IS_COORDINATED_SET_MEMBER: - deviceProperties.setIsCoordinatedSetMember(val[0] != 0); - break; - case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_CAPABILITY: - deviceProperties.setAshaCapability(val[0]); - break; - case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_TRUNCATED_HISYNCID: - deviceProperties.setAshaTruncatedHiSyncId(val[0]); - break; - case AbstractionLayer.BT_PROPERTY_REMOTE_MODEL_NUM: - final String modelName = new String(val); - debugLog("Remote device model name: " + modelName); - deviceProperties.setModelName(modelName); - BluetoothStatsLog.write( - BluetoothStatsLog.BLUETOOTH_DEVICE_INFO_REPORTED, - mAdapterService.obfuscateAddress(bdDevice), - BluetoothProtoEnums.DEVICE_INFO_INTERNAL, - LOG_SOURCE_DIS, - null, - modelName, - null, - null, - mAdapterService.getMetricId(bdDevice), - bdDevice.getAddressType(), - 0, - 0, - 0); + deviceProperties.setUuidsLe(newUuidsLe); + } + uuids_updated = true; + break; + case AbstractionLayer.BT_PROPERTY_TYPE_OF_DEVICE: + if (deviceProperties.isConsolidated()) { break; - } + } + // The device type from hal layer, defined in bluetooth.h, + // matches the type defined in BluetoothDevice.java + deviceProperties.setDeviceType(Utils.byteArrayToInt(val)); + break; + case AbstractionLayer.BT_PROPERTY_REMOTE_RSSI: + // RSSI from hal is in one byte + deviceProperties.setRssi(val[0]); + break; + case AbstractionLayer.BT_PROPERTY_REMOTE_IS_COORDINATED_SET_MEMBER: + deviceProperties.setIsCoordinatedSetMember(val[0] != 0); + break; + case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_CAPABILITY: + deviceProperties.setAshaCapability(val[0]); + break; + case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_TRUNCATED_HISYNCID: + deviceProperties.setAshaTruncatedHiSyncId(val[0]); + break; + case AbstractionLayer.BT_PROPERTY_REMOTE_MODEL_NUM: + final String modelName = new String(val); + debugLog("Remote device model name: " + modelName); + deviceProperties.setModelName(modelName); + BluetoothStatsLog.write( + BluetoothStatsLog.BLUETOOTH_DEVICE_INFO_REPORTED, + mAdapterService.obfuscateAddress(bdDevice), + BluetoothProtoEnums.DEVICE_INFO_INTERNAL, + LOG_SOURCE_DIS, + null, + modelName, + null, + null, + mAdapterService.getMetricId(bdDevice), + bdDevice.getAddressType(), + 0, + 0, + 0); + break; } } } + + if (!uuids_updated) { + return; + } + + /* uuids_updated == true + * We might have received LE and BREDR UUIDS separately, ensure that UUID intent is sent + * just once */ + + if (mAdapterService.getState() == BluetoothAdapter.STATE_ON) { + // SDP Adding UUIDs to property cache and sending intent + MetricsLogger.getInstance().cacheCount(BluetoothProtoEnums.SDP_ADD_UUID_WITH_INTENT, 1); + mAdapterService.deviceUuidUpdated(bdDevice); + sendUuidIntent(bdDevice, deviceProperties, true); + } else if (mAdapterService.getState() == BluetoothAdapter.STATE_BLE_ON) { + // SDP Adding UUIDs to property cache but with no intent + MetricsLogger.getInstance() + .cacheCount(BluetoothProtoEnums.SDP_ADD_UUID_WITH_NO_INTENT, 1); + mAdapterService.deviceUuidUpdated(bdDevice); + } else { + // SDP Silently dropping UUIDs and with no intent + MetricsLogger.getInstance().cacheCount(BluetoothProtoEnums.SDP_DROP_UUID, 1); + } } void deviceFoundCallback(byte[] address) { diff --git a/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.java b/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.java deleted file mode 100644 index 9e4d281bda..0000000000 --- a/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.bluetooth.gatt; - -import static android.Manifest.permission.BLUETOOTH_ADVERTISE; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; - -import static java.util.Objects.requireNonNull; - -import android.annotation.Nullable; -import android.annotation.RequiresPermission; -import android.bluetooth.IBluetoothAdvertise; -import android.bluetooth.le.AdvertiseData; -import android.bluetooth.le.AdvertisingSetParameters; -import android.bluetooth.le.IAdvertisingSetCallback; -import android.bluetooth.le.PeriodicAdvertisingParameters; -import android.content.AttributionSource; -import android.content.Context; - -import com.android.bluetooth.Utils; - -class AdvertiseBinder extends IBluetoothAdvertise.Stub { - private final Context mContext; - private final AdvertiseManager mAdvertiseManager; - private volatile boolean mIsAvailable = true; - - AdvertiseBinder(Context context, AdvertiseManager manager) { - mContext = context; - mAdvertiseManager = manager; - } - - void cleanup() { - mIsAvailable = false; - } - - @RequiresPermission(BLUETOOTH_ADVERTISE) - @Nullable - private AdvertiseManager getManager(AttributionSource source) { - requireNonNull(source); - if (!Utils.checkAdvertisePermissionForDataDelivery( - mContext, source, "AdvertiseManager startAdvertisingSet")) { - return null; - } - return mIsAvailable ? mAdvertiseManager : null; - } - - @Override - public void startAdvertisingSet( - AdvertisingSetParameters parameters, - @Nullable AdvertiseData advertiseData, - @Nullable AdvertiseData scanResponse, - @Nullable PeriodicAdvertisingParameters periodicParameters, - @Nullable AdvertiseData periodicData, - int duration, - int maxExtAdvEvents, - int serverIf, - IAdvertisingSetCallback callback, - AttributionSource source) { - requireNonNull(parameters); - requireNonNull(callback); - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - if (parameters.getOwnAddressType() != AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT - || serverIf != 0 - || parameters.isDirected()) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - } - manager.startAdvertisingSet( - parameters, - advertiseData, - scanResponse, - periodicParameters, - periodicData, - duration, - maxExtAdvEvents, - serverIf, - callback, - source); - } - - @Override - public void stopAdvertisingSet(IAdvertisingSetCallback callback, AttributionSource source) { - requireNonNull(callback); - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - manager.stopAdvertisingSet(callback); - } - - @Override - public void getOwnAddress(int advertiserId, AttributionSource source) { - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - manager.getOwnAddress(advertiserId); - } - - @Override - public void enableAdvertisingSet( - int advertiserId, - boolean enable, - int duration, - int maxExtAdvEvents, - AttributionSource source) { - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - manager.enableAdvertisingSet(advertiserId, enable, duration, maxExtAdvEvents); - } - - @Override - public void setAdvertisingData( - int advertiserId, @Nullable AdvertiseData data, AttributionSource source) { - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - manager.setAdvertisingData(advertiserId, data); - } - - @Override - public void setScanResponseData( - int advertiserId, @Nullable AdvertiseData data, AttributionSource source) { - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - manager.setScanResponseData(advertiserId, data); - } - - @Override - public void setAdvertisingParameters( - int advertiserId, AdvertisingSetParameters parameters, AttributionSource source) { - requireNonNull(parameters); - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - if (parameters.getOwnAddressType() != AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT - || parameters.isDirected()) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - } - manager.setAdvertisingParameters(advertiserId, parameters); - } - - @Override - public void setPeriodicAdvertisingParameters( - int advertiserId, - @Nullable PeriodicAdvertisingParameters parameters, - AttributionSource source) { - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - manager.setPeriodicAdvertisingParameters(advertiserId, parameters); - } - - @Override - public void setPeriodicAdvertisingData( - int advertiserId, @Nullable AdvertiseData data, AttributionSource source) { - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - manager.setPeriodicAdvertisingData(advertiserId, data); - } - - @Override - public void setPeriodicAdvertisingEnable( - int advertiserId, boolean enable, AttributionSource source) { - AdvertiseManager manager = getManager(source); - if (manager == null) { - return; - } - manager.setPeriodicAdvertisingEnable(advertiserId, enable); - } -} diff --git a/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.kt b/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.kt new file mode 100644 index 0000000000..66415cd82f --- /dev/null +++ b/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.bluetooth.gatt + +import android.Manifest.permission.BLUETOOTH_ADVERTISE +import android.Manifest.permission.BLUETOOTH_PRIVILEGED +import android.annotation.RequiresPermission +import android.bluetooth.IBluetoothAdvertise +import android.bluetooth.le.AdvertiseData +import android.bluetooth.le.AdvertisingSetParameters +import android.bluetooth.le.IAdvertisingSetCallback +import android.bluetooth.le.PeriodicAdvertisingParameters +import android.content.AttributionSource +import android.content.Context +import com.android.bluetooth.Utils + +class AdvertiseBinder( + private val mContext: Context, + private val mAdvertiseManager: AdvertiseManager, +) : IBluetoothAdvertise.Stub() { + @Volatile private var mIsAvailable = true + + fun cleanup() { + mIsAvailable = false + } + + @RequiresPermission(BLUETOOTH_ADVERTISE) + private fun getManager(source: AttributionSource): AdvertiseManager? { + if (!Utils.checkAdvertisePermissionForDataDelivery(mContext, source, "AdvertiseManager")) { + return null + } + return if (mIsAvailable) mAdvertiseManager else null + } + + override fun startAdvertisingSet( + parameters: AdvertisingSetParameters, + advertiseData: AdvertiseData?, + scanResponse: AdvertiseData?, + periodicParameters: PeriodicAdvertisingParameters?, + periodicData: AdvertiseData?, + duration: Int, + maxExtAdvEvents: Int, + serverIf: Int, + callback: IAdvertisingSetCallback, + source: AttributionSource, + ) { + if ( + parameters.ownAddressType != AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT || + serverIf != 0 || + parameters.isDirected + ) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null) + } + + getManager(source)?.let { + it.doOnAdvertiseThread { + it.startAdvertisingSet( + parameters, + advertiseData, + scanResponse, + periodicParameters, + periodicData, + duration, + maxExtAdvEvents, + serverIf, + callback, + source, + ) + } + } + } + + override fun stopAdvertisingSet(callback: IAdvertisingSetCallback, source: AttributionSource) { + getManager(source)?.let { it.doOnAdvertiseThread { it.stopAdvertisingSet(callback) } } + } + + override fun getOwnAddress(advertiserId: Int, source: AttributionSource) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null) + getManager(source)?.let { it.doOnAdvertiseThread { it.getOwnAddress(advertiserId) } } + } + + override fun enableAdvertisingSet( + advertiserId: Int, + enable: Boolean, + duration: Int, + maxExtAdvEvents: Int, + source: AttributionSource, + ) { + getManager(source)?.let { + it.doOnAdvertiseThread { + it.enableAdvertisingSet(advertiserId, enable, duration, maxExtAdvEvents) + } + } + } + + override fun setAdvertisingData( + advertiserId: Int, + data: AdvertiseData?, + source: AttributionSource, + ) { + getManager(source)?.let { + it.doOnAdvertiseThread { it.setAdvertisingData(advertiserId, data) } + } + } + + override fun setScanResponseData( + advertiserId: Int, + data: AdvertiseData?, + source: AttributionSource, + ) { + getManager(source)?.let { + it.doOnAdvertiseThread { it.setScanResponseData(advertiserId, data) } + } + } + + override fun setAdvertisingParameters( + advertiserId: Int, + parameters: AdvertisingSetParameters, + source: AttributionSource, + ) { + if ( + parameters.ownAddressType != AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT || + parameters.isDirected + ) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null) + } + getManager(source)?.let { + it.doOnAdvertiseThread { it.setAdvertisingParameters(advertiserId, parameters) } + } + } + + override fun setPeriodicAdvertisingParameters( + advertiserId: Int, + parameters: PeriodicAdvertisingParameters?, + source: AttributionSource, + ) { + getManager(source)?.let { + it.doOnAdvertiseThread { it.setPeriodicAdvertisingParameters(advertiserId, parameters) } + } + } + + override fun setPeriodicAdvertisingData( + advertiserId: Int, + data: AdvertiseData?, + source: AttributionSource, + ) { + getManager(source)?.let { + it.doOnAdvertiseThread { it.setPeriodicAdvertisingData(advertiserId, data) } + } + } + + override fun setPeriodicAdvertisingEnable( + advertiserId: Int, + enable: Boolean, + source: AttributionSource, + ) { + getManager(source)?.let { + it.doOnAdvertiseThread { it.setPeriodicAdvertisingEnable(advertiserId, enable) } + } + } +} diff --git a/android/app/src/com/android/bluetooth/gatt/AdvertiseManager.java b/android/app/src/com/android/bluetooth/gatt/AdvertiseManager.java index 7e89f99055..6ed719cedc 100644 --- a/android/app/src/com/android/bluetooth/gatt/AdvertiseManager.java +++ b/android/app/src/com/android/bluetooth/gatt/AdvertiseManager.java @@ -24,45 +24,52 @@ import android.bluetooth.le.PeriodicAdvertisingParameters; import android.content.AttributionSource; import android.os.Binder; import android.os.Handler; -import android.os.HandlerThread; import android.os.IBinder; -import android.os.IInterface; import android.os.Looper; import android.os.RemoteException; import android.util.Log; +import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; -import com.android.internal.annotations.GuardedBy; +import com.android.bluetooth.flags.Flags; import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; -/** - * Manages Bluetooth LE advertising operations and interacts with bluedroid stack. TODO: add tests. - */ -@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +/** Manages Bluetooth LE advertising operations. */ public class AdvertiseManager { private static final String TAG = GattServiceConfig.TAG_PREFIX + "AdvertiseManager"; - private final GattService mService; + private static final long RUN_SYNC_WAIT_TIME_MS = 2000L; + + private final AdapterService mService; private final AdvertiseManagerNativeInterface mNativeInterface; private final AdvertiseBinder mAdvertiseBinder; private final AdvertiserMap mAdvertiserMap; - @GuardedBy("itself") private final Map<IBinder, AdvertiserInfo> mAdvertisers = new HashMap<>(); - private Handler mHandler; - static int sTempRegistrationId = -1; + private final Handler mHandler; + private volatile boolean mIsAvailable = true; + @VisibleForTesting int mTempRegistrationId = -1; - AdvertiseManager(GattService service) { - this(service, AdvertiseManagerNativeInterface.getInstance(), new AdvertiserMap()); + AdvertiseManager(AdapterService service, Looper advertiseLooper) { + this( + service, + advertiseLooper, + AdvertiseManagerNativeInterface.getInstance(), + new AdvertiserMap()); } @VisibleForTesting AdvertiseManager( - GattService service, + AdapterService service, + Looper advertiseLooper, AdvertiseManagerNativeInterface nativeInterface, AdvertiserMap advertiserMap) { Log.d(TAG, "advertise manager created"); @@ -70,42 +77,26 @@ public class AdvertiseManager { mNativeInterface = nativeInterface; mAdvertiserMap = advertiserMap; - // Start a HandlerThread that handles advertising operations mNativeInterface.init(this); - HandlerThread thread = new HandlerThread("BluetoothAdvertiseManager"); - thread.start(); - mHandler = new Handler(thread.getLooper()); + mHandler = new Handler(advertiseLooper); mAdvertiseBinder = new AdvertiseBinder(service, this); } - // TODO(b/327849650): We shouldn't need this, it should be safe to do in the cleanup method. But - // it would be a logic change. - void clear() { - mAdvertiserMap.clear(); - } - void cleanup() { Log.d(TAG, "cleanup()"); - mAdvertiseBinder.cleanup(); - mNativeInterface.cleanup(); - synchronized (mAdvertisers) { - mAdvertisers.clear(); - } - sTempRegistrationId = -1; - - if (mHandler != null) { - // Shut down the thread - mHandler.removeCallbacksAndMessages(null); - Looper looper = mHandler.getLooper(); - if (looper != null) { - looper.quit(); - } - mHandler = null; - } + mIsAvailable = false; + mHandler.removeCallbacksAndMessages(null); + forceRunSyncOnAdvertiseThread( + () -> { + mAdvertiserMap.clear(); + mAdvertiseBinder.cleanup(); + mNativeInterface.cleanup(); + mAdvertisers.clear(); + }); } void dump(StringBuilder sb) { - mAdvertiserMap.dump(sb); + forceRunSyncOnAdvertiseThread(() -> mAdvertiserMap.dump(sb)); } AdvertiseBinder getBinder() { @@ -129,8 +120,12 @@ public class AdvertiseManager { } } + private interface CallbackWrapper { + void call() throws RemoteException; + } + IBinder toBinder(IAdvertisingSetCallback e) { - return ((IInterface) e).asBinder(); + return e.asBinder(); } class AdvertisingSetDeathRecipient implements IBinder.DeathRecipient { @@ -145,25 +140,22 @@ public class AdvertiseManager { @Override public void binderDied() { Log.d(TAG, "Binder is dead - unregistering advertising set (" + mPackageName + ")!"); - stopAdvertisingSet(callback); + doOnAdvertiseThread(() -> stopAdvertisingSet(callback)); } } - Map.Entry<IBinder, AdvertiserInfo> findAdvertiser(int advertiserId) { + private Map.Entry<IBinder, AdvertiserInfo> findAdvertiser(int advertiserId) { Map.Entry<IBinder, AdvertiserInfo> entry = null; - synchronized (mAdvertisers) { - for (Map.Entry<IBinder, AdvertiserInfo> e : mAdvertisers.entrySet()) { - if (e.getValue().id == advertiserId) { - entry = e; - break; - } + for (Map.Entry<IBinder, AdvertiserInfo> e : mAdvertisers.entrySet()) { + if (e.getValue().id == advertiserId) { + entry = e; + break; } } return entry; } - void onAdvertisingSetStarted(int regId, int advertiserId, int txPower, int status) - throws Exception { + void onAdvertisingSetStarted(int regId, int advertiserId, int txPower, int status) { Log.d( TAG, "onAdvertisingSetStarted() - regId=" @@ -172,6 +164,7 @@ public class AdvertiseManager { + advertiserId + ", status=" + status); + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(regId); @@ -191,26 +184,24 @@ public class AdvertiseManager { } else { IBinder binder = entry.getKey(); binder.unlinkToDeath(entry.getValue().deathRecipient, 0); - synchronized (mAdvertisers) { - mAdvertisers.remove(binder); - } + mAdvertisers.remove(binder); AppAdvertiseStats stats = mAdvertiserMap.getAppAdvertiseStatsById(regId); if (stats != null) { - int instanceCount; - synchronized (mAdvertisers) { - instanceCount = mAdvertisers.size(); - } - stats.recordAdvertiseStop(instanceCount); + stats.recordAdvertiseStop(mAdvertisers.size()); stats.recordAdvertiseErrorCount(status); } mAdvertiserMap.removeAppAdvertiseStats(regId); } - callback.onAdvertisingSetStarted(mAdvertiseBinder, advertiserId, txPower, status); + sendToCallback( + advertiserId, + () -> + callback.onAdvertisingSetStarted( + mAdvertiseBinder, advertiserId, txPower, status)); } - void onAdvertisingEnabled(int advertiserId, boolean enable, int status) throws Exception { + void onAdvertisingEnabled(int advertiserId, boolean enable, int status) { Log.d( TAG, "onAdvertisingSetEnabled() - advertiserId=" @@ -219,6 +210,7 @@ public class AdvertiseManager { + enable + ", status=" + status); + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { @@ -230,16 +222,13 @@ public class AdvertiseManager { } IAdvertisingSetCallback callback = entry.getValue().callback; - callback.onAdvertisingEnabled(advertiserId, enable, status); + sendToCallback( + advertiserId, () -> callback.onAdvertisingEnabled(advertiserId, enable, status)); if (!enable && status != 0) { AppAdvertiseStats stats = mAdvertiserMap.getAppAdvertiseStatsById(advertiserId); if (stats != null) { - int instanceCount; - synchronized (mAdvertisers) { - instanceCount = mAdvertisers.size(); - } - stats.recordAdvertiseStop(instanceCount); + stats.recordAdvertiseStop(mAdvertisers.size()); } } } @@ -255,6 +244,7 @@ public class AdvertiseManager { int serverIf, IAdvertisingSetCallback callback, AttributionSource attrSource) { + checkThread(); // If we are using an isolated server, force usage of an NRPA if (serverIf != 0 && parameters.getOwnAddressType() @@ -289,7 +279,7 @@ public class AdvertiseManager { throw new IllegalArgumentException("Can't link to advertiser's death"); } - String deviceName = AdapterService.getAdapterService().getName(); + String deviceName = mService.getName(); try { byte[] advDataBytes = AdvertiseHelper.advertiseDataToBytes(advertiseData, deviceName); byte[] scanResponseBytes = @@ -297,10 +287,8 @@ public class AdvertiseManager { byte[] periodicDataBytes = AdvertiseHelper.advertiseDataToBytes(periodicData, deviceName); - int cbId = --sTempRegistrationId; - synchronized (mAdvertisers) { - mAdvertisers.put(binder, new AdvertiserInfo(cbId, deathRecipient, callback)); - } + int cbId = --mTempRegistrationId; + mAdvertisers.put(binder, new AdvertiserInfo(cbId, deathRecipient, callback)); Log.d(TAG, "startAdvertisingSet() - reg_id=" + cbId + ", callback: " + binder); @@ -340,9 +328,9 @@ public class AdvertiseManager { } } - void onOwnAddressRead(int advertiserId, int addressType, String address) - throws RemoteException { + void onOwnAddressRead(int advertiserId, int addressType, String address) { Log.d(TAG, "onOwnAddressRead() advertiserId=" + advertiserId); + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { @@ -351,10 +339,12 @@ public class AdvertiseManager { } IAdvertisingSetCallback callback = entry.getValue().callback; - callback.onOwnAddressRead(advertiserId, addressType, address); + sendToCallback( + advertiserId, () -> callback.onOwnAddressRead(advertiserId, addressType, address)); } void getOwnAddress(int advertiserId) { + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { Log.w(TAG, "getOwnAddress() - bad advertiserId " + advertiserId); @@ -364,13 +354,11 @@ public class AdvertiseManager { } void stopAdvertisingSet(IAdvertisingSetCallback callback) { + checkThread(); IBinder binder = toBinder(callback); Log.d(TAG, "stopAdvertisingSet() " + binder); - AdvertiserInfo adv; - synchronized (mAdvertisers) { - adv = mAdvertisers.remove(binder); - } + AdvertiserInfo adv = mAdvertisers.remove(binder); if (adv == null) { Log.e(TAG, "stopAdvertisingSet() - no client found for callback"); return; @@ -397,6 +385,7 @@ public class AdvertiseManager { } void enableAdvertisingSet(int advertiserId, boolean enable, int duration, int maxExtAdvEvents) { + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { Log.w(TAG, "enableAdvertisingSet() - bad advertiserId " + advertiserId); @@ -408,12 +397,13 @@ public class AdvertiseManager { } void setAdvertisingData(int advertiserId, AdvertiseData data) { + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { Log.w(TAG, "setAdvertisingData() - bad advertiserId " + advertiserId); return; } - String deviceName = AdapterService.getAdapterService().getName(); + String deviceName = mService.getName(); try { mNativeInterface.setAdvertisingData( advertiserId, AdvertiseHelper.advertiseDataToBytes(data, deviceName)); @@ -430,12 +420,13 @@ public class AdvertiseManager { } void setScanResponseData(int advertiserId, AdvertiseData data) { + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { Log.w(TAG, "setScanResponseData() - bad advertiserId " + advertiserId); return; } - String deviceName = AdapterService.getAdapterService().getName(); + String deviceName = mService.getName(); try { mNativeInterface.setScanResponseData( advertiserId, AdvertiseHelper.advertiseDataToBytes(data, deviceName)); @@ -452,6 +443,7 @@ public class AdvertiseManager { } void setAdvertisingParameters(int advertiserId, AdvertisingSetParameters parameters) { + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { Log.w(TAG, "setAdvertisingParameters() - bad advertiserId " + advertiserId); @@ -464,6 +456,7 @@ public class AdvertiseManager { void setPeriodicAdvertisingParameters( int advertiserId, PeriodicAdvertisingParameters parameters) { + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { Log.w(TAG, "setPeriodicAdvertisingParameters() - bad advertiserId " + advertiserId); @@ -475,12 +468,13 @@ public class AdvertiseManager { } void setPeriodicAdvertisingData(int advertiserId, AdvertiseData data) { + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { Log.w(TAG, "setPeriodicAdvertisingData() - bad advertiserId " + advertiserId); return; } - String deviceName = AdapterService.getAdapterService().getName(); + String deviceName = mService.getName(); try { mNativeInterface.setPeriodicAdvertisingData( advertiserId, AdvertiseHelper.advertiseDataToBytes(data, deviceName)); @@ -497,6 +491,7 @@ public class AdvertiseManager { } void setPeriodicAdvertisingEnable(int advertiserId, boolean enable) { + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { Log.w(TAG, "setPeriodicAdvertisingEnable() - bad advertiserId " + advertiserId); @@ -505,7 +500,8 @@ public class AdvertiseManager { mNativeInterface.setPeriodicAdvertisingEnable(advertiserId, enable); } - void onAdvertisingDataSet(int advertiserId, int status) throws Exception { + void onAdvertisingDataSet(int advertiserId, int status) { + checkThread(); Log.d(TAG, "onAdvertisingDataSet() advertiserId=" + advertiserId + ", status=" + status); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); @@ -515,10 +511,11 @@ public class AdvertiseManager { } IAdvertisingSetCallback callback = entry.getValue().callback; - callback.onAdvertisingDataSet(advertiserId, status); + sendToCallback(advertiserId, () -> callback.onAdvertisingDataSet(advertiserId, status)); } - void onScanResponseDataSet(int advertiserId, int status) throws Exception { + void onScanResponseDataSet(int advertiserId, int status) { + checkThread(); Log.d(TAG, "onScanResponseDataSet() advertiserId=" + advertiserId + ", status=" + status); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); @@ -528,11 +525,10 @@ public class AdvertiseManager { } IAdvertisingSetCallback callback = entry.getValue().callback; - callback.onScanResponseDataSet(advertiserId, status); + sendToCallback(advertiserId, () -> callback.onScanResponseDataSet(advertiserId, status)); } - void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status) - throws Exception { + void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status) { Log.d( TAG, "onAdvertisingParametersUpdated() advertiserId=" @@ -541,6 +537,7 @@ public class AdvertiseManager { + txPower + ", status=" + status); + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { @@ -549,16 +546,19 @@ public class AdvertiseManager { } IAdvertisingSetCallback callback = entry.getValue().callback; - callback.onAdvertisingParametersUpdated(advertiserId, txPower, status); + sendToCallback( + advertiserId, + () -> callback.onAdvertisingParametersUpdated(advertiserId, txPower, status)); } - void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) throws Exception { + void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) { Log.d( TAG, "onPeriodicAdvertisingParametersUpdated() advertiserId=" + advertiserId + ", status=" + status); + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { @@ -569,16 +569,19 @@ public class AdvertiseManager { } IAdvertisingSetCallback callback = entry.getValue().callback; - callback.onPeriodicAdvertisingParametersUpdated(advertiserId, status); + sendToCallback( + advertiserId, + () -> callback.onPeriodicAdvertisingParametersUpdated(advertiserId, status)); } - void onPeriodicAdvertisingDataSet(int advertiserId, int status) throws Exception { + void onPeriodicAdvertisingDataSet(int advertiserId, int status) { Log.d( TAG, "onPeriodicAdvertisingDataSet() advertiserId=" + advertiserId + ", status=" + status); + checkThread(); Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId); if (entry == null) { @@ -587,11 +590,11 @@ public class AdvertiseManager { } IAdvertisingSetCallback callback = entry.getValue().callback; - callback.onPeriodicAdvertisingDataSet(advertiserId, status); + sendToCallback( + advertiserId, () -> callback.onPeriodicAdvertisingDataSet(advertiserId, status)); } - void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status) - throws Exception { + void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status) { Log.d( TAG, "onPeriodicAdvertisingEnabled() advertiserId=" @@ -604,9 +607,12 @@ public class AdvertiseManager { Log.i(TAG, "onAdvertisingSetEnable() - bad advertiserId " + advertiserId); return; } + checkThread(); IAdvertisingSetCallback callback = entry.getValue().callback; - callback.onPeriodicAdvertisingEnabled(advertiserId, enable, status); + sendToCallback( + advertiserId, + () -> callback.onPeriodicAdvertisingEnabled(advertiserId, enable, status)); AppAdvertiseStats stats = mAdvertiserMap.getAppAdvertiseStatsById(advertiserId); if (stats != null) { @@ -614,4 +620,52 @@ public class AdvertiseManager { } } + void doOnAdvertiseThread(Runnable r) { + if (mIsAvailable) { + if (Flags.advertiseThread()) { + mHandler.post( + () -> { + if (mIsAvailable) { + r.run(); + } + }); + } else { + r.run(); + } + } + } + + private void forceRunSyncOnAdvertiseThread(Runnable r) { + if (!Flags.advertiseThread()) { + r.run(); + return; + } + final CompletableFuture<Void> future = new CompletableFuture<>(); + mHandler.postAtFrontOfQueue( + () -> { + r.run(); + future.complete(null); + }); + try { + future.get(RUN_SYNC_WAIT_TIME_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + Log.w(TAG, "Unable to complete sync task: " + e); + } + } + + private void checkThread() { + if (Flags.advertiseThread() + && !mHandler.getLooper().isCurrentThread() + && !Utils.isInstrumentationTestMode()) { + throw new IllegalStateException("Not on advertise thread"); + } + } + + private void sendToCallback(int advertiserId, CallbackWrapper wrapper) { + try { + wrapper.call(); + } catch (RemoteException e) { + Log.i(TAG, "RemoteException in callback for advertiserId: " + advertiserId); + } + } } diff --git a/android/app/src/com/android/bluetooth/gatt/AdvertiseManagerNativeInterface.java b/android/app/src/com/android/bluetooth/gatt/AdvertiseManagerNativeInterface.java index 215c709970..6cdd47f97e 100644 --- a/android/app/src/com/android/bluetooth/gatt/AdvertiseManagerNativeInterface.java +++ b/android/app/src/com/android/bluetooth/gatt/AdvertiseManagerNativeInterface.java @@ -19,11 +19,12 @@ package com.android.bluetooth.gatt; import android.bluetooth.le.AdvertisingSetParameters; import android.bluetooth.le.PeriodicAdvertisingParameters; +import androidx.annotation.VisibleForTesting; + import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; /** Native interface for AdvertiseManager */ -@VisibleForTesting +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public class AdvertiseManagerNativeInterface { private static final String TAG = AdvertiseManagerNativeInterface.class.getSimpleName(); @@ -121,43 +122,47 @@ public class AdvertiseManagerNativeInterface { setPeriodicAdvertisingEnableNative(advertiserId, enable); } - void onAdvertisingSetStarted(int regId, int advertiserId, int txPower, int status) - throws Exception { - mManager.onAdvertisingSetStarted(regId, advertiserId, txPower, status); + void onAdvertisingSetStarted(int regId, int advertiserId, int txPower, int status) { + mManager.doOnAdvertiseThread( + () -> mManager.onAdvertisingSetStarted(regId, advertiserId, txPower, status)); } - void onOwnAddressRead(int advertiserId, int addressType, String address) throws Exception { - mManager.onOwnAddressRead(advertiserId, addressType, address); + void onOwnAddressRead(int advertiserId, int addressType, String address) { + mManager.doOnAdvertiseThread( + () -> mManager.onOwnAddressRead(advertiserId, addressType, address)); } - void onAdvertisingEnabled(int advertiserId, boolean enable, int status) throws Exception { - mManager.onAdvertisingEnabled(advertiserId, enable, status); + void onAdvertisingEnabled(int advertiserId, boolean enable, int status) { + mManager.doOnAdvertiseThread( + () -> mManager.onAdvertisingEnabled(advertiserId, enable, status)); } - void onAdvertisingDataSet(int advertiserId, int status) throws Exception { - mManager.onAdvertisingDataSet(advertiserId, status); + void onAdvertisingDataSet(int advertiserId, int status) { + mManager.doOnAdvertiseThread(() -> mManager.onAdvertisingDataSet(advertiserId, status)); } - void onScanResponseDataSet(int advertiserId, int status) throws Exception { - mManager.onScanResponseDataSet(advertiserId, status); + void onScanResponseDataSet(int advertiserId, int status) { + mManager.doOnAdvertiseThread(() -> mManager.onScanResponseDataSet(advertiserId, status)); } - void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status) - throws Exception { - mManager.onAdvertisingParametersUpdated(advertiserId, txPower, status); + void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status) { + mManager.doOnAdvertiseThread( + () -> mManager.onAdvertisingParametersUpdated(advertiserId, txPower, status)); } - void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) throws Exception { - mManager.onPeriodicAdvertisingParametersUpdated(advertiserId, status); + void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) { + mManager.doOnAdvertiseThread( + () -> mManager.onPeriodicAdvertisingParametersUpdated(advertiserId, status)); } - void onPeriodicAdvertisingDataSet(int advertiserId, int status) throws Exception { - mManager.onPeriodicAdvertisingDataSet(advertiserId, status); + void onPeriodicAdvertisingDataSet(int advertiserId, int status) { + mManager.doOnAdvertiseThread( + () -> mManager.onPeriodicAdvertisingDataSet(advertiserId, status)); } - void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status) - throws Exception { - mManager.onPeriodicAdvertisingEnabled(advertiserId, enable, status); + void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status) { + mManager.doOnAdvertiseThread( + () -> mManager.onPeriodicAdvertisingEnabled(advertiserId, enable, status)); } private native void initializeNative(); diff --git a/android/app/src/com/android/bluetooth/gatt/GattService.java b/android/app/src/com/android/bluetooth/gatt/GattService.java index 7773ff9f43..e9563f8908 100644 --- a/android/app/src/com/android/bluetooth/gatt/GattService.java +++ b/android/app/src/com/android/bluetooth/gatt/GattService.java @@ -51,6 +51,7 @@ import android.content.pm.PackageManager.PackageInfoFlags; import android.content.res.Resources; import android.os.Binder; import android.os.Build; +import android.os.HandlerThread; import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; @@ -119,6 +120,9 @@ public class GattService extends ProfileService { private static final Map<String, Integer> EARLY_MTU_EXCHANGE_PACKAGES = Map.of("com.teslamotors", GATT_MTU_MAX); + private static final Map<String, String> GATT_CLIENTS_NOTIFY_TO_ADAPTER_PACKAGES = + Map.of("com.google.android.gms", "com.google.android.gms.findmydevice"); + @VisibleForTesting static final int GATT_CLIENT_LIMIT_PER_APP = 32; @Nullable public final ScanController mScanController; @@ -153,6 +157,7 @@ public class GattService extends ProfileService { private final DistanceMeasurementManager mDistanceMeasurementManager; private final ActivityManager mActivityManager; private final PackageManager mPackageManager; + private final HandlerThread mHandlerThread; public GattService(AdapterService adapterService) { super(requireNonNull(adapterService)); @@ -166,7 +171,12 @@ public class GattService extends ProfileService { mNativeInterface = GattObjectsFactory.getInstance().getNativeInterface(); mNativeInterface.init(this); - mAdvertiseManager = new AdvertiseManager(this); + + // Create a thread to handle LE operations + mHandlerThread = new HandlerThread("Bluetooth LE"); + mHandlerThread.start(); + + mAdvertiseManager = new AdvertiseManager(mAdapterService, mHandlerThread.getLooper()); if (!Flags.scanManagerRefactor()) { mScanController = new ScanController(adapterService); @@ -206,7 +216,6 @@ public class GattService extends ProfileService { if (mScanController != null) { mScanController.stop(); } - mAdvertiseManager.clear(); mClientMap.clear(); mRestrictedHandles.clear(); mServerMap.clear(); @@ -221,6 +230,7 @@ public class GattService extends ProfileService { mNativeInterface.cleanup(); mAdvertiseManager.cleanup(); mDistanceMeasurementManager.cleanup(); + mHandlerThread.quit(); } /** This is only used when Flags.scanManagerRefactor() is true. */ @@ -857,7 +867,9 @@ public class GattService extends ProfileService { + ", connId=" + connId + ", address=" - + BluetoothUtils.toAnonymizedAddress(address)); + + BluetoothUtils.toAnonymizedAddress(address) + + ", status=" + + status); int connectionState = BluetoothProtoEnums.CONNECTION_STATE_DISCONNECTED; if (status == 0) { mClientMap.addConnection(clientIf, connId, address); @@ -871,7 +883,10 @@ public class GattService extends ProfileService { mPermits.putIfAbsent(address, -1); } connectionState = BluetoothProtoEnums.CONNECTION_STATE_CONNECTED; + } else { + mAdapterService.notifyGattClientConnectFailed(clientIf, getDevice(address)); } + ContextMap<IBluetoothGattCallback>.App app = mClientMap.getById(clientIf); if (app != null) { app.callback.onClientConnectionState( @@ -900,6 +915,7 @@ public class GattService extends ProfileService { + BluetoothUtils.toAnonymizedAddress(address)); mClientMap.removeConnection(clientIf, connId); + mAdapterService.notifyGattClientDisconnect(clientIf, getDevice(address)); ContextMap<IBluetoothGattCallback>.App app = mClientMap.getById(clientIf); mRestrictedHandles.remove(connId); @@ -1496,6 +1512,10 @@ public class GattService extends ProfileService { unregisterClient( appId, attributionSource, ContextMap.RemoveReason.REASON_UNREGISTER_ALL); } + for (Integer appId : mServerMap.getAllAppsIds()) { + Log.d(TAG, "unreg:" + appId); + unregisterServer(appId, attributionSource); + } } /************************************************************************** @@ -1631,6 +1651,21 @@ public class GattService extends ProfileService { } } + if (transport != BluetoothDevice.TRANSPORT_BREDR && isDirect && !opportunistic) { + String attributionTag = getLastAttributionTag(attributionSource); + if (packageName != null && attributionTag != null) { + for (Map.Entry<String, String> entry : + GATT_CLIENTS_NOTIFY_TO_ADAPTER_PACKAGES.entrySet()) { + if (packageName.contains(entry.getKey()) + && attributionTag.contains(entry.getValue())) { + mAdapterService.notifyDirectLeGattClientConnect( + clientIf, getDevice(address)); + break; + } + } + } + } + mNativeInterface.gattClientConnect( clientIf, address, @@ -1669,6 +1704,9 @@ public class GattService extends ProfileService { .BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__GATT_DISCONNECT_JAVA, BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__START, attributionSource.getUid()); + + mAdapterService.notifyGattClientDisconnect(clientIf, getDevice(address)); + mNativeInterface.gattClientDisconnect(clientIf, address, connId != null ? connId : 0); } diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java index 949db48483..179b7fafb0 100644 --- a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java +++ b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java @@ -1502,9 +1502,6 @@ public class HeadsetService extends ProfileService { } else { broadcastActiveDevice(mActiveDevice); } - if (Flags.updateActiveDeviceInBandRingtone()) { - updateInbandRinging(device, true); - } } else if (shouldPersistAudio()) { if (Utils.isScoManagedByAudioEnabled()) { // tell Audio Framework that active device changed @@ -1546,9 +1543,9 @@ public class HeadsetService extends ProfileService { } else { broadcastActiveDevice(mActiveDevice); } - if (Flags.updateActiveDeviceInBandRingtone()) { - updateInbandRinging(device, true); - } + } + if (Flags.updateActiveDeviceInBandRingtone()) { + updateInbandRinging(device, true); } } return true; diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java index fd56faa613..fb0ed9d2f0 100644 --- a/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java +++ b/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java @@ -55,7 +55,6 @@ import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.storage.DatabaseManager; -import com.android.bluetooth.flags.Flags; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.State; import com.android.internal.util.StateMachine; @@ -1122,12 +1121,6 @@ class HeadsetStateMachine extends StateMachine { } } break; - case INTENT_SCO_VOLUME_CHANGED: - if (Flags.hfpAllowVolumeChangeWithoutSco()) { - // when flag is removed, remove INTENT_SCO_VOLUME_CHANGED case in AudioOn - processIntentScoVolume((Intent) message.obj, mDevice); - } - break; case INTENT_CONNECTION_ACCESS_REPLY: handleAccessPermissionResult((Intent) message.obj); break; @@ -1630,8 +1623,6 @@ class HeadsetStateMachine extends StateMachine { break; } case INTENT_SCO_VOLUME_CHANGED: - // TODO: b/362313390 Remove this case once the fix is in place because this - // message will be handled by the ConnectedBase state. processIntentScoVolume((Intent) message.obj, mDevice); break; case STACK_EVENT: 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 8c9ca8735b..bb5346fe6e 100644 --- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java @@ -102,6 +102,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -142,6 +143,9 @@ public class LeAudioService extends ProfileService { /** Indicates group is active */ private static final int ACTIVE_STATE_ACTIVE = 0x02; + /** Filter for Targeted Announcements */ + static final byte[] CAP_TARGETED_ANNOUNCEMENT_PAYLOAD = new byte[] {0x01}; + /** This is used by application read-only for checking the fallback active group id. */ public static final String BLUETOOTH_LE_BROADCAST_FALLBACK_ACTIVE_GROUP_ID = "bluetooth_le_broadcast_fallback_active_group_id"; @@ -255,6 +259,7 @@ public class LeAudioService extends ProfileService { mInputSelectableConfig = new ArrayList<>(); mOutputSelectableConfig = new ArrayList<>(); mInactivatedDueToContextType = false; + mAutoActiveModeEnabled = true; } Integer mGroupId; @@ -270,6 +275,7 @@ public class LeAudioService extends ProfileService { List<BluetoothLeAudioCodecConfig> mInputSelectableConfig; List<BluetoothLeAudioCodecConfig> mOutputSelectableConfig; Boolean mInactivatedDueToContextType; + Boolean mAutoActiveModeEnabled; private Integer mActiveState; private Integer mAllowedSinkContexts; @@ -816,7 +822,7 @@ public class LeAudioService extends ProfileService { } @VisibleForTesting - static synchronized void setLeAudioService(LeAudioService instance) { + public static synchronized void setLeAudioService(LeAudioService instance) { Log.d(TAG, "setLeAudioService(): set to: " + instance); sLeAudioService = instance; } @@ -1650,6 +1656,31 @@ public class LeAudioService extends ProfileService { && groupId == mUnicastGroupIdDeactivatedForBroadcastTransition; } + /** Get local broadcast receiving devices */ + public Set<BluetoothDevice> getLocalBroadcastReceivers() { + if (mBroadcastDescriptors == null) { + Log.e(TAG, "getLocalBroadcastReceivers: Invalid Broadcast Descriptors"); + return Collections.emptySet(); + } + + BassClientService bassClientService = getBassClientService(); + if (bassClientService == null) { + Log.e(TAG, "getLocalBroadcastReceivers: Bass service not available"); + return Collections.emptySet(); + } + + Set<BluetoothDevice> deviceList = new HashSet<>(); + for (Map.Entry<Integer, LeAudioBroadcastDescriptor> entry : + mBroadcastDescriptors.entrySet()) { + if (!entry.getValue().mState.equals(LeAudioStackEvent.BROADCAST_STATE_STOPPED)) { + List<BluetoothDevice> devices = + bassClientService.getSyncedBroadcastSinks(entry.getKey()); + deviceList.addAll(devices); + } + } + return deviceList; + } + private boolean areBroadcastsAllStopped() { if (mBroadcastDescriptors == null) { Log.e(TAG, "areBroadcastsAllStopped: Invalid Broadcast Descriptors"); @@ -1987,12 +2018,37 @@ public class LeAudioService extends ProfileService { mExposedActiveDevice = device; } + boolean isAnyGroupDisabledFromAutoActiveMode() { + mGroupReadLock.lock(); + try { + for (Map.Entry<Integer, LeAudioGroupDescriptor> groupEntry : + mGroupDescriptorsView.entrySet()) { + LeAudioGroupDescriptor groupDescriptor = groupEntry.getValue(); + if (!groupDescriptor.mAutoActiveModeEnabled) { + Log.d( + TAG, + "isAnyGroupDisabledFromAutoActiveMode: disabled groupId " + + groupEntry.getKey()); + return true; + } + } + } finally { + mGroupReadLock.unlock(); + } + return false; + } + boolean isScannerNeeded() { if (mDeviceDescriptors.isEmpty() || !mBluetoothEnabled) { Log.d(TAG, "isScannerNeeded: false, mBluetoothEnabled: " + mBluetoothEnabled); return false; } + if (isAnyGroupDisabledFromAutoActiveMode()) { + Log.d(TAG, "isScannerNeeded true, some group has disabled Auto Active Mode"); + return true; + } + if (allLeAudioDevicesConnected()) { Log.d(TAG, "isScannerNeeded: all devices connected, scanner not needed"); return false; @@ -2060,9 +2116,7 @@ public class LeAudioService extends ProfileService { .getBluetoothScanController() .stopScanInternal(mScannerId); - mAdapterService - .getBluetoothScanController() - .unregisterScannerInternal(mScannerId); + mAdapterService.getBluetoothScanController().unregisterScannerInternal(mScannerId); mScannerId = SCANNER_NOT_INITIALIZED; } @@ -2075,18 +2129,15 @@ public class LeAudioService extends ProfileService { } mScannerId = scannerId; - /* Filter we are building here will not match to anything. - * Eventually we should be able to start scan from native when - * b/276350722 is done - */ ScanFilter filter = new ScanFilter.Builder() - .setServiceData(BluetoothUuid.LE_AUDIO, new byte[] {0x11}) + .setServiceData(BluetoothUuid.CAP, CAP_TARGETED_ANNOUNCEMENT_PAYLOAD) .build(); ScanSettings settings = new ScanSettings.Builder() .setLegacy(false) + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .setScanMode(ScanSettings.SCAN_MODE_BALANCED) .setPhy(BluetoothDevice.PHY_LE_1M) .build(); @@ -2096,10 +2147,28 @@ public class LeAudioService extends ProfileService { .startScanInternal(scannerId, settings, List.of(filter)); } - // Eventually we should be able to start scan from native when b/276350722 is done - // All the result returned here are ignored @Override - public void onScanResult(ScanResult scanResult) {} + public void onScanResult(ScanResult scanResult) { + Log.d(TAG, "onScanResult: " + scanResult.getDevice()); + BluetoothDevice device = scanResult.getDevice(); + if (device == null) { + return; + } + + int groupId = getGroupId(device); + LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); + if (descriptor == null) { + return; + } + + if (!descriptor.mAutoActiveModeEnabled) { + Log.i(TAG, "onScanResult: GroupId: " + groupId + " is getting Active"); + descriptor.mAutoActiveModeEnabled = true; + if (!getConnectedPeerDevices(groupId).isEmpty()) { + setActiveDevice(device); + } + } + } @Override public void onBatchScanResults(List<ScanResult> batchResults) {} @@ -2398,6 +2467,25 @@ public class LeAudioService extends ProfileService { } } + private void clearAutoActiveModeToDefault() { + mGroupReadLock.lock(); + try { + for (Map.Entry<Integer, LeAudioGroupDescriptor> groupEntry : + mGroupDescriptorsView.entrySet()) { + LeAudioGroupDescriptor groupDescriptor = groupEntry.getValue(); + if (!groupDescriptor.mAutoActiveModeEnabled) { + Log.d( + TAG, + "mAutoActiveModeEnabled back to default for groupId: " + + groupEntry.getKey()); + groupDescriptor.mAutoActiveModeEnabled = true; + } + } + } finally { + mGroupReadLock.unlock(); + } + } + /** * Set the active device group. * @@ -2416,6 +2504,9 @@ public class LeAudioService extends ProfileService { groupId = descriptor.mGroupId; + /* User force device being active, clear the flag */ + clearAutoActiveModeToDefault(); + if (!isGroupAvailableForStream(groupId)) { Log.e( TAG, @@ -2442,11 +2533,10 @@ public class LeAudioService extends ProfileService { + ", mExposedActiveDevice: " + mExposedActiveDevice); - if (!Flags.leaudioBroadcastPrimaryGroupSelection() - && isBroadcastActive() - && currentlyActiveGroupId == LE_AUDIO_GROUP_ID_INVALID - && mUnicastGroupIdDeactivatedForBroadcastTransition != LE_AUDIO_GROUP_ID_INVALID) { - + /* Replace fallback unicast and monitoring input device if device is active local + * broadcaster. + */ + if (isAnyBroadcastInStreamingState()) { LeAudioGroupDescriptor fallbackGroupDescriptor = getGroupDescriptor(groupId); // If broadcast is ongoing and need to update unicast fallback active group @@ -3125,12 +3215,16 @@ public class LeAudioService extends ProfileService { } } + private boolean isAnyBroadcastInStreamingState() { + return mBroadcastDescriptors.values().stream() + .anyMatch(d -> d.mState.equals(LeAudioStackEvent.BROADCAST_STATE_STREAMING)); + } + void transitionFromBroadcastToUnicast() { if (mUnicastGroupIdDeactivatedForBroadcastTransition == LE_AUDIO_GROUP_ID_INVALID) { Log.d(TAG, "No deactivated group due for broadcast transmission"); // Notify audio manager - if (mBroadcastDescriptors.values().stream() - .noneMatch(d -> d.mState.equals(LeAudioStackEvent.BROADCAST_STATE_STREAMING))) { + if (!isAnyBroadcastInStreamingState()) { updateBroadcastActiveDevice(null, mActiveBroadcastAudioDevice, false); } return; @@ -3896,11 +3990,7 @@ public class LeAudioService extends ProfileService { } // Notify audio manager - if (mBroadcastDescriptors.values().stream() - .anyMatch( - d -> - d.mState.equals( - LeAudioStackEvent.BROADCAST_STATE_STREAMING))) { + if (isAnyBroadcastInStreamingState()) { if (!Objects.equals(device, mActiveBroadcastAudioDevice)) { updateBroadcastActiveDevice(device, mActiveBroadcastAudioDevice, true); } @@ -4189,6 +4279,7 @@ public class LeAudioService extends ProfileService { if (getConnectedPeerDevices(groupId).isEmpty()) { descriptor.mIsConnected = false; + descriptor.mAutoActiveModeEnabled = true; descriptor.mAvailableContexts = Flags.leaudioUnicastNoAvailableContexts() ? null : 0; if (descriptor.isActive()) { /* Notify Native layer */ @@ -4531,6 +4622,91 @@ public class LeAudioService extends ProfileService { } /** + * Set auto active mode state. + * + * <p>Auto Active Mode by default is set to true and it means, that after ACL connection is + * created to LeAudio device which is part of the group, can be connected to Audio Framework + * (set as Active). + * + * <p>If Auto Active Mode is set to false, it means that after LeAudio device is connected for + * given group, the function isGroupAvailableForStream(groupId) will return false and + * ActiveDeviceManager will not make this group active. + * + * <p>This mode can change internally when two things happen: 1. LeAudioService detects Targeted + * Announcements from the device which belongs to the group. + * 2. @BluetoothLeAudio.setActiveDevice() is called with a device which belongs to the group. + * + * <p>Note: Auto Active Mode can be disabled only when all devices from the group are + * disconnected. + * + * @param groupId LeAudio group id which Auto Active Mode should be changed. + * @param enabled true when Auto Active Mode should be enabled (default value), false otherwise. + * @return true when Auto Active Mode is set, false otherwise + */ + public boolean setAutoActiveModeState(int groupId, boolean enabled) { + Log.d(TAG, "setAutoActiveModeState: groupId: " + groupId + " enabled: " + enabled); + + mGroupReadLock.lock(); + try { + LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); + if (descriptor == null) { + Log.i( + TAG, + "setAutoActiveModeState: groupId: " + + groupId + + " is not known LeAudio group"); + return false; + } + + /* Disabling Auto Active Mode is allowed only when all the devices from the group + * are disconnected */ + if (!enabled && descriptor.mIsConnected) { + Log.i( + TAG, + "setAutoActiveModeState: GroupId: " + groupId + " is already connected "); + return false; + } + + Log.i(TAG, "setAutoActiveModeState: groupId: " + groupId + ", enabled: " + enabled); + descriptor.mAutoActiveModeEnabled = enabled; + return true; + } finally { + mGroupReadLock.unlock(); + } + } + + /** + * Is Auto Active Mode enabled + * + * @param groupId LeAudio group id which Auto Active Mode should be taken. + * @return true when Auto Active Mode is enabled, false otherwise + */ + public boolean isAutoActiveModeEnabled(int groupId) { + mGroupReadLock.lock(); + try { + LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); + if (descriptor == null) { + Log.i( + TAG, + "isAutoActiveModeEnabled: groupId: " + + groupId + + " is not known LeAudio group"); + return false; + } + Log.i( + TAG, + "isAutoActiveModeEnabled: groupId: " + + groupId + + ", mAutoActiveModeEnabled: " + + descriptor.mAutoActiveModeEnabled); + return descriptor.mAutoActiveModeEnabled; + + } finally { + mGroupReadLock.unlock(); + } + } + + /** * Check if group is available for streaming. If there is no available context types then group * is not available for streaming. * @@ -4542,9 +4718,21 @@ public class LeAudioService extends ProfileService { try { LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); if (descriptor == null) { - Log.e(TAG, "getGroupId: No valid descriptor for groupId: " + groupId); + Log.e( + TAG, + "isGroupAvailableForStream: No valid descriptor for groupId: " + groupId); return false; } + + if (!descriptor.mAutoActiveModeEnabled) { + Log.e( + TAG, + "isGroupAvailableForStream: Auto Active Mode is disabled for groupId: " + + groupId); + return false; + } + + Log.i(TAG, " descriptor.mAvailableContexts: " + descriptor.mAvailableContexts); return descriptor.mAvailableContexts != null && descriptor.mAvailableContexts != 0; } finally { mGroupReadLock.unlock(); @@ -6056,6 +6244,8 @@ public class LeAudioService extends ProfileService { sb, "mInactivatedDueToContextType: " + groupDescriptor.mInactivatedDueToContextType); + ProfileService.println( + sb, "mAutoActiveModeEnabled: " + groupDescriptor.mAutoActiveModeEnabled); for (Map.Entry<BluetoothDevice, LeAudioDeviceDescriptor> deviceEntry : mDeviceDescriptors.entrySet()) { diff --git a/android/app/src/com/android/bluetooth/le_scan/ScanController.java b/android/app/src/com/android/bluetooth/le_scan/ScanController.java index a8f5a4025e..7f39fdaaf1 100644 --- a/android/app/src/com/android/bluetooth/le_scan/ScanController.java +++ b/android/app/src/com/android/bluetooth/le_scan/ScanController.java @@ -91,7 +91,7 @@ public class ScanController { private static final int TRUNCATED_RESULT_SIZE = 11; /** The default floor value for LE batch scan report delays greater than 0 */ - @VisibleForTesting static final long DEFAULT_REPORT_DELAY_FLOOR = 5000; + static final long DEFAULT_REPORT_DELAY_FLOOR = 5000L; private static final int NUM_SCAN_EVENTS_KEPT = 20; @@ -759,17 +759,19 @@ public class ScanController { } } } - if (permittedResults.isEmpty()) { - return; - } } if (client.hasDisavowedLocation) { permittedResults.removeIf(mLocationDenylistPredicate); } + if (permittedResults.isEmpty()) { + mScanManager.callbackDone(scannerId, status); + return; + } if (app.mCallback != null) { app.mCallback.onBatchScanResults(permittedResults); + mScanManager.batchScanResultDelivered(); } else { // PendingIntent based try { @@ -791,6 +793,9 @@ public class ScanController { @SuppressWarnings("NonApiType") private void sendBatchScanResults( ScannerMap.ScannerApp app, ScanClient client, ArrayList<ScanResult> results) { + if (results.isEmpty()) { + return; + } try { if (app.mCallback != null) { if (mScanManager.isAutoBatchScanClientEnabled(client)) { @@ -811,6 +816,7 @@ public class ScanController { Log.e(TAG, "Exception: " + e); handleDeadScanClient(client); } + mScanManager.batchScanResultDelivered(); } // Check and deliver scan results for different scan clients. @@ -833,14 +839,11 @@ public class ScanController { } } } - if (permittedResults.isEmpty()) { - return; - } } if (client.filters == null || client.filters.isEmpty()) { sendBatchScanResults(app, client, permittedResults); - // TODO: Question to reviewer: Shouldn't there be a return here? + return; } // Reconstruct the scan results. ArrayList<ScanResult> results = new ArrayList<ScanResult>(); diff --git a/android/app/src/com/android/bluetooth/le_scan/ScanManager.java b/android/app/src/com/android/bluetooth/le_scan/ScanManager.java index b228bf5fc8..e946b3afcf 100644 --- a/android/app/src/com/android/bluetooth/le_scan/ScanManager.java +++ b/android/app/src/com/android/bluetooth/le_scan/ScanManager.java @@ -146,6 +146,7 @@ public class ScanManager { @VisibleForTesting boolean mIsConnecting; @VisibleForTesting int mProfilesConnecting; private int mProfilesConnected, mProfilesDisconnecting; + private final BatchScanThrottler mBatchScanThrottler; @VisibleForTesting static class UidImportance { @@ -158,7 +159,7 @@ public class ScanManager { } } - public ScanManager( + ScanManager( AdapterService adapterService, ScanController scanController, BluetoothAdapterProxy bluetoothAdapterProxy, @@ -202,13 +203,13 @@ public class ScanManager { IntentFilter locationIntentFilter = new IntentFilter(LocationManager.MODE_CHANGED_ACTION); locationIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); mAdapterService.registerReceiver(mLocationReceiver, locationIntentFilter); + mBatchScanThrottler = new BatchScanThrottler(timeProvider, mScreenOn); } - public void cleanup() { + void cleanup() { mRegularScanClients.clear(); mBatchClients.clear(); mSuspendedScanClients.clear(); - mScanNative.cleanup(); if (mActivityManager != null) { try { @@ -225,6 +226,8 @@ public class ScanManager { // Shut down the thread mHandler.removeCallbacksAndMessages(null); + mScanNative.cleanup(); + try { mAdapterService.unregisterReceiver(mLocationReceiver); } catch (IllegalArgumentException e) { @@ -232,16 +235,16 @@ public class ScanManager { } } - public void registerScanner(UUID uuid) { + void registerScanner(UUID uuid) { mScanNative.registerScanner(uuid.getLeastSignificantBits(), uuid.getMostSignificantBits()); } - public void unregisterScanner(int scannerId) { + void unregisterScanner(int scannerId) { mScanNative.unregisterScanner(scannerId); } /** Returns the regular scan queue. */ - public Set<ScanClient> getRegularScanQueue() { + Set<ScanClient> getRegularScanQueue() { return mRegularScanClients; } @@ -251,12 +254,12 @@ public class ScanManager { } /** Returns batch scan queue. */ - public Set<ScanClient> getBatchScanQueue() { + Set<ScanClient> getBatchScanQueue() { return mBatchClients; } /** Returns a set of full batch scan clients. */ - public Set<ScanClient> getFullBatchScanQueue() { + Set<ScanClient> getFullBatchScanQueue() { // TODO: split full batch scan clients and truncated batch clients so we don't need to // construct this every time. Set<ScanClient> fullBatchClients = new HashSet<ScanClient>(); @@ -268,12 +271,12 @@ public class ScanManager { return fullBatchClients; } - public void startScan(ScanClient client) { + void startScan(ScanClient client) { Log.d(TAG, "startScan() " + client); sendMessage(MSG_START_BLE_SCAN, client); } - public void stopScan(int scannerId) { + void stopScan(int scannerId) { ScanClient client = mScanNative.getBatchScanClient(scannerId); if (client == null) { client = mScanNative.getRegularScanClient(scannerId); @@ -284,14 +287,18 @@ public class ScanManager { sendMessage(MSG_STOP_BLE_SCAN, client); } - public void flushBatchScanResults(ScanClient client) { + void flushBatchScanResults(ScanClient client) { sendMessage(MSG_FLUSH_BATCH_RESULTS, client); } - public void callbackDone(int scannerId, int status) { + void callbackDone(int scannerId, int status) { mScanNative.callbackDone(scannerId, status); } + void batchScanResultDelivered() { + mBatchScanThrottler.resetBackoff(); + } + private void sendMessage(int what, ScanClient client) { mHandler.obtainMessage(what, client).sendToTarget(); } @@ -304,10 +311,16 @@ public class ScanManager { return mBluetoothAdapterProxy.isOffloadedScanFilteringSupported(); } - public boolean isAutoBatchScanClientEnabled(ScanClient client) { + boolean isAutoBatchScanClientEnabled(ScanClient client) { return mScanNative.isAutoBatchScanClientEnabled(client); } + int getCurrentUsedTrackingAdvertisement() { + synchronized (mCurUsedTrackableAdvertisementsLock) { + return mCurUsedTrackableAdvertisements; + } + } + // Handler class that handles BLE scan operations. @VisibleForTesting class ClientHandler extends Handler { @@ -533,6 +546,7 @@ public class ScanManager { } mScreenOn = false; Log.d(TAG, "handleScreenOff()"); + mBatchScanThrottler.onScreenOn(false); handleSuspendScans(); updateRegularScanClientsScreenOff(); updateRegularScanToBatchScanClients(); @@ -863,6 +877,7 @@ public class ScanManager { } mScreenOn = true; Log.d(TAG, "handleScreenOn()"); + mBatchScanThrottler.onScreenOn(true); updateBatchScanToRegularScanClients(); handleResumeScans(); updateRegularScanClientsScreenOn(); @@ -920,14 +935,14 @@ public class ScanManager { /** Parameters for batch scans. */ static class BatchScanParams { - public int scanMode; - public int fullScanscannerId; - public int truncatedScanscannerId; + @VisibleForTesting int mScanMode; + private int mFullScanscannerId; + private int mTruncatedScanscannerId; BatchScanParams() { - scanMode = -1; - fullScanscannerId = -1; - truncatedScanscannerId = -1; + mScanMode = -1; + mFullScanscannerId = -1; + mTruncatedScanscannerId = -1; } @Override @@ -938,20 +953,14 @@ public class ScanManager { if (!(obj instanceof BatchScanParams other)) { return false; } - return scanMode == other.scanMode - && fullScanscannerId == other.fullScanscannerId - && truncatedScanscannerId == other.truncatedScanscannerId; + return mScanMode == other.mScanMode + && mFullScanscannerId == other.mFullScanscannerId + && mTruncatedScanscannerId == other.mTruncatedScanscannerId; } @Override public int hashCode() { - return Objects.hash(scanMode, fullScanscannerId, truncatedScanscannerId); - } - } - - public int getCurrentUsedTrackingAdvertisement() { - synchronized (mCurUsedTrackableAdvertisementsLock) { - return mCurUsedTrackableAdvertisements; + return Objects.hash(mScanMode, mFullScanscannerId, mTruncatedScanscannerId); } } @@ -1261,9 +1270,9 @@ public class ScanManager { waitForCallback(); resetCountDownLatch(); int scanInterval = - Utils.millsToUnit(getBatchScanIntervalMillis(batchScanParams.scanMode)); + Utils.millsToUnit(getBatchScanIntervalMillis(batchScanParams.mScanMode)); int scanWindow = - Utils.millsToUnit(getBatchScanWindowMillis(batchScanParams.scanMode)); + Utils.millsToUnit(getBatchScanWindowMillis(batchScanParams.mScanMode)); mNativeInterface.gattClientStartBatchScan( scannerId, resultType, @@ -1297,15 +1306,15 @@ public class ScanManager { BatchScanParams params = new BatchScanParams(); ScanClient winner = getAggressiveClient(mBatchClients); if (winner != null) { - params.scanMode = winner.settings.getScanMode(); + params.mScanMode = winner.settings.getScanMode(); } // TODO: split full batch scan results and truncated batch scan results to different // collections. for (ScanClient client : mBatchClients) { if (client.settings.getScanResultType() == ScanSettings.SCAN_RESULT_TYPE_FULL) { - params.fullScanscannerId = client.scannerId; + params.mFullScanscannerId = client.scannerId; } else { - params.truncatedScanscannerId = client.scannerId; + params.mTruncatedScanscannerId = client.scannerId; } } return params; @@ -1358,7 +1367,10 @@ public class ScanManager { if (mBatchClients.isEmpty()) { return; } - long batchTriggerIntervalMillis = getBatchTriggerIntervalMillis(); + long batchTriggerIntervalMillis = + Flags.batchScanOptimization() + ? mBatchScanThrottler.getBatchTriggerIntervalMillis(mBatchClients) + : getBatchTriggerIntervalMillis(); // Allows the alarm to be triggered within // [batchTriggerIntervalMillis, 1.1 * batchTriggerIntervalMillis] long windowLengthMillis = batchTriggerIntervalMillis / 10; @@ -1493,16 +1505,16 @@ public class ScanManager { void flushBatchResults(int scannerId) { Log.d(TAG, "flushPendingBatchResults - scannerId = " + scannerId); - if (mBatchScanParams.fullScanscannerId != -1) { + if (mBatchScanParams.mFullScanscannerId != -1) { resetCountDownLatch(); mNativeInterface.gattClientReadScanReports( - mBatchScanParams.fullScanscannerId, SCAN_RESULT_TYPE_FULL); + mBatchScanParams.mFullScanscannerId, SCAN_RESULT_TYPE_FULL); waitForCallback(); } - if (mBatchScanParams.truncatedScanscannerId != -1) { + if (mBatchScanParams.mTruncatedScanscannerId != -1) { resetCountDownLatch(); mNativeInterface.gattClientReadScanReports( - mBatchScanParams.truncatedScanscannerId, SCAN_RESULT_TYPE_TRUNCATED); + mBatchScanParams.mTruncatedScanscannerId, SCAN_RESULT_TYPE_TRUNCATED); waitForCallback(); } setBatchAlarm(); @@ -1661,13 +1673,13 @@ public class ScanManager { /** Return batch scan result type value defined in bt stack. */ private int getResultType(BatchScanParams params) { - if (params.fullScanscannerId != -1 && params.truncatedScanscannerId != -1) { + if (params.mFullScanscannerId != -1 && params.mTruncatedScanscannerId != -1) { return SCAN_RESULT_TYPE_BOTH; } - if (params.truncatedScanscannerId != -1) { + if (params.mTruncatedScanscannerId != -1) { return SCAN_RESULT_TYPE_TRUNCATED; } - if (params.fullScanscannerId != -1) { + if (params.mFullScanscannerId != -1) { return SCAN_RESULT_TYPE_FULL; } return -1; diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppService.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppService.java index 4bf981a0a5..b912e8e966 100644 --- a/android/app/src/com/android/bluetooth/opp/BluetoothOppService.java +++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppService.java @@ -724,8 +724,9 @@ public class BluetoothOppService extends ProfileService implements IObexConnecti mPendingUpdate = false; } Cursor cursor = - getContentResolver() - .query( + BluetoothMethodProxy.getInstance() + .contentResolverQuery( + getContentResolver(), BluetoothShare.CONTENT_URI, null, null, diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGatt.java b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java index 803b249a3d..9403b5fc93 100644 --- a/android/app/src/com/android/bluetooth/tbs/TbsGatt.java +++ b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java @@ -19,6 +19,8 @@ package com.android.bluetooth.tbs; import static android.bluetooth.BluetoothDevice.METADATA_GTBS_CCCD; +import static java.util.Objects.requireNonNull; + import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; @@ -27,7 +29,6 @@ import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattServerCallback; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothProfile; -import android.content.Context; import android.net.Uri; import android.os.Handler; import android.os.Looper; @@ -48,12 +49,10 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.UUID; public class TbsGatt { - - private static final String TAG = "TbsGatt"; + private static final String TAG = TbsGatt.class.getSimpleName(); private static final String UUID_PREFIX = "0000"; private static final String UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb"; @@ -66,51 +65,50 @@ public class TbsGatt { @VisibleForTesting static final UUID UUID_BEARER_TECHNOLOGY = makeUuid("2BB5"); @VisibleForTesting static final UUID UUID_BEARER_URI_SCHEMES_SUPPORTED_LIST = makeUuid("2BB6"); @VisibleForTesting static final UUID UUID_BEARER_LIST_CURRENT_CALLS = makeUuid("2BB9"); - @VisibleForTesting static final UUID UUID_CONTENT_CONTROL_ID = makeUuid("2BBA"); + private static final UUID UUID_CONTENT_CONTROL_ID = makeUuid("2BBA"); @VisibleForTesting static final UUID UUID_STATUS_FLAGS = makeUuid("2BBB"); @VisibleForTesting static final UUID UUID_CALL_STATE = makeUuid("2BBD"); @VisibleForTesting static final UUID UUID_CALL_CONTROL_POINT = makeUuid("2BBE"); - - @VisibleForTesting - static final UUID UUID_CALL_CONTROL_POINT_OPTIONAL_OPCODES = makeUuid("2BBF"); - + private static final UUID UUID_CALL_CONTROL_POINT_OPTIONAL_OPCODES = makeUuid("2BBF"); @VisibleForTesting static final UUID UUID_TERMINATION_REASON = makeUuid("2BC0"); @VisibleForTesting static final UUID UUID_INCOMING_CALL = makeUuid("2BC1"); @VisibleForTesting static final UUID UUID_CALL_FRIENDLY_NAME = makeUuid("2BC2"); - @VisibleForTesting static final UUID UUID_CLIENT_CHARACTERISTIC_CONFIGURATION = makeUuid("2902"); @VisibleForTesting static final int STATUS_FLAG_INBAND_RINGTONE_ENABLED = 0x0001; @VisibleForTesting static final int STATUS_FLAG_SILENT_MODE_ENABLED = 0x0002; - @VisibleForTesting static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_LOCAL_HOLD = 0x0001; - @VisibleForTesting static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_JOIN = 0x0002; - - @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_ACCEPT = 0x00; - @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_TERMINATE = 0x01; - @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD = 0x02; - @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE = 0x03; - @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_ORIGINATE = 0x04; - @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_JOIN = 0x05; + private static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_LOCAL_HOLD = 0x0001; + private static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_JOIN = 0x0002; - @VisibleForTesting public static final int CALL_CONTROL_POINT_RESULT_SUCCESS = 0x00; + static final int CALL_CONTROL_POINT_OPCODE_ACCEPT = 0x00; + static final int CALL_CONTROL_POINT_OPCODE_TERMINATE = 0x01; + static final int CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD = 0x02; + static final int CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE = 0x03; + static final int CALL_CONTROL_POINT_OPCODE_ORIGINATE = 0x04; + static final int CALL_CONTROL_POINT_OPCODE_JOIN = 0x05; - @VisibleForTesting - public static final int CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED = 0x01; + static final int CALL_CONTROL_POINT_RESULT_SUCCESS = 0x00; + static final int CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED = 0x01; + static final int CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE = 0x02; + static final int CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX = 0x03; + static final int CALL_CONTROL_POINT_RESULT_STATE_MISMATCH = 0x04; + static final int CALL_CONTROL_POINT_RESULT_INVALID_OUTGOING_URI = 0x06; - @VisibleForTesting - public static final int CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE = 0x02; + private final Object mPendingGattOperationsLock = new Object(); + private final Map<BluetoothDevice, Integer> mStatusFlagValue = new HashMap<>(); - @VisibleForTesting public static final int CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX = 0x03; - @VisibleForTesting public static final int CALL_CONTROL_POINT_RESULT_STATE_MISMATCH = 0x04; - @VisibleForTesting public static final int CALL_CONTROL_POINT_RESULT_LACK_OF_RESOURCES = 0x05; + @GuardedBy("mPendingGattOperationsLock") + private final Map<BluetoothDevice, List<GattOpContext>> mPendingGattOperations = + new HashMap<>(); - @VisibleForTesting - public static final int CALL_CONTROL_POINT_RESULT_INVALID_OUTGOING_URI = 0x06; + private final Map<BluetoothDevice, HashMap<UUID, Short>> mCccDescriptorValues = new HashMap<>(); - private final Object mPendingGattOperationsLock = new Object(); - private final Context mContext; + private final AdapterService mAdapterService; + private final TbsService mTbsService; + private final Handler mHandler; + private final BluetoothGattServerProxy mBluetoothGattServer; private final GattCharacteristic mBearerProviderNameCharacteristic; private final GattCharacteristic mBearerUciCharacteristic; private final GattCharacteristic mBearerTechnologyCharacteristic; @@ -124,18 +122,10 @@ public class TbsGatt { private final GattCharacteristic mTerminationReasonCharacteristic; private final GattCharacteristic mIncomingCallCharacteristic; private final GattCharacteristic mCallFriendlyNameCharacteristic; - private boolean mSilentMode = false; - private Map<BluetoothDevice, Integer> mStatusFlagValue = new HashMap<>(); - - @GuardedBy("mPendingGattOperationsLock") - private Map<BluetoothDevice, List<GattOpContext>> mPendingGattOperations = new HashMap<>(); - private BluetoothGattServerProxy mBluetoothGattServer; - private Handler mHandler; private Callback mCallback; - private AdapterService mAdapterService; - private HashMap<BluetoothDevice, HashMap<UUID, Short>> mCccDescriptorValues; - private TbsService mTbsService; + + private boolean mSilentMode = false; private static final int LOG_NB_EVENTS = 200; private BluetoothEventLogger mEventLogger = null; @@ -242,12 +232,19 @@ public class TbsGatt { public byte[] mValue; } - TbsGatt(TbsService tbsService) { - mContext = tbsService; - mAdapterService = - Objects.requireNonNull( - AdapterService.getAdapterService(), - "AdapterService shouldn't be null when creating TbsGatt"); + TbsGatt(AdapterService adapterService, TbsService tbsService) { + this(adapterService, tbsService, new BluetoothGattServerProxy(adapterService)); + } + + @VisibleForTesting + TbsGatt( + AdapterService adapterService, + TbsService tbsService, + BluetoothGattServerProxy gattServerProxy) { + mTbsService = requireNonNull(tbsService); + mAdapterService = requireNonNull(adapterService); + mBluetoothGattServer = requireNonNull(gattServerProxy); + mHandler = new Handler(Looper.getMainLooper()); mBearerProviderNameCharacteristic = new GattCharacteristic( @@ -317,13 +314,6 @@ public class TbsGatt { | BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED); - mTbsService = tbsService; - mBluetoothGattServer = null; - } - - @VisibleForTesting - void setBluetoothGattServerForTesting(BluetoothGattServerProxy proxy) { - mBluetoothGattServer = proxy; } public boolean init( @@ -335,7 +325,6 @@ public class TbsGatt { String providerName, int technology, Callback callback) { - mCccDescriptorValues = new HashMap<>(); mBearerProviderNameCharacteristic.setValue(providerName); mBearerTechnologyCharacteristic.setValue(new byte[] {(byte) (technology & 0xFF)}); mBearerUciCharacteristic.setValue(uci); @@ -344,11 +333,6 @@ public class TbsGatt { setCallControlPointOptionalOpcodes(isLocalHoldOpcodeSupported, isJoinOpcodeSupported); mStatusFlagsCharacteristic.setValue(0, BluetoothGattCharacteristic.FORMAT_UINT16, 0); mCallback = callback; - mHandler = new Handler(Looper.getMainLooper()); - - if (mBluetoothGattServer == null) { - mBluetoothGattServer = new BluetoothGattServerProxy(mContext); - } if (!mBluetoothGattServer.open(mGattServerCallback)) { Log.e(TAG, " Could not open Gatt server"); @@ -381,22 +365,14 @@ public class TbsGatt { mEventLogger.add("Initialized"); mAdapterService.registerBluetoothStateCallback( - mContext.getMainExecutor(), mBluetoothStateChangeCallback); + mAdapterService.getMainExecutor(), mBluetoothStateChangeCallback); return true; } public void cleanup() { mAdapterService.unregisterBluetoothStateCallback(mBluetoothStateChangeCallback); - if (mBluetoothGattServer == null) { - return; - } mBluetoothGattServer.close(); - mBluetoothGattServer = null; - } - - public Context getContext() { - return mContext; } private void removeUuidFromMetadata(ParcelUuid charUuid, BluetoothDevice device) { @@ -506,19 +482,14 @@ public class TbsGatt { BluetoothDevice device, BluetoothGattCharacteristic characteristic, byte[] value) { if (getDeviceAuthorization(device) != BluetoothDevice.ACCESS_ALLOWED) return; if (value == null) return; - if (mBluetoothGattServer != null) { - mBluetoothGattServer.notifyCharacteristicChanged( - device, characteristic, false, value); - } + mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false, value); } private void notifyCharacteristicChanged( BluetoothDevice device, BluetoothGattCharacteristic characteristic) { if (getDeviceAuthorization(device) != BluetoothDevice.ACCESS_ALLOWED) return; - if (mBluetoothGattServer != null) { - mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false); - } + mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false); } public void notifyWithValue( diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java index 957e1e2b69..b49eee0a03 100644 --- a/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java +++ b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java @@ -17,6 +17,8 @@ package com.android.bluetooth.tbs; +import static java.util.Objects.requireNonNull; + import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothLeCall; @@ -50,8 +52,7 @@ import java.util.UUID; /** Container class to store TBS instances */ public class TbsGeneric { - - private static final String TAG = "TbsGeneric"; + private static final String TAG = TbsGeneric.class.getSimpleName(); private static final String UCI = "GTBS"; private static final String DEFAULT_PROVIDER_NAME = "none"; @@ -121,17 +122,20 @@ public class TbsGeneric { } } - private boolean mIsInitialized = false; - private TbsGatt mTbsGatt = null; - private List<Bearer> mBearerList = new ArrayList<>(); + private final List<Bearer> mBearerList = new ArrayList<>(); + private final Map<Integer, TbsCall> mCurrentCallsList = new TreeMap<>(); + private final Receiver mReceiver = new Receiver(); + private final ServiceFactory mFactory = new ServiceFactory(); + + private final TbsGatt mTbsGatt; + private final Context mContext; + + private boolean mIsInitialized; private int mLastIndexAssigned = TbsCall.INDEX_UNASSIGNED; - private Map<Integer, TbsCall> mCurrentCallsList = new TreeMap<>(); private Bearer mForegroundBearer = null; private int mLastRequestIdAssigned = 0; private List<String> mUriSchemes = new ArrayList<>(Arrays.asList("tel")); - private Receiver mReceiver = null; private int mStoredRingerMode = -1; - private final ServiceFactory mFactory = new ServiceFactory(); private LeAudioService mLeAudioService; private final class Receiver extends BroadcastReceiver { @@ -162,9 +166,9 @@ public class TbsGeneric { } ; - public synchronized boolean init(TbsGatt tbsGatt) { - Log.d(TAG, "init"); - mTbsGatt = tbsGatt; + TbsGeneric(Context ctx, TbsGatt tbsGatt) { + mTbsGatt = requireNonNull(tbsGatt); + mContext = requireNonNull(ctx); int ccid = ContentControlIdKeeper.acquireCcid( @@ -173,7 +177,7 @@ public class TbsGeneric { if (!isCcidValid(ccid)) { Log.e(TAG, " CCID is not valid"); cleanup(); - return false; + return; } if (!mTbsGatt.init( @@ -187,15 +191,10 @@ public class TbsGeneric { mTbsGattCallback)) { Log.e(TAG, " TbsGatt init failed"); cleanup(); - return false; + return; } - AudioManager audioManager = mTbsGatt.getContext().getSystemService(AudioManager.class); - if (audioManager == null) { - Log.w(TAG, " AudioManager is not available"); - cleanup(); - return false; - } + AudioManager audioManager = requireNonNull(mContext.getSystemService(AudioManager.class)); // read initial value of ringer mode mStoredRingerMode = audioManager.getRingerMode(); @@ -206,25 +205,20 @@ public class TbsGeneric { mTbsGatt.clearSilentModeFlag(); } - mReceiver = new Receiver(); IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); - mTbsGatt.getContext().registerReceiver(mReceiver, filter); + mContext.registerReceiver(mReceiver, filter); mIsInitialized = true; - return true; } public synchronized void cleanup() { Log.d(TAG, "cleanup"); - if (mTbsGatt != null) { - if (mReceiver != null) { - mTbsGatt.getContext().unregisterReceiver(mReceiver); - } - mTbsGatt.cleanup(); - mTbsGatt = null; + if (mIsInitialized) { + mContext.unregisterReceiver(mReceiver); } + mTbsGatt.cleanup(); mIsInitialized = false; } @@ -236,9 +230,7 @@ public class TbsGeneric { */ public synchronized void onDeviceAuthorizationSet(BluetoothDevice device) { // Notify TBS GATT service instance in case of pending operations - if (mTbsGatt != null) { - mTbsGatt.onDeviceAuthorizationSet(device); - } + mTbsGatt.onDeviceAuthorizationSet(device); } /** @@ -247,10 +239,6 @@ public class TbsGeneric { * @param device device for which inband ringtone has been set */ public synchronized void setInbandRingtoneSupport(BluetoothDevice device) { - if (mTbsGatt == null) { - Log.w(TAG, "setInbandRingtoneSupport, mTbsGatt is null"); - return; - } mTbsGatt.setInbandRingtoneFlag(device); } @@ -260,10 +248,6 @@ public class TbsGeneric { * @param device device for which inband ringtone has been cleared */ public synchronized void clearInbandRingtoneSupport(BluetoothDevice device) { - if (mTbsGatt == null) { - Log.w(TAG, "setInbandRingtoneSupport, mTbsGatt is null"); - return; - } mTbsGatt.clearInbandRingtoneFlag(device); } @@ -767,7 +751,7 @@ public class TbsGeneric { Log.i(TAG, "originate uri=" + uri); Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, Uri.parse(uri)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mTbsGatt.getContext().startActivity(intent); + mContext.startActivity(intent); mTbsGatt.setCallControlPointResult( device, TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE, @@ -844,6 +828,20 @@ public class TbsGeneric { return; } + if (shouldBlockTbsForBroadcastReceiver(device)) { + Log.w( + TAG, + "Blocking TBS operation for non-primary device in broadcast," + + " opcode = " + + callControlRequestOpcodeStr(opcode)); + mTbsGatt.setCallControlPointResult( + device, + opcode, + 0, + TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE); + return; + } + int result; switch (opcode) { @@ -1245,6 +1243,20 @@ public class TbsGeneric { return false; } + private boolean shouldBlockTbsForBroadcastReceiver(BluetoothDevice device) { + if (device == null) { + Log.w(TAG, "shouldBlockTbsForBroadcastReceiver: Ignore null device"); + return false; + } + if (!isLeAudioServiceAvailable()) { + Log.w(TAG, "shouldBlockTbsForBroadcastReceiver: LeAudioService is not available"); + return false; + } + + return mLeAudioService.getLocalBroadcastReceivers().contains(device) + && !mLeAudioService.isPrimaryDevice(device); + } + /** * Dump status of TBS service along with related objects * diff --git a/android/app/src/com/android/bluetooth/tbs/TbsService.java b/android/app/src/com/android/bluetooth/tbs/TbsService.java index dc3819eec1..263f765e53 100644 --- a/android/app/src/com/android/bluetooth/tbs/TbsService.java +++ b/android/app/src/com/android/bluetooth/tbs/TbsService.java @@ -20,6 +20,8 @@ package com.android.bluetooth.tbs; import static android.Manifest.permission.BLUETOOTH_CONNECT; import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static java.util.Objects.requireNonNull; + import android.annotation.RequiresPermission; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeCall; @@ -27,13 +29,13 @@ import android.bluetooth.BluetoothProfile; import android.bluetooth.IBluetoothLeCallControl; import android.bluetooth.IBluetoothLeCallControlCallback; import android.content.AttributionSource; -import android.content.Context; import android.os.ParcelUuid; import android.os.RemoteException; import android.sysprop.BluetoothProperties; import android.util.Log; import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.le_audio.LeAudioService; import com.android.internal.annotations.VisibleForTesting; @@ -44,16 +46,20 @@ import java.util.Map; import java.util.UUID; public class TbsService extends ProfileService { - - private static final String TAG = "TbsService"; + private static final String TAG = TbsService.class.getSimpleName(); private static TbsService sTbsService; + private final Map<BluetoothDevice, Integer> mDeviceAuthorizations = new HashMap<>(); + private final TbsGeneric mTbsGeneric; - private final TbsGeneric mTbsGeneric = new TbsGeneric(); + public TbsService(AdapterService adapterService) { + super(requireNonNull(adapterService)); - public TbsService(Context ctx) { - super(ctx); + // Mark service as started + setTbsService(this); + + mTbsGeneric = new TbsGeneric(adapterService, new TbsGatt(adapterService, this)); } public static boolean isEnabled() { @@ -66,19 +72,6 @@ public class TbsService extends ProfileService { } @Override - public void start() { - Log.d(TAG, "start()"); - if (sTbsService != null) { - throw new IllegalStateException("start() called twice"); - } - - // Mark service as started - setTbsService(this); - - mTbsGeneric.init(new TbsGatt(this)); - } - - @Override public void stop() { Log.d(TAG, "stop()"); if (sTbsService == null) { diff --git a/android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java b/android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java new file mode 100644 index 0000000000..4fd324dcf9 --- /dev/null +++ b/android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java @@ -0,0 +1,112 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.bluetooth.le_scan; + +import static com.android.bluetooth.le_scan.ScanController.DEFAULT_REPORT_DELAY_FLOOR; + +import android.provider.DeviceConfig; + +import com.android.bluetooth.Utils.TimeProvider; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Set; + +/** + * Throttler to reduce the number of times the Bluetooth process wakes up to check for pending batch + * scan results. The wake-up intervals are increased when no matching results are found and are + * longer when the screen is off. + */ +class BatchScanThrottler { + // Minimum batch trigger interval to check for batched results when the screen is off + @VisibleForTesting static final long SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS = 20000L; + // Adjusted minimum report delay for unfiltered batch scan clients + @VisibleForTesting static final long UNFILTERED_DELAY_FLOOR_MS = 20000L; + // Adjusted minimum report delay for unfiltered batch scan clients when the screen is off + @VisibleForTesting static final long UNFILTERED_SCREEN_OFF_DELAY_FLOOR_MS = 60000L; + // Backoff stages used as multipliers for the minimum delay floor (standard or screen-off) + @VisibleForTesting static final int[] BACKOFF_MULTIPLIERS = {1, 1, 2, 2, 4}; + // Start screen-off trigger interval throttling after the screen has been off for this period + // of time. This allows the screen-on intervals to be used for a short period of time after the + // screen has gone off, and avoids too much flipping between screen-off and screen-on backoffs + // when the screen is off for a short period of time + @VisibleForTesting static final long SCREEN_OFF_DELAY_MS = 60000L; + private final TimeProvider mTimeProvider; + private final long mDelayFloor; + private final long mScreenOffDelayFloor; + private int mBackoffStage = 0; + private long mScreenOffTriggerTime = 0L; + private boolean mScreenOffThrottling = false; + + BatchScanThrottler(TimeProvider timeProvider, boolean screenOn) { + mTimeProvider = timeProvider; + mDelayFloor = + DeviceConfig.getLong( + DeviceConfig.NAMESPACE_BLUETOOTH, + "report_delay", + DEFAULT_REPORT_DELAY_FLOOR); + mScreenOffDelayFloor = Math.max(mDelayFloor, SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS); + onScreenOn(screenOn); + } + + void resetBackoff() { + mBackoffStage = 0; + } + + void onScreenOn(boolean screenOn) { + if (screenOn) { + mScreenOffTriggerTime = 0L; + mScreenOffThrottling = false; + resetBackoff(); + } else { + // Screen-off intervals to be used after the trigger time + mScreenOffTriggerTime = mTimeProvider.elapsedRealtime() + SCREEN_OFF_DELAY_MS; + } + } + + long getBatchTriggerIntervalMillis(Set<ScanClient> batchClients) { + // Check if we're past the screen-off time and should be using screen-off backoff values + if (!mScreenOffThrottling + && mScreenOffTriggerTime != 0 + && mTimeProvider.elapsedRealtime() >= mScreenOffTriggerTime) { + mScreenOffThrottling = true; + resetBackoff(); + } + long unfilteredFloor = + mScreenOffThrottling + ? UNFILTERED_SCREEN_OFF_DELAY_FLOOR_MS + : UNFILTERED_DELAY_FLOOR_MS; + long intervalMillis = Long.MAX_VALUE; + for (ScanClient client : batchClients) { + if (client.settings.getReportDelayMillis() > 0) { + long clientIntervalMillis = client.settings.getReportDelayMillis(); + if ((client.filters == null || client.filters.isEmpty()) + && clientIntervalMillis < unfilteredFloor) { + clientIntervalMillis = unfilteredFloor; + } + intervalMillis = Math.min(intervalMillis, clientIntervalMillis); + } + } + int backoffIndex = + mBackoffStage >= BACKOFF_MULTIPLIERS.length + ? BACKOFF_MULTIPLIERS.length - 1 + : mBackoffStage++; + return Math.max( + intervalMillis, + (mScreenOffThrottling ? mScreenOffDelayFloor : mDelayFloor) + * BACKOFF_MULTIPLIERS[backoffIndex]); + } +} diff --git a/android/app/tests/unit/Android.bp b/android/app/tests/unit/Android.bp index 346c57fffa..eae8e4a89f 100644 --- a/android/app/tests/unit/Android.bp +++ b/android/app/tests/unit/Android.bp @@ -22,6 +22,7 @@ java_defaults { static_libs: [ "PlatformProperties", + "TestParameterInjector", "android.media.audio-aconfig-exported-java", "androidx.media_media", "androidx.room_room-migration", diff --git a/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java b/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java index 877df75ef2..78d1e66db1 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java +++ b/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java @@ -41,7 +41,6 @@ import androidx.test.uiautomator.UiDevice; import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService; import com.android.bluetooth.btservice.AdapterService; -import org.junit.Assert; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -112,11 +111,11 @@ public class TestUtils { * TestUtils#setAdapterService(AdapterService)} */ public static void clearAdapterService(AdapterService adapterService) { - Assert.assertSame( - "AdapterService.getAdapterService() must return the same object as the" - + " supplied adapterService in this method", - adapterService, - AdapterService.getAdapterService()); + assertWithMessage( + "AdapterService.getAdapterService() must return the same object as the" + + " supplied adapterService in this method") + .that(adapterService) + .isSameInstanceAs(AdapterService.getAdapterService()); assertThat(adapterService).isNotNull(); AdapterService.clearAdapterService(adapterService); } @@ -157,10 +156,7 @@ public class TestUtils { return context.getPackageManager() .getResourcesForApplication("com.android.bluetooth.tests"); } catch (PackageManager.NameNotFoundException e) { - assertWithMessage( - "Setup Failure: Unable to get test application resources" - + e.toString()) - .fail(); + assertWithMessage("Unable to get test application resources: " + e.toString()).fail(); return null; } } @@ -178,7 +174,7 @@ public class TestUtils { assertThat(intent).isNotNull(); return intent; } catch (InterruptedException e) { - Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); + assertWithMessage("Cannot obtain an Intent from the queue: " + e.toString()).fail(); } return null; } @@ -194,7 +190,7 @@ public class TestUtils { Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); assertThat(intent).isNull(); } catch (InterruptedException e) { - Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); + assertWithMessage("Cannot obtain an Intent from the queue: " + e.toString()).fail(); } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java b/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java index 156151e9b0..a481cef2e0 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java @@ -17,6 +17,7 @@ package com.android.bluetooth.a2dp; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.*; @@ -33,7 +34,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.R; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -787,10 +787,12 @@ public class A2dpCodecConfigTest { codecConfig.getCodecSpecific3(), codecConfig.getCodecSpecific4()); } - Assert.fail( - "getDefaultCodecConfigByType: No such codecType=" - + codecType - + " in sDefaultCodecConfigs"); + assertWithMessage( + "Default codec (" + + Arrays.toString(sDefaultCodecConfigs) + + ") does not contains " + + codecType) + .fail(); return null; } @@ -810,10 +812,12 @@ public class A2dpCodecConfigTest { codecCapabilities.getCodecSpecific3(), codecCapabilities.getCodecSpecific4()); } - Assert.fail( - "getCodecCapabilitiesByType: No such codecType=" - + codecType - + " in sCodecCapabilities"); + assertWithMessage( + "Codec capabilities (" + + Arrays.toString(sCodecCapabilities) + + ") does not contains " + + codecType) + .fail(); return null; } diff --git a/android/app/tests/unit/src/com/android/bluetooth/audio_util/BrowserPlayerWrapperTest.java b/android/app/tests/unit/src/com/android/bluetooth/audio_util/BrowserPlayerWrapperTest.java index cd9ad4dd9a..b3d61e33af 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/audio_util/BrowserPlayerWrapperTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/audio_util/BrowserPlayerWrapperTest.java @@ -40,7 +40,6 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import org.junit.After; @@ -75,7 +74,6 @@ public class BrowserPlayerWrapperTest { private HandlerThread mThread; @Mock Context mMockContext; - @Mock Resources mMockResources; private Context mTargetContext; private Resources mTestResources; private MockContentResolver mTestContentResolver; @@ -116,8 +114,7 @@ public class BrowserPlayerWrapperTest { }); when(mMockContext.getContentResolver()).thenReturn(mTestContentResolver); - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(true); - when(mMockContext.getResources()).thenReturn(mMockResources); + Util.sUriImagesSupport = true; // Set up Looper thread for the timeout handler mThread = new HandlerThread("MediaPlayerWrapperTestThread"); @@ -138,6 +135,7 @@ public class BrowserPlayerWrapperTest { mTestBitmap = null; mTestResources = null; mTargetContext = null; + Util.sUriImagesSupport = false; } private Bitmap loadImage(int resId) { diff --git a/android/app/tests/unit/src/com/android/bluetooth/audio_util/ImageTest.java b/android/app/tests/unit/src/com/android/bluetooth/audio_util/ImageTest.java index 60ca5a5aef..6c6e6e78fc 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/audio_util/ImageTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/audio_util/ImageTest.java @@ -36,7 +36,6 @@ import android.test.mock.MockContentResolver; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; -import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import org.junit.After; @@ -57,7 +56,6 @@ public class ImageTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); private @Mock Context mMockContext; - private @Mock Resources mMockResources; private Resources mTestResources; private MockContentResolver mTestContentResolver; @@ -108,8 +106,7 @@ public class ImageTest { }); when(mMockContext.getContentResolver()).thenReturn(mTestContentResolver); - when(mMockContext.getResources()).thenReturn(mMockResources); - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(true); + Util.sUriImagesSupport = true; } @After @@ -119,6 +116,7 @@ public class ImageTest { mTestResources = null; mTargetContext = null; mMockContext = null; + Util.sUriImagesSupport = false; } private Bitmap loadImage(int resId) { @@ -287,7 +285,7 @@ public class ImageTest { */ @Test public void testCreateImageFromMediaMetadataWithArtUriDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; MediaMetadata metadata = getMediaMetadataWithUri(MediaMetadata.METADATA_KEY_ART_URI, IMAGE_STRING_1); Image artwork = new Image(mMockContext, metadata); @@ -302,7 +300,7 @@ public class ImageTest { */ @Test public void testCreateImageFromMediaMetadataWithAlbumArtUriDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; MediaMetadata metadata = getMediaMetadataWithUri(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, IMAGE_STRING_1); Image artwork = new Image(mMockContext, metadata); @@ -317,7 +315,7 @@ public class ImageTest { */ @Test public void testCreateImageFromMediaMetadataWithDisplayIconUriDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; MediaMetadata metadata = getMediaMetadataWithUri( MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, IMAGE_STRING_1); @@ -459,7 +457,7 @@ public class ImageTest { */ @Test public void testCreateImageFromBundleWithArtUriDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; Bundle bundle = getBundleWithUri(MediaMetadata.METADATA_KEY_ART_URI, IMAGE_STRING_1); Image artwork = new Image(mMockContext, bundle); assertThat(artwork.getImage()).isNull(); @@ -473,7 +471,7 @@ public class ImageTest { */ @Test public void testCreateImageFromBundleWithAlbumArtUriDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; Bundle bundle = getBundleWithUri(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, IMAGE_STRING_1); Image artwork = new Image(mMockContext, bundle); assertThat(artwork.getImage()).isNull(); @@ -487,7 +485,7 @@ public class ImageTest { */ @Test public void testCreateImageFromBundleWithDisplayIconUriDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; Bundle bundle = getBundleWithUri(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, IMAGE_STRING_1); Image artwork = new Image(mMockContext, bundle); diff --git a/android/app/tests/unit/src/com/android/bluetooth/audio_util/MediaPlayerWrapperTest.java b/android/app/tests/unit/src/com/android/bluetooth/audio_util/MediaPlayerWrapperTest.java index 2b323d9810..794a1200b3 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/audio_util/MediaPlayerWrapperTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/audio_util/MediaPlayerWrapperTest.java @@ -36,9 +36,9 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; -import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -75,7 +75,6 @@ public class MediaPlayerWrapperTest { @Mock MediaController mMockController; @Mock MediaPlayerWrapper.Callback mTestCbs; @Mock Context mMockContext; - @Mock Resources mMockResources; List<MediaSession.QueueItem> getQueueFromDescriptions( List<MediaDescription.Builder> descriptions) { @@ -98,8 +97,7 @@ public class MediaPlayerWrapperTest { InstrumentationRegistry.getInstrumentation().getTargetContext()); mTestBitmap = loadImage(com.android.bluetooth.tests.R.raw.image_200_200); - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(true); - when(mMockContext.getResources()).thenReturn(mMockResources); + Util.sUriImagesSupport = true; // Set failure handler to capture Log.wtf messages Log.setWtfHandler(mFailHandler); @@ -160,6 +158,14 @@ public class MediaPlayerWrapperTest { MediaPlayerWrapper.sTesting = true; } + @After + public void tearDown() { + if (mThread != null) { + mThread.quitSafely(); + } + Util.sUriImagesSupport = false; + } + private Bitmap loadImage(int resId) { InputStream imageInputStream = mTestResources.openRawResource(resId); return BitmapFactory.decodeStream(imageInputStream); diff --git a/android/app/tests/unit/src/com/android/bluetooth/audio_util/MetadataTest.java b/android/app/tests/unit/src/com/android/bluetooth/audio_util/MetadataTest.java index 5ba6e0f4e5..4cc12f763d 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/audio_util/MetadataTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/audio_util/MetadataTest.java @@ -38,7 +38,6 @@ import android.test.mock.MockContentResolver; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; -import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import org.junit.After; @@ -59,7 +58,6 @@ public class MetadataTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); private @Mock Context mMockContext; - private @Mock Resources mMockResources; private Resources mTestResources; private MockContentResolver mTestContentResolver; @@ -111,8 +109,7 @@ public class MetadataTest { }); when(mMockContext.getContentResolver()).thenReturn(mTestContentResolver); - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(true); - when(mMockContext.getResources()).thenReturn(mMockResources); + Util.sUriImagesSupport = true; mSongImage = new Image(mMockContext, mTestBitmap); } @@ -125,6 +122,7 @@ public class MetadataTest { mTestResources = null; mTargetContext = null; mMockContext = null; + Util.sUriImagesSupport = false; } private Bitmap loadImage(int resId) { @@ -427,7 +425,7 @@ public class MetadataTest { */ @Test public void testBuildMetadataFromMediaMetadataWithUriAndUrisDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; MediaMetadata m = getMediaMetadataWithUri(MediaMetadata.METADATA_KEY_ART_URI, IMAGE_URI_1); Metadata metadata = new Metadata.Builder().useContext(mMockContext).fromMediaMetadata(m).build(); @@ -737,7 +735,7 @@ public class MetadataTest { */ @Test public void testBuildMetadataFromBundleWithUriAndUrisDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; Bundle bundle = getBundleWithUri(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, IMAGE_URI_1); Metadata metadata = new Metadata.Builder().useContext(mMockContext).fromBundle(bundle).build(); @@ -852,7 +850,7 @@ public class MetadataTest { */ @Test public void testBuildMetadataFromMediaItemWithIconUriAndUrisDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; MediaDescription description = getMediaDescription(null, IMAGE_URI_1, null); MediaItem item = getMediaItem(description); Metadata metadata = @@ -980,7 +978,7 @@ public class MetadataTest { */ @Test public void testBuildMetadataFromQueueItemWithIconUriandUrisDisabled() { - when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false); + Util.sUriImagesSupport = false; MediaDescription description = getMediaDescription(null, IMAGE_URI_1, null); QueueItem queueItem = getQueueItem(description); Metadata metadata = diff --git a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java index 2749dd7e09..c1c7734f2e 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java @@ -4472,6 +4472,8 @@ public class BassClientServiceTest { // Verify getSyncedBroadcastSinks returns empty device list if no broadcst ID assertThat(mBassClientService.getSyncedBroadcastSinks().isEmpty()).isTrue(); + assertThat(mBassClientService.getSyncedBroadcastSinks(TEST_BROADCAST_ID).isEmpty()) + .isTrue(); // Update receiver state with broadcast ID injectRemoteSourceStateChanged(meta, true, false); @@ -4487,6 +4489,16 @@ public class BassClientServiceTest { assertThat(mBassClientService.getSyncedBroadcastSinks().isEmpty()).isTrue(); } + activeSinks.clear(); + // Verify getSyncedBroadcastSinks by broadcast id + activeSinks = mBassClientService.getSyncedBroadcastSinks(TEST_BROADCAST_ID); + if (Flags.leaudioBigDependsOnAudioState()) { + // Verify getSyncedBroadcastSinks returns correct device list if no BIS synced + assertThat(activeSinks.size()).isEqualTo(2); + assertThat(activeSinks.contains(mCurrentDevice)).isTrue(); + assertThat(activeSinks.contains(mCurrentDevice1)).isTrue(); + } + // Update receiver state with BIS sync injectRemoteSourceStateChanged(meta, true, true); @@ -6348,7 +6360,8 @@ public class BassClientServiceTest { @Test @EnableFlags({ Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER, - Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE + Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE, + Flags.FLAG_LEAUDIO_MONITOR_UNICAST_SOURCE_WHEN_MANAGED_BY_BROADCAST_DELEGATOR }) public void sinkUnintentional_handleUnicastSourceStreamStatusChange_withoutScanning() { sinkUnintentionalWithoutScanning(); @@ -6357,7 +6370,6 @@ public class BassClientServiceTest { mBassClientService.handleUnicastSourceStreamStatusChange( 0 /* STATUS_LOCAL_STREAM_REQUESTED */); verifyStopBigMonitoringWithUnsync(); - verifyRemoveMessageAndInjectSourceRemoval(); checkNoResumeSynchronizationByBig(); /* Unicast finished streaming */ @@ -6370,7 +6382,8 @@ public class BassClientServiceTest { @Test @EnableFlags({ Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER, - Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE + Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE, + Flags.FLAG_LEAUDIO_MONITOR_UNICAST_SOURCE_WHEN_MANAGED_BY_BROADCAST_DELEGATOR }) public void sinkUnintentional_handleUnicastSourceStreamStatusChange_duringScanning() { sinkUnintentionalDuringScanning(); @@ -6379,7 +6392,6 @@ public class BassClientServiceTest { mBassClientService.handleUnicastSourceStreamStatusChange( 0 /* STATUS_LOCAL_STREAM_REQUESTED */); verifyStopBigMonitoringWithoutUnsync(); - verifyRemoveMessageAndInjectSourceRemoval(); checkNoResumeSynchronizationByBig(); /* Unicast finished streaming */ @@ -6642,6 +6654,44 @@ public class BassClientServiceTest { @Test @EnableFlags({ Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER, + Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE, + Flags.FLAG_LEAUDIO_BROADCAST_ASSISTANT_PERIPHERAL_ENTRUSTMENT, + Flags.FLAG_LEAUDIO_MONITOR_UNICAST_SOURCE_WHEN_MANAGED_BY_BROADCAST_DELEGATOR + }) + public void hostIntentional_handleUnicastSourceStreamStatusChange_beforeResumeCompleted() { + prepareSynchronizedPairAndStopSearching(); + + /* Unicast would like to stream */ + mBassClientService.handleUnicastSourceStreamStatusChange( + 0 /* STATUS_LOCAL_STREAM_REQUESTED */); + checkNoSinkPause(); + + /* Unicast finished streaming */ + mBassClientService.handleUnicastSourceStreamStatusChange( + 2 /* STATUS_LOCAL_STREAM_SUSPENDED */); + mInOrderMethodProxy + .verify(mMethodProxy) + .periodicAdvertisingManagerRegisterSync( + any(), any(), anyInt(), anyInt(), any(), any()); + + /* Unicast would like to stream again before previous resume was complete*/ + mBassClientService.handleUnicastSourceStreamStatusChange( + 0 /* STATUS_LOCAL_STREAM_REQUESTED */); + + /* Unicast finished streaming */ + mBassClientService.handleUnicastSourceStreamStatusChange( + 2 /* STATUS_LOCAL_STREAM_SUSPENDED */); + mInOrderMethodProxy + .verify(mMethodProxy) + .periodicAdvertisingManagerRegisterSync( + any(), any(), anyInt(), anyInt(), any(), any()); + onSyncEstablished(mSourceDevice, TEST_SYNC_HANDLE); // In case of add source to inactive + verifyAllGroupMembersGettingUpdateOrAddSource(createBroadcastMetadata(TEST_BROADCAST_ID)); + } + + @Test + @EnableFlags({ + Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER, Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE }) public void hostIntentional_handleUnicastSourceStreamStatusChangeNoContext_withoutScanning() { diff --git a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java index f3854445e2..e6b2d5f1d6 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java @@ -48,6 +48,7 @@ import static com.android.bluetooth.bass_client.BassClientStateMachine.UPDATE_BC import static com.android.bluetooth.bass_client.BassConstants.CLIENT_CHARACTERISTIC_CONFIG; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -111,7 +112,6 @@ import com.google.common.primitives.Bytes; import org.hamcrest.Matcher; import org.hamcrest.core.AllOf; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -210,7 +210,7 @@ public class BassClientStateMachineTest { || type == BassClientStateMachine.ConnectedProcessing.class) { return BluetoothProfile.STATE_CONNECTED; } else { - Assert.fail("Invalid class type given: " + type); + assertWithMessage("Invalid class type given: " + type).fail(); return 0; } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java index 44956086f3..f3dab9d9be 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java @@ -81,6 +81,7 @@ import com.android.bluetooth.flags.Flags; import com.android.bluetooth.gatt.AdvertiseManagerNativeInterface; import com.android.bluetooth.gatt.DistanceMeasurementNativeInterface; import com.android.bluetooth.gatt.GattNativeInterface; +import com.android.bluetooth.le_audio.LeAudioService; import com.android.bluetooth.le_scan.PeriodicScanNativeInterface; import com.android.bluetooth.le_scan.ScanNativeInterface; import com.android.bluetooth.sdp.SdpManagerNativeInterface; @@ -93,6 +94,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -144,6 +146,7 @@ public class AdapterServiceTest { private @Mock Context mMockContext; private @Mock ApplicationInfo mMockApplicationInfo; + private @Mock LeAudioService mMockLeAudioService; private @Mock Resources mMockResources; private @Mock ProfileService mMockGattService; private @Mock ProfileService mMockService; @@ -185,6 +188,10 @@ public class AdapterServiceTest { private int mForegroundUserId; private TestLooper mLooper; + private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter(); + private final BluetoothDevice mDevice = TestUtils.getTestDevice(mAdapter, 0); + private final BluetoothDevice mDeviceTwo = TestUtils.getTestDevice(mAdapter, 2); + static void configureEnabledProfiles() { Log.e(TAG, "configureEnabledProfiles"); @@ -224,6 +231,12 @@ public class AdapterServiceTest { doReturn(mJniCallbacks).when(mNativeInterface).getCallbacks(); + doReturn(true).when(mMockLeAudioService).isAvailable(); + LeAudioService.setLeAudioService(mMockLeAudioService); + doReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED) + .when(mMockLeAudioService) + .getConnectionPolicy(any()); + AdapterNativeInterface.setInstance(mNativeInterface); BluetoothKeystoreNativeInterface.setInstance(mKeystoreNativeInterface); BluetoothQualityReportNativeInterface.setInstance(mQualityNativeInterface); @@ -351,6 +364,7 @@ public class AdapterServiceTest { // Restores the foregroundUserId to the ID prior to the test setup Utils.setForegroundUserId(mForegroundUserId); + LeAudioService.setLeAudioService(null); mAdapterService.cleanup(); mAdapterService.unregisterRemoteCallback(mIBluetoothCallback); AdapterNativeInterface.setInstance(null); @@ -1121,4 +1135,333 @@ public class AdapterServiceTest { } assertThat(mLooper.nextMessage()).isNull(); } + + InOrder prepareLeAudioWithConnectedDevices( + List<BluetoothDevice> devices, + int groupId, + boolean returnOnSetAutoActiveModeState, + int returnOnGetConnectionStateLeAudio, + int returnOnGetConnectionStateAdapter) { + doEnable(false); + + doReturn(groupId).when(mMockLeAudioService).getGroupId(any()); + + doReturn(returnOnGetConnectionStateLeAudio) + .when(mMockLeAudioService) + .getConnectionState(any()); + doReturn(returnOnGetConnectionStateAdapter) + .when(mNativeInterface) + .getConnectionState(any()); + + doReturn(returnOnSetAutoActiveModeState) + .when(mMockLeAudioService) + .setAutoActiveModeState(groupId, false); + doReturn(devices).when(mMockLeAudioService).getGroupDevices(groupId); + + return inOrder(mMockLeAudioService); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE) + public void testGattConnectionToLeAudioDevice_whenDeviceIsNotConnected_success() { + int groupId = 1; + int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED; + int getConnectionState_AdapterService = + BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE + | BluetoothDevice.CONNECTION_STATE_CONNECTED; + InOrder order = + prepareLeAudioWithConnectedDevices( + List.of(mDevice), + groupId, + true, + getConnectionState_LeAudioService, + getConnectionState_AdapterService); + + mAdapterService.notifyDirectLeGattClientConnect(1, mDevice); + + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE) + public void testGattConnectionToLeAudioDevice_whenDeviceIsConnected_ignore() { + int groupId = 1; + int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED; + int getConnectionState_AdapterService = + BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE + | BluetoothDevice.CONNECTION_STATE_CONNECTED; + InOrder order = + prepareLeAudioWithConnectedDevices( + List.of(mDevice), + groupId, + false, + getConnectionState_LeAudioService, + getConnectionState_AdapterService); + + mAdapterService.notifyDirectLeGattClientConnect(1, mDevice); + + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE) + public void testGattConnectionToLeAudioDevice_whenLeAudioIsNotAllowed_ignore() { + int groupId = 1; + int getConnectionState_LeAudioService = BluetoothProfile.STATE_DISCONNECTED; + int getConnectionState_AdapterService = + BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE + | BluetoothDevice.CONNECTION_STATE_CONNECTED; + InOrder order = + prepareLeAudioWithConnectedDevices( + List.of(mDevice), + groupId, + false, + getConnectionState_LeAudioService, + getConnectionState_AdapterService); + + doReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) + .when(mMockLeAudioService) + .getConnectionPolicy(any()); + mAdapterService.notifyDirectLeGattClientConnect(1, mDevice); + + order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE) + public void testGattConnectionToLeAudioDevice_failedToConnect() { + int groupId = 1; + int clientIf = 1; + + int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED; + int getConnectionState_AdapterService = + BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE + | BluetoothDevice.CONNECTION_STATE_CONNECTED; + InOrder order = + prepareLeAudioWithConnectedDevices( + List.of(mDevice), + groupId, + true, + getConnectionState_LeAudioService, + getConnectionState_AdapterService); + + mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice); + + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + + mAdapterService.notifyGattClientConnectFailed(clientIf, mDevice); + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE) + public void testGattConnectionToLeAudioDevice_triggerDisconnected() { + int groupId = 1; + int clientIf = 1; + + int getConnectionState_LeAudioService = BluetoothProfile.STATE_DISCONNECTED; + int getConnectionState_AdapterService = BluetoothDevice.CONNECTION_STATE_DISCONNECTED; + InOrder order = + prepareLeAudioWithConnectedDevices( + List.of(mDevice), + groupId, + true, + getConnectionState_LeAudioService, + getConnectionState_AdapterService); + InOrder orderNative = inOrder(mNativeInterface); + + mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice); + + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + + mAdapterService.notifyGattClientDisconnect(clientIf, mDevice); + orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt()); + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE) + public void testGattConnectionToLeAudioDevice_triggerDisconnecting() { + int groupId = 1; + int clientIf = 1; + int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED; + int getConnectionState_AdapterService = + BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE + | BluetoothDevice.CONNECTION_STATE_CONNECTED; + InOrder order = + prepareLeAudioWithConnectedDevices( + List.of(mDevice), + groupId, + true, + getConnectionState_LeAudioService, + getConnectionState_AdapterService); + + InOrder orderNative = inOrder(mNativeInterface); + + mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice); + + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + + mAdapterService.notifyGattClientDisconnect(clientIf, mDevice); + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true); + orderNative.verify(mNativeInterface).disconnectAcl(any(), eq(BluetoothDevice.TRANSPORT_LE)); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE) + public void testGattConnectionToLeAudioDevice_connectingMultipleClients() { + int groupId = 1; + int clientIf = 1; + int clientIfTwo = 2; + + int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED; + int getConnectionState_AdapterService = + BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE + | BluetoothDevice.CONNECTION_STATE_CONNECTED; + InOrder order = + prepareLeAudioWithConnectedDevices( + List.of(mDevice), + groupId, + true, + getConnectionState_LeAudioService, + getConnectionState_AdapterService); + + InOrder orderNative = inOrder(mNativeInterface); + + // Connect first client to device + mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice); + + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + + // Connect second client to device + mAdapterService.notifyDirectLeGattClientConnect(clientIfTwo, mDevice); + + order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(2); + + // Disconnect first client to device + mAdapterService.notifyGattClientDisconnect(clientIf, mDevice); + order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, true); + orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt()); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + + // Disconnect second client to device + mAdapterService.notifyGattClientDisconnect(clientIfTwo, mDevice); + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true); + orderNative + .verify(mNativeInterface, times(1)) + .disconnectAcl(any(), eq(BluetoothDevice.TRANSPORT_LE)); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE) + public void testGattConnectionToLeAudioDevice_connectingMultipleDevicesInSameGroup() { + int groupId = 1; + int clientIf = 1; + int clientIfTwo = 2; + + int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED; + int getConnectionState_AdapterService = + BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE + | BluetoothDevice.CONNECTION_STATE_CONNECTED; + InOrder order = + prepareLeAudioWithConnectedDevices( + List.of(mDevice, mDeviceTwo), + groupId, + true, + getConnectionState_LeAudioService, + getConnectionState_AdapterService); + + InOrder orderNative = inOrder(mNativeInterface); + + // Connecting device one + when(mMockLeAudioService.setAutoActiveModeState(groupId, false)).thenReturn(true); + mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice); + + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + + // Connecting device two + mAdapterService.notifyDirectLeGattClientConnect(clientIfTwo, mDeviceTwo); + + order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(2); + + // Disconnect first device + mAdapterService.notifyGattClientDisconnect(clientIf, mDevice); + order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, true); + orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt()); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + + // Disconnect second device + mAdapterService.notifyGattClientDisconnect(clientIfTwo, mDeviceTwo); + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true); + orderNative + .verify(mNativeInterface, times(2)) + .disconnectAcl(any(), eq(BluetoothDevice.TRANSPORT_LE)); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE) + public void testGattConnectionToLeAudioDevice_remoteSwitchesToActiveBeforeDisconnect() { + int groupId = 1; + int clientIf = 1; + int clientIfTwo = 2; + + int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED; + int getConnectionState_AdapterService = + BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE + | BluetoothDevice.CONNECTION_STATE_CONNECTED; + InOrder order = + prepareLeAudioWithConnectedDevices( + List.of(mDevice, mDeviceTwo), + groupId, + true, + getConnectionState_LeAudioService, + getConnectionState_AdapterService); + + InOrder orderNative = inOrder(mNativeInterface); + + // Connecting device one + when(mMockLeAudioService.setAutoActiveModeState(groupId, false)).thenReturn(true); + mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice); + + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + + // Connecting device two + mAdapterService.notifyDirectLeGattClientConnect(clientIfTwo, mDeviceTwo); + + order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, false); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(2); + + // Remote switches to Active + when(mMockLeAudioService.isAutoActiveModeEnabled(groupId)).thenReturn(true); + + // Disconnect first device + mAdapterService.notifyGattClientDisconnect(clientIf, mDevice); + order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, true); + orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt()); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1); + + // Disconnect second device + mAdapterService.notifyGattClientDisconnect(clientIfTwo, mDeviceTwo); + + // Verify devices will not be disconnected + order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true); + orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt()); + assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty(); + } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java index cc40c6af72..e274ac6eed 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java @@ -218,7 +218,7 @@ public class BondStateMachineTest { RemoteDevices.DeviceProperties testDeviceProperties = mRemoteDevices.addDeviceProperties(TEST_BT_ADDR_BYTES); - testDeviceProperties.mUuids = TEST_UUIDS; + testDeviceProperties.mUuidsBrEdr = TEST_UUIDS; BluetoothDevice testDevice = testDeviceProperties.getDevice(); assertThat(testDevice).isNotNull(); @@ -228,7 +228,7 @@ public class BondStateMachineTest { bondingMsg.arg2 = AbstractionLayer.BT_STATUS_RMT_DEV_DOWN; mBondStateMachine.sendMessage(bondingMsg); - pendingDeviceProperties.mUuids = TEST_UUIDS; + pendingDeviceProperties.mUuidsBrEdr = TEST_UUIDS; Message uuidUpdateMsg = mBondStateMachine.obtainMessage(BondStateMachine.UUID_UPDATE); uuidUpdateMsg.obj = pendingDevice; @@ -634,7 +634,7 @@ public class BondStateMachineTest { } if (uuids != null) { // Add dummy UUID for the device. - mDeviceProperties.mUuids = TEST_UUIDS; + mDeviceProperties.mUuidsBrEdr = TEST_UUIDS; } testSendIntentCase( oldState, diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java index b2c68e4cfa..2753ee7b17 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java @@ -18,6 +18,7 @@ package com.android.bluetooth.btservice.storage; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; @@ -788,16 +789,16 @@ public final class DatabaseManagerTest { preferences.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX, BluetoothProfile.LE_AUDIO); // TEST 1: If input is invalid, throws the right Exception - Assert.assertThrows( + assertThrows( NullPointerException.class, () -> mDatabaseManager.setPreferredAudioProfiles(null, preferences)); - Assert.assertThrows( + assertThrows( NullPointerException.class, () -> mDatabaseManager.setPreferredAudioProfiles(new ArrayList<>(), null)); - Assert.assertThrows( + assertThrows( IllegalArgumentException.class, () -> mDatabaseManager.setPreferredAudioProfiles(new ArrayList<>(), preferences)); - Assert.assertThrows( + assertThrows( IllegalArgumentException.class, () -> mDatabaseManager.getPreferredAudioProfiles(null)); @@ -1748,7 +1749,7 @@ public final class DatabaseManagerTest { // Check whether the value is saved in database restartDatabaseManagerHelper(); - Assert.assertArrayEquals(value, mDatabaseManager.getCustomMeta(mTestDevice, key)); + assertThat(mDatabaseManager.getCustomMeta(mTestDevice, key)).isEqualTo(value); mDatabaseManager.factoryReset(); mDatabaseManager.mMetadataCache.clear(); diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java index 3a0abb2870..1eca1e2483 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java @@ -16,6 +16,8 @@ package com.android.bluetooth.gatt; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -54,6 +56,13 @@ public class AdvertiseBinderTest { @Before public void setUp() { + doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mAdvertiseManager) + .doOnAdvertiseThread(any()); mBinder = new AdvertiseBinder(mAdapterService, mAdvertiseManager); } diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseManagerTest.java index 6621a43331..4cd5423ce9 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseManagerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseManagerTest.java @@ -27,15 +27,16 @@ import android.bluetooth.le.AdvertisingSetParameters; import android.bluetooth.le.IAdvertisingSetCallback; import android.bluetooth.le.PeriodicAdvertisingParameters; import android.os.IBinder; +import android.os.test.TestLooper; +import android.platform.test.flag.junit.FlagsParameterization; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; -import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; +import com.android.bluetooth.flags.Flags; -import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -44,17 +45,21 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +import java.util.List; + /** Test cases for {@link AdvertiseManager}. */ @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) public class AdvertiseManagerTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule; @Mock private AdapterService mAdapterService; - @Mock private GattService mService; - @Mock private AdvertiserMap mAdvertiserMap; @Mock private AdvertiseManagerNativeInterface mNativeInterface; @@ -66,10 +71,23 @@ public class AdvertiseManagerTest { private AdvertiseManager mAdvertiseManager; private int mAdvertiserId; + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return FlagsParameterization.allCombinationsOf(Flags.FLAG_ADVERTISE_THREAD); + } + + public AdvertiseManagerTest(FlagsParameterization flags) { + mSetFlagsRule = new SetFlagsRule(flags); + } + @Before public void setUp() throws Exception { - TestUtils.setAdapterService(mAdapterService); - mAdvertiseManager = new AdvertiseManager(mService, mNativeInterface, mAdvertiserMap); + mAdvertiseManager = + new AdvertiseManager( + mAdapterService, + new TestLooper().getLooper(), + mNativeInterface, + mAdvertiserMap); AdvertisingSetParameters parameters = new AdvertisingSetParameters.Builder().build(); AdvertiseData advertiseData = new AdvertiseData.Builder().build(); @@ -95,12 +113,7 @@ public class AdvertiseManagerTest { mCallback, InstrumentationRegistry.getTargetContext().getAttributionSource()); - mAdvertiserId = AdvertiseManager.sTempRegistrationId; - } - - @After - public void tearDown() throws Exception { - TestUtils.clearAdapterService(mAdapterService); + mAdvertiserId = mAdvertiseManager.mTempRegistrationId; } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java index f64e934955..2ff148fa41 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java @@ -36,6 +36,7 @@ import android.content.Context; import android.content.res.Resources; import android.location.LocationManager; import android.os.Bundle; +import android.os.Process; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.test.mock.MockContentProvider; @@ -269,6 +270,118 @@ public class GattServiceTest { } @Test + public void clientConnectOverLeFailed() throws Exception { + int clientIf = 1; + String address = REMOTE_DEVICE_ADDRESS; + int addressType = BluetoothDevice.ADDRESS_TYPE_RANDOM; + boolean isDirect = true; + int transport = BluetoothDevice.TRANSPORT_LE; + boolean opportunistic = false; + int phy = 3; + + AttributionSource testAttributeSource = + new AttributionSource.Builder(Process.SYSTEM_UID) + .setPid(Process.myPid()) + .setDeviceId(Context.DEVICE_ID_DEFAULT) + .setPackageName("com.google.android.gms") + .setAttributionTag("com.google.android.gms.findmydevice") + .build(); + + mService.clientConnect( + clientIf, + address, + addressType, + isDirect, + transport, + opportunistic, + phy, + testAttributeSource); + + verify(mAdapterService).notifyDirectLeGattClientConnect(anyInt(), any()); + verify(mNativeInterface) + .gattClientConnect( + clientIf, address, addressType, isDirect, transport, opportunistic, phy, 0); + mService.onConnected(clientIf, 0, BluetoothGatt.GATT_CONNECTION_TIMEOUT, address); + verify(mAdapterService).notifyGattClientConnectFailed(anyInt(), any()); + } + + @Test + public void clientConnectDisconnectOverLe() throws Exception { + int clientIf = 1; + String address = REMOTE_DEVICE_ADDRESS; + int addressType = BluetoothDevice.ADDRESS_TYPE_RANDOM; + boolean isDirect = true; + int transport = BluetoothDevice.TRANSPORT_LE; + boolean opportunistic = false; + int phy = 3; + + AttributionSource testAttributeSource = + new AttributionSource.Builder(Process.SYSTEM_UID) + .setPid(Process.myPid()) + .setDeviceId(Context.DEVICE_ID_DEFAULT) + .setPackageName("com.google.android.gms") + .setAttributionTag("com.google.android.gms.findmydevice") + .build(); + + mService.clientConnect( + clientIf, + address, + addressType, + isDirect, + transport, + opportunistic, + phy, + testAttributeSource); + + verify(mAdapterService).notifyDirectLeGattClientConnect(anyInt(), any()); + verify(mNativeInterface) + .gattClientConnect( + clientIf, address, addressType, isDirect, transport, opportunistic, phy, 0); + mService.onConnected(clientIf, 15, BluetoothGatt.GATT_SUCCESS, address); + mService.clientDisconnect(clientIf, address, mAttributionSource); + + verify(mAdapterService).notifyGattClientDisconnect(anyInt(), any()); + } + + @Test + public void clientConnectOverLeDisconnectedByRemote() throws Exception { + int clientIf = 1; + String address = REMOTE_DEVICE_ADDRESS; + int addressType = BluetoothDevice.ADDRESS_TYPE_RANDOM; + boolean isDirect = true; + int transport = BluetoothDevice.TRANSPORT_LE; + boolean opportunistic = false; + int phy = 3; + + AttributionSource testAttributeSource = + new AttributionSource.Builder(Process.SYSTEM_UID) + .setPid(Process.myPid()) + .setDeviceId(Context.DEVICE_ID_DEFAULT) + .setPackageName("com.google.android.gms") + .setAttributionTag("com.google.android.gms.findmydevice") + .build(); + + mService.clientConnect( + clientIf, + address, + addressType, + isDirect, + transport, + opportunistic, + phy, + testAttributeSource); + + verify(mAdapterService).notifyDirectLeGattClientConnect(anyInt(), any()); + verify(mNativeInterface) + .gattClientConnect( + clientIf, address, addressType, isDirect, transport, opportunistic, phy, 0); + mService.onConnected(clientIf, 15, BluetoothGatt.GATT_SUCCESS, address); + mService.onDisconnected(clientIf, 15, 1, address); + + verify(mAdapterService).notifyGattClientDisconnect(anyInt(), any()); + } + + @Test public void disconnectAll() { Map<Integer, String> connMap = new HashMap<>(); int clientIf = 1; diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java index 0ff583d158..b7a9a49116 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java @@ -18,9 +18,11 @@ package com.android.bluetooth.hfp; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -46,6 +48,7 @@ import android.media.AudioManager; import android.os.ParcelUuid; import android.os.RemoteException; import android.os.SystemClock; +import android.platform.test.annotations.EnableFlags; import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; @@ -60,7 +63,6 @@ import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.bluetooth.flags.Flags; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -397,12 +399,9 @@ public class HeadsetServiceTest { HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED, HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED, mCurrentDevice); - try { - mHeadsetService.messageFromNative(connectingEvent); - Assert.fail("Expect an IllegalStateException"); - } catch (IllegalStateException exception) { - // Do nothing - } + assertThrows( + IllegalStateException.class, + () -> mHeadsetService.messageFromNative(connectingEvent)); verifyNoMoreInteractions(mObjectsFactory); } @@ -1299,6 +1298,54 @@ public class HeadsetServiceTest { assertThat(mHeadsetService.isInbandRingingEnabled()).isFalse(); } + @Test + @EnableFlags({Flags.FLAG_UPDATE_ACTIVE_DEVICE_IN_BAND_RINGTONE}) + public void testIncomingCallDeviceConnect_InbandRingStatus() { + when(mDatabaseManager.getProfileConnectionPolicy( + any(BluetoothDevice.class), eq(BluetoothProfile.HEADSET))) + .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN); + mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0); + connectDeviceHelper(mCurrentDevice); + + when(mStateMachines.get(mCurrentDevice).getDevice()).thenReturn(mCurrentDevice); + when(mStateMachines.get(mCurrentDevice).getConnectionState()) + .thenReturn(BluetoothProfile.STATE_CONNECTED); + + when(mSystemInterface.isRinging()).thenReturn(true); + mHeadsetService.setActiveDevice(mCurrentDevice); + + verify(mNativeInterface).setActiveDevice(mCurrentDevice); + verify(mStateMachines.get(mCurrentDevice)) + .sendMessage(HeadsetStateMachine.CONNECT_AUDIO, mCurrentDevice); + verify(mStateMachines.get(mCurrentDevice)) + .sendMessage(eq(HeadsetStateMachine.SEND_BSIR), eq(1)); + } + + @Test + @EnableFlags({Flags.FLAG_UPDATE_ACTIVE_DEVICE_IN_BAND_RINGTONE}) + public void testIncomingCallWithDeviceAudioConnected() { + ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>(); + when(mDatabaseManager.getProfileConnectionPolicy( + any(BluetoothDevice.class), eq(BluetoothProfile.HEADSET))) + .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN); + for (int i = 2; i >= 0; i--) { + mCurrentDevice = TestUtils.getTestDevice(mAdapter, i); + connectDeviceHelper(mCurrentDevice); + connectedDevices.add(mCurrentDevice); + } + + mHeadsetService.setActiveDevice(connectedDevices.get(1)); + when(mStateMachines.get(connectedDevices.get(1)).getAudioState()) + .thenReturn(BluetoothHeadset.STATE_AUDIO_CONNECTED); + + when(mSystemInterface.isRinging()).thenReturn(true); + mHeadsetService.setActiveDevice(connectedDevices.get(2)); + + verify(mNativeInterface).setActiveDevice(connectedDevices.get(2)); + verify(mStateMachines.get(connectedDevices.get(2)), atLeast(1)) + .sendMessage(eq(HeadsetStateMachine.SEND_BSIR), eq(0)); + } + private void addConnectedDeviceHelper(BluetoothDevice device) { mCurrentDevice = device; when(mDatabaseManager.getProfileConnectionPolicy( @@ -1332,4 +1379,19 @@ public class HeadsetServiceTest { .thenReturn(priority); assertThat(mHeadsetService.okToAcceptConnection(device, false)).isEqualTo(expected); } + + private void connectDeviceHelper(BluetoothDevice device) { + assertThat(mHeadsetService.connect(device)).isTrue(); + verify(mObjectsFactory) + .makeStateMachine( + device, + mHeadsetService.getStateMachinesThreadLooper(), + mHeadsetService, + mAdapterService, + mNativeInterface, + mSystemInterface); + when(mStateMachines.get(device).getDevice()).thenReturn(device); + when(mStateMachines.get(device).getConnectionState()) + .thenReturn(BluetoothProfile.STATE_CONNECTED); + } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java index e594c924a8..0b42a50d92 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java @@ -1793,28 +1793,6 @@ public class HeadsetStateMachineTest { } @Test - @EnableFlags({Flags.FLAG_HFP_ALLOW_VOLUME_CHANGE_WITHOUT_SCO}) - public void testVolumeChangeEvent_fromIntentWhenConnected() { - setUpConnectedState(); - int originalVolume = mHeadsetStateMachine.mSpeakerVolume; - mHeadsetStateMachine.mSpeakerVolume = 0; - int vol = 10; - - // Send INTENT_SCO_VOLUME_CHANGED message - Intent volumeChange = new Intent(AudioManager.ACTION_VOLUME_CHANGED); - volumeChange.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, vol); - - mHeadsetStateMachine.sendMessage( - HeadsetStateMachine.INTENT_SCO_VOLUME_CHANGED, volumeChange); - TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper()); - - // verify volume processed - verify(mNativeInterface).setVolume(mTestDevice, HeadsetHalConstants.VOLUME_TYPE_SPK, vol); - - mHeadsetStateMachine.mSpeakerVolume = originalVolume; - } - - @Test public void testVolumeChangeEvent_fromIntentWhenAudioOn() { setUpAudioOnState(); int originalVolume = mHeadsetStateMachine.mSpeakerVolume; diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java index 823456b5a2..500e697502 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java @@ -30,7 +30,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.btservice.ServiceFactory; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -64,7 +63,7 @@ public class ContentControlIdKeeperTest { public int testCcidAcquire(ParcelUuid uuid, int context, int expectedListSize) { int ccid = ContentControlIdKeeper.acquireCcid(uuid, context); - Assert.assertNotEquals(ccid, ContentControlIdKeeper.CCID_INVALID); + assertThat(ccid).isNotEqualTo(ContentControlIdKeeper.CCID_INVALID); verify(mLeAudioServiceMock).setCcidInformation(eq(uuid), eq(ccid), eq(context)); Map<ParcelUuid, Pair<Integer, Integer>> uuidToCcidContextPair = @@ -98,7 +97,7 @@ public class ContentControlIdKeeperTest { int ccid_one = testCcidAcquire(uuid_one, BluetoothLeAudio.CONTEXT_TYPE_MEDIA, 1); int ccid_two = testCcidAcquire(uuid_two, BluetoothLeAudio.CONTEXT_TYPE_RINGTONE, 2); - Assert.assertNotEquals(ccid_one, ccid_two); + assertThat(ccid_one).isNotEqualTo(ccid_two); testCcidRelease(uuid_one, ccid_one, 1); testCcidRelease(uuid_two, ccid_two, 0); diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java index 401e0c6844..8433d1398c 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java @@ -22,6 +22,7 @@ import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID; import static com.android.bluetooth.bass_client.BassConstants.INVALID_BROADCAST_ID; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.*; @@ -57,7 +58,6 @@ import com.android.bluetooth.flags.Flags; import com.android.bluetooth.tbs.TbsService; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -91,7 +91,6 @@ public class LeAudioBroadcastServiceTest { private LeAudioService mService; private LeAudioIntentReceiver mLeAudioIntentReceiver; private LinkedBlockingQueue<Intent> mIntentQueue; - private boolean onBroadcastToUnicastFallbackGroupChangedCallbackCalled = false; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private ActiveDeviceManager mActiveDeviceManager; @@ -105,6 +104,7 @@ public class LeAudioBroadcastServiceTest { @Mock private TbsService mTbsService; @Mock private MetricsLogger mMetricsLogger; @Mock private IBluetoothLeBroadcastCallback mCallbacks; + @Mock private IBluetoothLeAudioCallback mLeAudioCallbacks; @Mock private IBinder mBinder; @Spy private LeAudioObjectsFactory mObjectsFactory = LeAudioObjectsFactory.getInstance(); @@ -155,6 +155,7 @@ public class LeAudioBroadcastServiceTest { mTargetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); doReturn(mBinder).when(mCallbacks).asBinder(); + doReturn(mBinder).when(mLeAudioCallbacks).asBinder(); doNothing().when(mBinder).linkToDeath(any(), eq(0)); // Use spied objects factory @@ -750,10 +751,8 @@ public class LeAudioBroadcastServiceTest { TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper()); - List<BluetoothLeBroadcastMetadata> meta_list = mService.getAllBroadcastMetadata(); - assertThat(meta_list).isNotNull(); - Assert.assertNotEquals(meta_list.size(), 0); - assertThat(meta_list.get(0)).isEqualTo(state_event.broadcastMetadata); + assertThat(mService.getAllBroadcastMetadata()) + .containsExactly(state_event.broadcastMetadata); } @Test @@ -1674,7 +1673,7 @@ public class LeAudioBroadcastServiceTest { reset(mAudioManager); - Assert.assertTrue(mService.setActiveDevice(mDevice2)); + assertThat(mService.setActiveDevice(mDevice2)).isTrue(); if (!Flags.leaudioUseAudioRecordingListener()) { /* Update fallback active device (only input is active) */ @@ -1707,34 +1706,8 @@ public class LeAudioBroadcastServiceTest { when(mDatabaseManager.getMostRecentlyConnectedDevices()).thenReturn(devices); - onBroadcastToUnicastFallbackGroupChangedCallbackCalled = false; - - IBluetoothLeAudioCallback leAudioCallbacks = - new IBluetoothLeAudioCallback.Stub() { - @Override - public void onCodecConfigChanged(int gid, BluetoothLeAudioCodecStatus status) {} - - @Override - public void onGroupStatusChanged(int gid, int gStatus) {} - - @Override - public void onGroupNodeAdded(BluetoothDevice device, int gid) {} - - @Override - public void onGroupNodeRemoved(BluetoothDevice device, int gid) {} - - @Override - public void onGroupStreamStatusChanged(int groupId, int groupStreamStatus) {} - - @Override - public void onBroadcastToUnicastFallbackGroupChanged(int groupId) { - onBroadcastToUnicastFallbackGroupChangedCallbackCalled = true; - assertThat(groupId2).isEqualTo(groupId); - } - }; - synchronized (mService.mLeAudioCallbacks) { - mService.mLeAudioCallbacks.register(leAudioCallbacks); + mService.mLeAudioCallbacks.register(mLeAudioCallbacks); } initializeNative(); @@ -1745,21 +1718,49 @@ public class LeAudioBroadcastServiceTest { TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper()); assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId2); - assertThat(onBroadcastToUnicastFallbackGroupChangedCallbackCalled).isTrue(); - onBroadcastToUnicastFallbackGroupChangedCallbackCalled = false; + try { + verify(mLeAudioCallbacks).onBroadcastToUnicastFallbackGroupChanged(groupId2); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + synchronized (mService.mLeAudioCallbacks) { - mService.mLeAudioCallbacks.unregister(leAudioCallbacks); + mService.mLeAudioCallbacks.unregister(mLeAudioCallbacks); } } + @Test + public void testGetLocalBroadcastReceivers() { + int broadcastId = 243; + byte[] code = {0x00, 0x01, 0x00, 0x02}; + + synchronized (mService.mBroadcastCallbacks) { + mService.mBroadcastCallbacks.register(mCallbacks); + } + + BluetoothLeAudioContentMetadata.Builder meta_builder = + new BluetoothLeAudioContentMetadata.Builder(); + meta_builder.setLanguage("deu"); + meta_builder.setProgramInfo("Subgroup broadcast info"); + BluetoothLeAudioContentMetadata meta = meta_builder.build(); + + verifyBroadcastStarted(broadcastId, buildBroadcastSettingsFromMetadata(meta, code, 1)); + when(mBassClientService.getSyncedBroadcastSinks(broadcastId)).thenReturn(List.of(mDevice)); + assertThat(mService.getLocalBroadcastReceivers().size()).isEqualTo(1); + assertThat(mService.getLocalBroadcastReceivers()).containsExactly(mDevice); + + verifyBroadcastStopped(broadcastId); + assertThat(mService.getLocalBroadcastReceivers()).isEmpty(); + } + private class LeAudioIntentReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { try { mIntentQueue.put(intent); } catch (InterruptedException e) { - Assert.fail("Cannot add Intent to the queue: " + e.getMessage()); + assertWithMessage("Cannot add Intent to the queue: " + e.toString()).fail(); } } } @@ -1802,11 +1803,11 @@ public class LeAudioBroadcastServiceTest { eq(mDevice2), eq(mDevice), connectionInfoArgumentCaptor.capture()); List<BluetoothProfileConnectionInfo> connInfos = connectionInfoArgumentCaptor.getAllValues(); - Assert.assertEquals(connInfos.size(), 1); + assertThat(connInfos.size()).isEqualTo(1); assertThat(connInfos.get(0).isLeOutput()).isFalse(); } - Assert.assertEquals(mService.getBroadcastToUnicastFallbackGroup(), groupId2); + assertThat(mService.getBroadcastToUnicastFallbackGroup()).isEqualTo(groupId2); } private void disconnectDevice(BluetoothDevice device) { @@ -1819,8 +1820,8 @@ public class LeAudioBroadcastServiceTest { device, BluetoothProfile.STATE_DISCONNECTING, BluetoothProfile.STATE_CONNECTED); - Assert.assertEquals( - BluetoothProfile.STATE_DISCONNECTING, mService.getConnectionState(device)); + assertThat(mService.getConnectionState(device)) + .isEqualTo(BluetoothProfile.STATE_DISCONNECTING); LeAudioStackEvent create_event = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); @@ -1833,8 +1834,8 @@ public class LeAudioBroadcastServiceTest { device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING); - Assert.assertEquals( - BluetoothProfile.STATE_DISCONNECTED, mService.getConnectionState(device)); + assertThat(mService.getConnectionState(device)) + .isEqualTo(BluetoothProfile.STATE_DISCONNECTED); mService.deviceDisconnected(device, false); TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper()); } @@ -1848,18 +1849,17 @@ public class LeAudioBroadcastServiceTest { public void testSetDefaultBroadcastToUnicastFallbackGroup() { int groupId = 1; int groupId2 = 2; - int broadcastId = 243; - byte[] code = {0x00, 0x01, 0x00, 0x02}; List<BluetoothDevice> devices = new ArrayList<>(); when(mDatabaseManager.getMostRecentlyConnectedDevices()).thenReturn(devices); - Assert.assertEquals( - mService.getBroadcastToUnicastFallbackGroup(), LE_AUDIO_GROUP_ID_INVALID); + /* If no connected devices - no fallback device */ + assertThat(mService.getBroadcastToUnicastFallbackGroup()) + .isEqualTo(LE_AUDIO_GROUP_ID_INVALID); initializeNative(); devices.add(mDevice); - prepareHandoverStreamingBroadcast(groupId, broadcastId, code); + prepareConnectedUnicastDevice(groupId, mDevice); mService.deviceConnected(mDevice); devices.add(mDevice2); prepareConnectedUnicastDevice(groupId2, mDevice2); @@ -1871,12 +1871,10 @@ public class LeAudioBroadcastServiceTest { mService.messageFromNative(stackEvent); /* First connected group become fallback group */ - Assert.assertEquals(mService.mUnicastGroupIdDeactivatedForBroadcastTransition, groupId); - - reset(mAudioManager); + assertThat(mService.getBroadcastToUnicastFallbackGroup()).isEqualTo(groupId); + /* Force group as fallback 1 -> 2 */ mService.setBroadcastToUnicastFallbackGroup(groupId2); - assertThat(mService.getBroadcastToUnicastFallbackGroup()).isEqualTo(groupId2); /* Disconnected last device from fallback should trigger set default group 2 -> 1 */ @@ -1891,6 +1889,63 @@ public class LeAudioBroadcastServiceTest { .isEqualTo(LE_AUDIO_GROUP_ID_INVALID); } + @Test + @EnableFlags({ + Flags.FLAG_LEAUDIO_BROADCAST_API_MANAGE_PRIMARY_GROUP, + Flags.FLAG_LEAUDIO_BROADCAST_PRIMARY_GROUP_SELECTION + }) + public void testUpdateFallbackDeviceWhileSettingActiveDevice() { + int groupId = 1; + int groupId2 = 2; + int broadcastId = 243; + byte[] code = {0x00, 0x01, 0x00, 0x02}; + List<BluetoothDevice> devices = new ArrayList<>(); + + when(mDatabaseManager.getMostRecentlyConnectedDevices()).thenReturn(devices); + + synchronized (mService.mLeAudioCallbacks) { + mService.mLeAudioCallbacks.register(mLeAudioCallbacks); + } + + initializeNative(); + devices.add(mDevice2); + prepareConnectedUnicastDevice(groupId2, mDevice2); + devices.add(mDevice); + prepareHandoverStreamingBroadcast(groupId, broadcastId, code); + + /* Earliest connected group (2) become fallback device */ + assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId2); + try { + verify(mLeAudioCallbacks).onBroadcastToUnicastFallbackGroupChanged(groupId2); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + Mockito.clearInvocations(mLeAudioCallbacks); + reset(mAudioManager); + + /* Change active device while broadcasting - result in replacing fallback group 2->1 */ + assertThat(mService.setActiveDevice(mDevice)).isTrue(); + TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper()); + assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId); + try { + verify(mLeAudioCallbacks).onBroadcastToUnicastFallbackGroupChanged(groupId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + /* Verify that fallback device is not changed when there is no running broadcast */ + Mockito.clearInvocations(mLeAudioCallbacks); + verifyBroadcastStopped(broadcastId); + assertThat(mService.setActiveDevice(mDevice2)).isTrue(); + TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper()); + try { + verify(mLeAudioCallbacks, times(0)).onBroadcastToUnicastFallbackGroupChanged(anyInt()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private BluetoothLeBroadcastSettings buildBroadcastSettingsFromMetadata( BluetoothLeAudioContentMetadata contentMetadata, @Nullable byte[] broadcastCode, diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java index 3e523ceecc..312a031f82 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java @@ -46,6 +46,8 @@ import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetoothLeAudioCallback; +import android.bluetooth.le.IScannerCallback; +import android.bluetooth.le.ScanResult; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -56,6 +58,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.os.Handler; import android.os.Looper; import android.os.ParcelUuid; +import android.os.RemoteException; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; @@ -282,6 +285,9 @@ public class LeAudioServiceTest { .when(mAdapterService) .getRemoteUuids(any(BluetoothDevice.class)); + doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); + doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); + verify(mNativeInterface, timeout(3000).times(1)).init(any()); } @@ -532,8 +538,6 @@ public class LeAudioServiceTest { .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO)) .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); // Return No UUID doReturn(new ParcelUuid[] {}) @@ -547,9 +551,6 @@ public class LeAudioServiceTest { /** Test that an outgoing connection to device with PRIORITY_OFF is rejected */ @Test public void testOutgoingConnectPriorityOff() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); - // Set the device priority to PRIORITY_OFF so connect() should fail when(mDatabaseManager.getProfileConnectionPolicy(mLeftDevice, BluetoothProfile.LE_AUDIO)) .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); @@ -568,8 +569,6 @@ public class LeAudioServiceTest { .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO)) .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); // Send a connect request assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue(); @@ -621,6 +620,13 @@ public class LeAudioServiceTest { } } + private void injectAndVerifyDeviceConnected(BluetoothDevice device) { + generateConnectionMessageFromNative( + device, + LeAudioStackEvent.CONNECTION_STATE_CONNECTED, + LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED); + } + private void injectNoVerifyDeviceConnected(BluetoothDevice device) { generateUnexpectedConnectionMessageFromNative( device, LeAudioStackEvent.CONNECTION_STATE_CONNECTED); @@ -648,8 +654,6 @@ public class LeAudioServiceTest { .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO)) .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); // Send a connect request assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue(); @@ -778,8 +782,6 @@ public class LeAudioServiceTest { .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO)) .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); // Create device descriptor with connect request assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue(); @@ -855,8 +857,6 @@ public class LeAudioServiceTest { .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO)) .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); // Create device descriptor with connect request assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue(); @@ -932,8 +932,6 @@ public class LeAudioServiceTest { .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO)) .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); // Create device descriptor with connect request assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue(); @@ -1073,8 +1071,6 @@ public class LeAudioServiceTest { /** Test setting connection policy */ @Test public void testSetConnectionPolicy() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); doReturn(true) .when(mDatabaseManager) .setProfileConnectionPolicy(any(BluetoothDevice.class), anyInt(), anyInt()); @@ -1187,6 +1183,13 @@ public class LeAudioServiceTest { // Make device bonded mBondedDevices.add(device); + LeAudioStackEvent nodeGroupAdded = + new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_NODE_STATUS_CHANGED); + nodeGroupAdded.device = device; + nodeGroupAdded.valueInt1 = GroupId; + nodeGroupAdded.valueInt2 = LeAudioStackEvent.GROUP_NODE_ADDED; + mService.messageFromNative(nodeGroupAdded); + // Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and // 250ms for processing two messages should be way more than enough. Anything that breaks // this indicate some breakage in other part of Android OS @@ -1214,13 +1217,6 @@ public class LeAudioServiceTest { assertThat(mService.getConnectionState(device)).isEqualTo(BluetoothProfile.STATE_CONNECTED); - LeAudioStackEvent nodeGroupAdded = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_NODE_STATUS_CHANGED); - nodeGroupAdded.device = device; - nodeGroupAdded.valueInt1 = GroupId; - nodeGroupAdded.valueInt2 = LeAudioStackEvent.GROUP_NODE_ADDED; - mService.messageFromNative(nodeGroupAdded); - // Verify that the device is in the list of connected devices assertThat(mService.getConnectedDevices().contains(device)).isTrue(); // Verify the list of previously connected devices @@ -1254,8 +1250,7 @@ public class LeAudioServiceTest { // Not connected device assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); - // Connected device - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); + // Connect device connectTestDevice(mSingleDevice, testGroupId); // Add location support @@ -1305,8 +1300,7 @@ public class LeAudioServiceTest { // Not connected device assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); - // Connected device - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); + // Connect device connectTestDevice(mSingleDevice, testGroupId); // Add location support @@ -1373,8 +1367,7 @@ public class LeAudioServiceTest { // Not connected device assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); - // Connected device - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); + // Connect device connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); @@ -1424,27 +1417,16 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 0; // Not connected device assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); - // Connected device - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); + // Connect device connectTestDevice(mSingleDevice, testGroupId); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice; - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); verify(mNativeInterface, times(0)).groupSetActive(groupId); @@ -1458,8 +1440,6 @@ public class LeAudioServiceTest { /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; // Not connected device @@ -1467,33 +1447,14 @@ public class LeAudioServiceTest { // Define some return values needed in test doReturn(-1).when(mVolumeControlService).getAudioDeviceGroupVolume(anyInt()); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); // Connect both connectTestDevice(mSingleDevice, groupId_1); connectTestDevice(mSingleDevice_2, groupId_2); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice; - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId_1; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); - - // Add location support - audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice_2; - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId_2; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId_1, availableContexts, direction); + injectAudioConfChanged(groupId_2, availableContexts, direction); assertThat(mService.setActiveDevice(mSingleDevice)).isTrue(); verify(mNativeInterface).groupSetActive(groupId_1); @@ -1556,8 +1517,6 @@ public class LeAudioServiceTest { /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; // Not connected device @@ -1571,15 +1530,7 @@ public class LeAudioServiceTest { connectTestDevice(mSingleDevice, groupId_1); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice; - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId_1; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId_1, availableContexts, direction); assertThat(mService.setActiveDevice(mSingleDevice)).isTrue(); verify(mNativeInterface).groupSetActive(groupId_1); @@ -1617,27 +1568,16 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5; // Not connected device assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); - // Connected device - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); + // Connect device connectTestDevice(mSingleDevice, testGroupId); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice; - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mSingleDevice)).isTrue(); verify(mNativeInterface).groupSetActive(groupId); @@ -1691,8 +1631,9 @@ public class LeAudioServiceTest { @Test @DisableFlags(Flags.FLAG_LEAUDIO_BROADCAST_PRIMARY_GROUP_SELECTION) public void testUpdateUnicastFallbackActiveDeviceGroupDuringBroadcast() { + List<BluetoothDevice> devices = new ArrayList<>(); int groupId = 1; - int preGroupId = 2; + int groupId_2 = 2; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; int snkAudioLocation = 3; @@ -1701,14 +1642,23 @@ public class LeAudioServiceTest { int broadcastId = 243; byte[] code = {0x00, 0x01, 0x00, 0x02}; + when(mDatabaseManager.getMostRecentlyConnectedDevices()).thenReturn(devices); + // Not connected device assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); - // Connected device + // Connect devices doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - connectTestDevice(mSingleDevice, testGroupId); + devices.add(mSingleDevice); + connectTestDevice(mSingleDevice, groupId); + devices.add(mSingleDevice_2); + connectTestDevice(mSingleDevice_2, groupId_2); - mService.mUnicastGroupIdDeactivatedForBroadcastTransition = preGroupId; + // Default fallback group is LE_AUDIO_GROUP_ID_INVALID + assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition) + .isEqualTo(LE_AUDIO_GROUP_ID_INVALID); + + mService.mUnicastGroupIdDeactivatedForBroadcastTransition = groupId_2; // mock create broadcast and currentlyActiveGroupId remains LE_AUDIO_GROUP_ID_INVALID BluetoothLeAudioContentMetadata.Builder meta_builder = new BluetoothLeAudioContentMetadata.Builder(); @@ -1720,24 +1670,31 @@ public class LeAudioServiceTest { LeAudioStackEvent broadcastCreatedEvent = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_CREATED); - broadcastCreatedEvent.device = mSingleDevice; broadcastCreatedEvent.valueInt1 = broadcastId; broadcastCreatedEvent.valueBool1 = true; mService.messageFromNative(broadcastCreatedEvent); + LeAudioStackEvent broadcastStateStreamingEvent = + new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_STATE); + broadcastStateStreamingEvent.valueInt1 = broadcastId; + broadcastStateStreamingEvent.valueInt2 = LeAudioStackEvent.BROADCAST_STATE_STREAMING; + mService.messageFromNative(broadcastStateStreamingEvent); + + injectAudioConfChanged(groupId, availableContexts, direction); + LeAudioStackEvent audioConfChangedEvent = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice; + audioConfChangedEvent.device = mSingleDevice_2; audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; + audioConfChangedEvent.valueInt2 = groupId_2; audioConfChangedEvent.valueInt3 = snkAudioLocation; audioConfChangedEvent.valueInt4 = srcAudioLocation; audioConfChangedEvent.valueInt5 = availableContexts; mService.messageFromNative(audioConfChangedEvent); // Verify only update the fallback group and not proceed to change active - assertThat(mService.setActiveDevice(mSingleDevice)).isTrue(); - assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId); + assertThat(mService.setActiveDevice(mSingleDevice_2)).isTrue(); + assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId_2); // Verify only update the fallback group to INVALID and not proceed to change active assertThat(mService.setActiveDevice(null)).isTrue(); @@ -1753,14 +1710,11 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5; int nodeStatus = LeAudioStackEvent.GROUP_NODE_ADDED; int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE; // Single active device - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); // Add device to group @@ -1774,15 +1728,7 @@ public class LeAudioServiceTest { assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice; - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mSingleDevice)).isTrue(); @@ -1850,7 +1796,6 @@ public class LeAudioServiceTest { doReturn(testVolume).when(mVolumeControlService).getAudioDeviceGroupVolume(testGroupId); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); injectAudioConfChanged( testGroupId, @@ -1921,7 +1866,6 @@ public class LeAudioServiceTest { /** Test native interface audio configuration changed message handling */ @Test public void testMessageFromNativeAudioConfChangedActiveGroup() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); injectAudioConfChanged( testGroupId, @@ -1943,7 +1887,6 @@ public class LeAudioServiceTest { /** Test native interface audio configuration changed message handling */ @Test public void testMessageFromNativeAudioConfChangedInactiveGroup() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); Integer contexts = @@ -1956,7 +1899,6 @@ public class LeAudioServiceTest { /** Test native interface audio configuration changed message handling */ @Test public void testMessageFromNativeAudioConfChangedNoGroupChanged() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); injectAudioConfChanged(testGroupId, 0, 3); @@ -1969,7 +1911,6 @@ public class LeAudioServiceTest { */ @Test public void testHealthBaseDeviceAction() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); LeAudioStackEvent healthBaseDevAction = @@ -1982,7 +1923,6 @@ public class LeAudioServiceTest { @Test public void testHealthBasedGroupAction() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); LeAudioStackEvent healthBasedGroupAction = @@ -2041,7 +1981,6 @@ public class LeAudioServiceTest { /** Test native interface group status message handling */ @Test public void testMessageFromNativeGroupStatusChanged() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); injectAudioConfChanged( @@ -2102,7 +2041,6 @@ public class LeAudioServiceTest { /** Test native interface group stream status message handling */ @Test public void testMessageFromNativeGroupStreamStatusChanged() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); injectAudioConfChanged( @@ -2164,7 +2102,6 @@ public class LeAudioServiceTest { injectLocalCodecConfigCapaChanged(INPUT_CAPABILITIES_CONFIG, OUTPUT_CAPABILITIES_CONFIG); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); testCodecStatus = @@ -2251,7 +2188,6 @@ public class LeAudioServiceTest { injectLocalCodecConfigCapaChanged(INPUT_CAPABILITIES_CONFIG, OUTPUT_CAPABILITIES_CONFIG); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); testCodecStatus = @@ -2347,7 +2283,6 @@ public class LeAudioServiceTest { injectLocalCodecConfigCapaChanged(INPUT_CAPABILITIES_CONFIG, OUTPUT_CAPABILITIES_CONFIG); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); testCodecStatus = @@ -2418,15 +2353,12 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; - ; + int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE; BluetoothDevice leadDevice; BluetoothDevice memberDevice = mLeftDevice; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); @@ -2437,15 +2369,7 @@ public class LeAudioServiceTest { assertThat(mService.setActiveDevice(leadDevice)).isFalse(); - // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(leadDevice)).isTrue(); @@ -2494,15 +2418,12 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; - ; + int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE; BluetoothDevice leadDevice; BluetoothDevice memberDevice = mLeftDevice; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); @@ -2514,14 +2435,7 @@ public class LeAudioServiceTest { assertThat(mService.setActiveDevice(leadDevice)).isFalse(); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(leadDevice)).isTrue(); @@ -2573,7 +2487,6 @@ public class LeAudioServiceTest { int direction = 1; int availableContexts = 4; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); @@ -2632,7 +2545,6 @@ public class LeAudioServiceTest { int direction = 1; int availableContexts = 4; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); assertThat(mService.setActiveDevice(mLeftDevice)).isFalse(); @@ -2693,7 +2605,6 @@ public class LeAudioServiceTest { @Test public void testGetAudioLocation() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); assertThat(mService.getAudioLocation(null)) @@ -2711,7 +2622,6 @@ public class LeAudioServiceTest { @Test public void testGetConnectedPeerDevices() { - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, testGroupId); connectTestDevice(mRightDevice, testGroupId); @@ -2772,8 +2682,6 @@ public class LeAudioServiceTest { doReturn(new BluetoothDevice[] {mSingleDevice}).when(mAdapterService).getBondedDevices(); when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO)) .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); @@ -2787,7 +2695,7 @@ public class LeAudioServiceTest { int groupId = 1; mService.handleBluetoothEnabled(); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); + connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); verify(mMcpService).setDeviceAuthorized(mLeftDevice, true); @@ -2799,7 +2707,6 @@ public class LeAudioServiceTest { public void testAuthorizeMcpServiceOnBluetoothEnableAndNodeRemoval() { int groupId = 1; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); @@ -2832,8 +2739,6 @@ public class LeAudioServiceTest { int groupId = 1; mService.handleBluetoothEnabled(); - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); - doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class)); doReturn(true) .when(mDatabaseManager) .setProfileConnectionPolicy(any(BluetoothDevice.class), anyInt(), anyInt()); @@ -2875,7 +2780,6 @@ public class LeAudioServiceTest { int firstGroupId = 1; int secondGroupId = 2; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, firstGroupId); connectTestDevice(mRightDevice, firstGroupId); connectTestDevice(mSingleDevice, secondGroupId); @@ -2923,14 +2827,11 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 | AUDIO_DIRECTION_INPUT_BIT = 0x02; */ int direction = 3; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5; int nodeStatus = LeAudioStackEvent.GROUP_NODE_ADDED; int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE; // Single active device - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mSingleDevice, testGroupId); // Add device to group @@ -2944,15 +2845,7 @@ public class LeAudioServiceTest { assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice; - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mSingleDevice)).isTrue(); @@ -2973,11 +2866,8 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); @@ -2989,14 +2879,7 @@ public class LeAudioServiceTest { assertThat(groupDevicesById.contains(mRightDevice)).isTrue(); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mLeftDevice)).isTrue(); verify(mNativeInterface).groupSetActive(groupId); @@ -3013,13 +2896,12 @@ public class LeAudioServiceTest { reset(mNativeInterface); /* Don't expect any change. */ - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); verify(mNativeInterface, times(0)).groupSetActive(groupId); reset(mNativeInterface); /* Expect device to be incactive */ - audioConfChangedEvent.valueInt5 = 0; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, 0, direction); verify(mNativeInterface).groupSetActive(-1); injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE); @@ -3034,8 +2916,7 @@ public class LeAudioServiceTest { reset(mAudioManager); /* Expect device to be incactive */ - audioConfChangedEvent.valueInt5 = 1; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, 1, direction); verify(mNativeInterface).groupSetActive(groupId); reset(mNativeInterface); @@ -3049,16 +2930,189 @@ public class LeAudioServiceTest { any(BluetoothProfileConnectionInfo.class)); } + @Test + public void testAutoActiveMode_verifyDefaultState() { + int groupId = 1; + + /* Test scenario: + * 1. Connected two devices + * 2. Verify that Auto Active Mode is true be default. + */ + + connectTestDevice(mLeftDevice, groupId); + connectTestDevice(mRightDevice, groupId); + + assertThat(mService.isAutoActiveModeEnabled(groupId)).isTrue(); + } + + @Test + public void testAutoActiveMode_whenDeviceIsConnected_failToDisableIt() { + int groupId = 1; + /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ + int direction = 1; + int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; + + /* Test scenario: + * 1. Connected two devices + * 2. Disconnect one device + * 3. Verify that Auto Active Mode cannot be set. + */ + + connectTestDevice(mLeftDevice, groupId); + connectTestDevice(mRightDevice, groupId); + + assertThat(mService.setAutoActiveModeState(groupId, false)).isFalse(); + + // Checks group device lists for groupId 1 + List<BluetoothDevice> groupDevicesById = mService.getGroupDevices(groupId); + + assertThat(groupDevicesById).containsExactly(mLeftDevice, mRightDevice); + + injectAudioConfChanged(groupId, availableContexts, direction); + assertThat(mService.isGroupAvailableForStream(groupId)).isTrue(); + + injectAndVerifyDeviceDisconnected(mLeftDevice); + + assertThat(mService.setAutoActiveModeState(groupId, false)).isFalse(); + } + + @Test + public void testAutoActiveMode_disabledWithSuccess() { + int groupId = 1; + /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ + int direction = 1; + int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; + + /* Test scenario: + * 1. Connected two devices + * 2. Disconnect both devices + * 3. Verify that Auto Active Mode can be set. + */ + + connectTestDevice(mLeftDevice, groupId); + connectTestDevice(mRightDevice, groupId); + + // Checks group device lists for groupId 1 + List<BluetoothDevice> groupDevicesById = mService.getGroupDevices(groupId); + + assertThat(groupDevicesById).containsExactly(mLeftDevice, mRightDevice); + + injectAudioConfChanged(groupId, availableContexts, direction); + + assertThat(mService.isGroupAvailableForStream(groupId)).isTrue(); + + injectAndVerifyDeviceDisconnected(mLeftDevice); + injectAndVerifyDeviceDisconnected(mRightDevice); + + assertThat(mService.setAutoActiveModeState(groupId, false)).isTrue(); + assertThat(mService.isAutoActiveModeEnabled(groupId)).isFalse(); + } + + @Test + public void testAutoActiveMode_whenUserSetsDeviceAsActive_resetToDefault() { + int groupId = 1; + /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ + int direction = 1; + int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; + + /* Test scenario: + * 1. Connected two devices + * 2. Disconnect both devices + * 3. Disable Auto Active Mode + * 4. Connect at least one device + * 5. Set group as Active + * 6. Verify Auto Active Mode is back to default + */ + + mService.handleBluetoothEnabled(); + + connectTestDevice(mLeftDevice, groupId); + connectTestDevice(mRightDevice, groupId); + + // Checks group device lists for groupId 1 + List<BluetoothDevice> groupDevicesById = mService.getGroupDevices(groupId); + + assertThat(groupDevicesById).containsExactly(mLeftDevice, mRightDevice); + + injectAudioConfChanged(groupId, availableContexts, direction); + + assertThat(mService.isGroupAvailableForStream(groupId)).isTrue(); + + injectAndVerifyDeviceDisconnected(mLeftDevice); + injectAndVerifyDeviceDisconnected(mRightDevice); + + assertThat(mService.setAutoActiveModeState(groupId, false)).isTrue(); + assertThat(mService.isAutoActiveModeEnabled(groupId)).isFalse(); + + injectAndVerifyDeviceConnected(mLeftDevice); + injectAudioConfChanged(groupId, availableContexts, direction); + + assertThat(mService.setActiveDevice(mLeftDevice)).isTrue(); + assertThat(mService.isAutoActiveModeEnabled(groupId)).isTrue(); + } + + @Test + public void testAutoActiveMode_whenRemoteUsesTargetedAnnouncements_resetToDefault() + throws RemoteException { + int groupId = 1; + /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ + int direction = 1; + int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; + + /* Test scenario: + * 1. Connected two devices + * 2. Disconnect both devices + * 3. Disable Auto Active Mode + * 4. Connect at least one device + * 5. Detect TA on remote device + * 6. Verify Auto Active Mode is back to default + */ + + mService.handleBluetoothEnabled(); + ArgumentCaptor<IScannerCallback> scanCallbacks = + ArgumentCaptor.forClass(IScannerCallback.class); + + connectTestDevice(mLeftDevice, groupId); + connectTestDevice(mRightDevice, groupId); + + // Checks group device lists for groupId 1 + List<BluetoothDevice> groupDevicesById = mService.getGroupDevices(groupId); + + assertThat(groupDevicesById).containsExactly(mLeftDevice, mRightDevice); + injectAudioConfChanged(groupId, availableContexts, direction); + + assertThat(mService.isGroupAvailableForStream(groupId)).isTrue(); + + injectAndVerifyDeviceDisconnected(mLeftDevice); + injectAndVerifyDeviceDisconnected(mRightDevice); + + assertThat(mService.setAutoActiveModeState(groupId, false)).isTrue(); + assertThat(mService.isAutoActiveModeEnabled(groupId)).isFalse(); + + injectAndVerifyDeviceConnected(mLeftDevice); + injectAudioConfChanged(groupId, availableContexts, direction); + + verify(mScanController).registerScannerInternal(scanCallbacks.capture(), any(), any()); + + ScanResult scanResult = new ScanResult(mRightDevice, null, 0, 0); + + scanCallbacks.getValue().onScanResult(scanResult); + + assertThat(mService.isAutoActiveModeEnabled(groupId)).isTrue(); + } + /** * Test the group is activated once the available contexts are back. * + * <pre> * Scenario: - * 1. Have a group of 2 devices that initially does not expose any available contexts. - * The group shall be inactive at this point. - * 2. Once the available contexts are updated with non-zero value, - * the group shall become active. - * 3. The available contexts are changed to zero. Group becomes inactive. - * 4. The available contexts are back again. Group becomes active. + * 1. Have a group of 2 devices that initially does not expose any available contexts. The + * group shall be inactive at this point. + * 2. Once the available contexts are updated with non-zero value, the group shall become + * active. + * 3. The available contexts are changed to zero. Group becomes inactive. + * 4. The available contexts are back again. Group becomes active. + * </pre> */ @Test @EnableFlags(Flags.FLAG_LEAUDIO_UNICAST_NO_AVAILABLE_CONTEXTS) @@ -3066,11 +3120,8 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); @@ -3082,21 +3133,13 @@ public class LeAudioServiceTest { assertThat(groupDevicesById.contains(mRightDevice)).isTrue(); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = 0; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, 0, direction); assertThat(mService.setActiveDevice(mLeftDevice)).isFalse(); verify(mNativeInterface, times(0)).groupSetActive(groupId); // Expect device to be active - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); verify(mNativeInterface).groupSetActive(groupId); @@ -3112,8 +3155,7 @@ public class LeAudioServiceTest { reset(mNativeInterface); // Expect device to be inactive - audioConfChangedEvent.valueInt5 = 0; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, 0, direction); verify(mNativeInterface).groupSetActive(-1); injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE); @@ -3128,8 +3170,7 @@ public class LeAudioServiceTest { reset(mAudioManager); // Expect device to be active - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); verify(mNativeInterface).groupSetActive(groupId); reset(mNativeInterface); @@ -3146,14 +3187,16 @@ public class LeAudioServiceTest { /** * Test the group is activated once the available contexts are back. * + * <pre> * Scenario: - * 1. Have a group of 2 devices. The available contexts are non-zero. - * The group shall be active at this point. - * 2. Once the available contexts are updated with zero value, - * the group shall become inactive. - * 3. All group devices are disconnected. - * 4. Group devices are reconnected. The available contexts are still zero. - * 4. The available contexts are updated with non-zero value. Group becomes active. + * 1. Have a group of 2 devices. The available contexts are non-zero. The group shall be + * active at this point. + * 2. Once the available contexts are updated with zero value, the group shall become + * inactive. + * 3. All group devices are disconnected. + * 4. Group devices are reconnected. The available contexts are still zero. + * 5. The available contexts are updated with non-zero value. Group becomes active. + * </pre> */ @Test @EnableFlags(Flags.FLAG_LEAUDIO_UNICAST_NO_AVAILABLE_CONTEXTS) @@ -3161,11 +3204,8 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); @@ -3177,14 +3217,7 @@ public class LeAudioServiceTest { assertThat(groupDevicesById.contains(mRightDevice)).isTrue(); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mLeftDevice)).isTrue(); verify(mNativeInterface).groupSetActive(groupId); @@ -3201,8 +3234,7 @@ public class LeAudioServiceTest { reset(mNativeInterface); // Expect device to be inactive - audioConfChangedEvent.valueInt5 = 0; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, 0, direction); verify(mNativeInterface).groupSetActive(-1); injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE); @@ -3227,8 +3259,7 @@ public class LeAudioServiceTest { assertThat(mService.getConnectedDevices().contains(mRightDevice)).isFalse(); // Expect device to be inactive - audioConfChangedEvent.valueInt5 = 0; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, 0, direction); generateConnectionMessageFromNative( mLeftDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); @@ -3237,18 +3268,18 @@ public class LeAudioServiceTest { assertThat(mService.getConnectedDevices().contains(mLeftDevice)).isTrue(); // Expect device to be inactive - audioConfChangedEvent.valueInt5 = 0; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, 0, direction); generateConnectionMessageFromNative( - mRightDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); + mRightDevice, + BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_DISCONNECTED); assertThat(mService.getConnectionState(mRightDevice)) .isEqualTo(BluetoothProfile.STATE_CONNECTED); assertThat(mService.getConnectedDevices().contains(mRightDevice)).isTrue(); // Expect device to be active - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); verify(mNativeInterface).groupSetActive(groupId); @@ -3277,11 +3308,8 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); @@ -3293,14 +3321,7 @@ public class LeAudioServiceTest { assertThat(groupDevicesById.contains(mRightDevice)).isTrue(); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mLeftDevice)).isTrue(); verify(mNativeInterface).groupSetActive(groupId); @@ -3326,8 +3347,7 @@ public class LeAudioServiceTest { reset(mAudioManager); // Expect device to be inactive - audioConfChangedEvent.valueInt5 = 0; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, 0, direction); generateConnectionMessageFromNative( mLeftDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); @@ -3336,8 +3356,7 @@ public class LeAudioServiceTest { assertThat(mService.getConnectedDevices().contains(mLeftDevice)).isTrue(); // Expect device to be inactive - audioConfChangedEvent.valueInt5 = 0; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, 0, direction); generateConnectionMessageFromNative( mRightDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); @@ -3346,8 +3365,7 @@ public class LeAudioServiceTest { assertThat(mService.getConnectedDevices().contains(mRightDevice)).isTrue(); // Expect device to be active - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); verify(mNativeInterface).groupSetActive(groupId); @@ -3368,27 +3386,16 @@ public class LeAudioServiceTest { int groupId = 1; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; // Not connected device assertThat(mService.setActiveDevice(mSingleDevice)).isFalse(); - // Connected device - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); + // Connect device connectTestDevice(mSingleDevice, testGroupId); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice; - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = groupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mSingleDevice)).isTrue(); verify(mNativeInterface).groupSetActive(groupId); @@ -3436,8 +3443,6 @@ public class LeAudioServiceTest { int secondGroupId = 2; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; - int snkAudioLocation = 3; - int srcAudioLocation = 4; int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE; List<BluetoothDevice> devices = new ArrayList<>(); @@ -3448,8 +3453,7 @@ public class LeAudioServiceTest { assertThat(mService.getBroadcastToUnicastFallbackGroup()) .isEqualTo(BluetoothLeAudio.GROUP_ID_INVALID); - // Connected device - doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); + // Connect device devices.add(mSingleDevice); connectTestDevice(mSingleDevice, testGroupId); @@ -3457,15 +3461,7 @@ public class LeAudioServiceTest { assertThat(mService.getBroadcastToUnicastFallbackGroup()).isEqualTo(firstGroupId); // Add location support - LeAudioStackEvent audioConfChangedEvent = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED); - audioConfChangedEvent.device = mSingleDevice; - audioConfChangedEvent.valueInt1 = direction; - audioConfChangedEvent.valueInt2 = firstGroupId; - audioConfChangedEvent.valueInt3 = snkAudioLocation; - audioConfChangedEvent.valueInt4 = srcAudioLocation; - audioConfChangedEvent.valueInt5 = availableContexts; - mService.messageFromNative(audioConfChangedEvent); + injectAudioConfChanged(firstGroupId, availableContexts, direction); assertThat(mService.setActiveDevice(mSingleDevice)).isTrue(); verify(mNativeInterface).groupSetActive(firstGroupId); diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java new file mode 100644 index 0000000000..67f32c4f0c --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.bluetooth.le_scan; + +import static android.bluetooth.le.ScanSettings.SCAN_MODE_BALANCED; + +import static com.android.bluetooth.le_scan.ScanController.DEFAULT_REPORT_DELAY_FLOOR; + +import static com.google.common.truth.Truth.assertThat; + +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanSettings; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.filters.SmallTest; + +import com.android.bluetooth.TestUtils.FakeTimeProvider; + +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.LongStream; + +/** Test cases for {@link BatchScanThrottler}. */ +@SmallTest +@RunWith(TestParameterInjector.class) +public class BatchScanThrottlerTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + private FakeTimeProvider mTimeProvider; + + @Before + public void setUp() { + mTimeProvider = new FakeTimeProvider(); + } + + private void advanceTime(long amountToAdvanceMillis) { + mTimeProvider.advanceTime(Duration.ofMillis(amountToAdvanceMillis)); + } + + @Test + public void basicThrottling( + @TestParameter boolean isFiltered, @TestParameter boolean isScreenOn) { + BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, isScreenOn); + if (!isScreenOn) { + advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS); + } + Set<ScanClient> clients = + Collections.singleton( + createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, isFiltered)); + long[] backoffIntervals = + getBackoffIntervals( + isScreenOn + ? DEFAULT_REPORT_DELAY_FLOOR + : BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS); + for (long x : backoffIntervals) { + long expected = adjustExpectedInterval(x, isFiltered, isScreenOn); + assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(expected); + } + long expected = + adjustExpectedInterval( + backoffIntervals[backoffIntervals.length - 1], isFiltered, isScreenOn); + // Ensure that subsequent calls continue to return the final throttled interval + assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(expected); + assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(expected); + } + + @Test + public void screenOffDelayAndReset(@TestParameter boolean screenOnAtStart) { + BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, screenOnAtStart); + if (screenOnAtStart) { + throttler.onScreenOn(false); + } + Set<ScanClient> clients = + Collections.singleton(createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, true)); + long[] backoffIntervals = getBackoffIntervals(DEFAULT_REPORT_DELAY_FLOOR); + advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS - 1); + for (long x : backoffIntervals) { + assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x); + } + + backoffIntervals = + getBackoffIntervals(BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS); + advanceTime(1); + for (long x : backoffIntervals) { + assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x); + } + assertThat(throttler.getBatchTriggerIntervalMillis(clients)) + .isEqualTo(backoffIntervals[backoffIntervals.length - 1]); + } + + @Test + public void testScreenOnReset() { + BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, false); + advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS); + Set<ScanClient> clients = + Collections.singleton(createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, true)); + long[] backoffIntervals = + getBackoffIntervals(BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS); + for (long x : backoffIntervals) { + assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x); + } + + throttler.onScreenOn(true); + backoffIntervals = getBackoffIntervals(DEFAULT_REPORT_DELAY_FLOOR); + for (long x : backoffIntervals) { + assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x); + } + assertThat(throttler.getBatchTriggerIntervalMillis(clients)) + .isEqualTo(backoffIntervals[backoffIntervals.length - 1]); + } + + @Test + public void resetBackoff_restartsToFirstStage(@TestParameter boolean isScreenOn) { + BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, isScreenOn); + if (!isScreenOn) { + // Advance the time before we start the test to when the screen-off intervals should be + // used + advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS); + } + Set<ScanClient> clients = + Collections.singleton(createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, true)); + long[] backoffIntervals = + getBackoffIntervals( + isScreenOn + ? DEFAULT_REPORT_DELAY_FLOOR + : BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS); + for (long x : backoffIntervals) { + assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x); + } + assertThat(throttler.getBatchTriggerIntervalMillis(clients)) + .isEqualTo(backoffIntervals[backoffIntervals.length - 1]); + + throttler.resetBackoff(); + for (long x : backoffIntervals) { + assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x); + } + assertThat(throttler.getBatchTriggerIntervalMillis(clients)) + .isEqualTo(backoffIntervals[backoffIntervals.length - 1]); + } + + private long adjustExpectedInterval(long interval, boolean isFiltered, boolean isScreenOn) { + if (isFiltered) { + return interval; + } + long threshold = + isScreenOn + ? BatchScanThrottler.UNFILTERED_DELAY_FLOOR_MS + : BatchScanThrottler.UNFILTERED_SCREEN_OFF_DELAY_FLOOR_MS; + return Math.max(interval, threshold); + } + + private long[] getBackoffIntervals(long baseInterval) { + return LongStream.range(0, BatchScanThrottler.BACKOFF_MULTIPLIERS.length) + .map(x -> BatchScanThrottler.BACKOFF_MULTIPLIERS[(int) x] * baseInterval) + .toArray(); + } + + private ScanClient createBatchScanClient(long reportDelayMillis, boolean isFiltered) { + ScanSettings scanSettings = + new ScanSettings.Builder() + .setScanMode(SCAN_MODE_BALANCED) + .setReportDelay(reportDelayMillis) + .build(); + + return new ScanClient(1, scanSettings, createScanFilterList(isFiltered), 1); + } + + private List<ScanFilter> createScanFilterList(boolean isFiltered) { + List<ScanFilter> scanFilterList = null; + if (isFiltered) { + scanFilterList = List.of(new ScanFilter.Builder().setDeviceName("TestName").build()); + } + return scanFilterList; + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanControllerTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanControllerTest.java index 2b19c1aa0b..ea051bc528 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanControllerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanControllerTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.bluetooth.BluetoothAdapter; @@ -47,7 +48,6 @@ import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; @@ -55,6 +55,9 @@ import com.android.bluetooth.btservice.CompanionManager; import com.android.bluetooth.gatt.GattNativeInterface; import com.android.bluetooth.gatt.GattObjectsFactory; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -73,7 +76,7 @@ import java.util.Set; /** Test cases for {@link ScanController}. */ @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(TestParameterInjector.class) public class ScanControllerTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -187,7 +190,8 @@ public class ScanControllerTest { } @Test - public void onBatchScanReportsInternal_deliverBatchScan() throws RemoteException { + public void onBatchScanReportsInternal_deliverBatchScan_full( + @TestParameter boolean expectResults) throws RemoteException { int status = 1; int scannerId = 2; int reportType = ScanManager.SCAN_RESULT_TYPE_FULL; @@ -200,28 +204,59 @@ public class ScanControllerTest { Set<ScanClient> scanClientSet = new HashSet<>(); ScanClient scanClient = new ScanClient(scannerId); scanClient.associatedDevices = new ArrayList<>(); - scanClient.associatedDevices.add("02:00:00:00:00:00"); scanClient.scannerId = scannerId; + if (expectResults) { + scanClient.hasScanWithoutLocationPermission = true; + } scanClientSet.add(scanClient); doReturn(scanClientSet).when(mScanManager).getFullBatchScanQueue(); doReturn(mApp).when(mScannerMap).getById(scanClient.scannerId); + IScannerCallback callback = mock(IScannerCallback.class); + mApp.mCallback = callback; mScanController.onBatchScanReportsInternal( status, scannerId, reportType, numRecords, recordData); verify(mScanManager).callbackDone(scannerId, status); + if (expectResults) { + verify(callback).onBatchScanResults(any()); + } else { + verify(callback, never()).onBatchScanResults(any()); + } + } - reportType = ScanManager.SCAN_RESULT_TYPE_TRUNCATED; - recordData = + @Test + public void onBatchScanReportsInternal_deliverBatchScan_truncated( + @TestParameter boolean expectResults) throws RemoteException { + int status = 1; + int scannerId = 2; + int reportType = ScanManager.SCAN_RESULT_TYPE_TRUNCATED; + int numRecords = 1; + byte[] recordData = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x04, 0x02, 0x02, 0x00, 0x00, 0x02 }; + + Set<ScanClient> scanClientSet = new HashSet<>(); + ScanClient scanClient = new ScanClient(scannerId); + scanClient.associatedDevices = new ArrayList<>(); + if (expectResults) { + scanClient.associatedDevices.add("02:00:00:00:00:00"); + } + scanClient.scannerId = scannerId; + scanClientSet.add(scanClient); doReturn(scanClientSet).when(mScanManager).getBatchScanQueue(); + doReturn(mApp).when(mScannerMap).getById(scanClient.scannerId); IScannerCallback callback = mock(IScannerCallback.class); mApp.mCallback = callback; mScanController.onBatchScanReportsInternal( status, scannerId, reportType, numRecords, recordData); - verify(callback).onBatchScanResults(any()); + verify(mScanManager).callbackDone(scannerId, status); + if (expectResults) { + verify(callback).onBatchScanResults(any()); + } else { + verify(callback, never()).onBatchScanResults(any()); + } } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java index 25a27ce876..041a982a9b 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java @@ -1045,7 +1045,7 @@ public class ScanManagerTest { assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client); assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client); assertThat(mScanManager.getBatchScanQueue()).contains(client); - assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode); + assertThat(mScanManager.getBatchScanParams().mScanMode).isEqualTo(expectedScanMode); } } @@ -1075,13 +1075,13 @@ public class ScanManagerTest { sendMessageWaitForProcessed(createStartStopScanMessage(true, client)); assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client); assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client); - assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode); + assertThat(mScanManager.getBatchScanParams().mScanMode).isEqualTo(expectedScanMode); // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client); assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client); assertThat(mScanManager.getBatchScanQueue()).contains(client); - assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode); + assertThat(mScanManager.getBatchScanParams().mScanMode).isEqualTo(expectedScanMode); } } @@ -1152,7 +1152,7 @@ public class ScanManagerTest { assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client); assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client); assertThat(mScanManager.getBatchScanQueue()).contains(client); - assertThat(mScanManager.getBatchScanParams().scanMode) + assertThat(mScanManager.getBatchScanParams().mScanMode) .isEqualTo(expectedScanMode); // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); @@ -1166,7 +1166,7 @@ public class ScanManagerTest { assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client); assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client); assertThat(mScanManager.getBatchScanQueue()).contains(client); - assertThat(mScanManager.getBatchScanParams().scanMode) + assertThat(mScanManager.getBatchScanParams().mScanMode) .isEqualTo(expectedScanMode); }); } diff --git a/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceTest.java index 7f519f2f01..35c7e125a5 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceTest.java @@ -34,7 +34,6 @@ import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -88,7 +87,7 @@ public class McpServiceTest { public void testGetService() { McpService mMcpServiceDuplicate = McpService.getMcpService(); assertThat(mMcpServiceDuplicate).isNotNull(); - Assert.assertSame(mMcpServiceDuplicate, mMcpService); + assertThat(mMcpServiceDuplicate).isSameInstanceAs(mMcpService); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlProfileTest.java b/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlProfileTest.java index 8c134f1980..6dede08e29 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlProfileTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlProfileTest.java @@ -41,7 +41,6 @@ import com.android.bluetooth.audio_util.Metadata; import com.android.bluetooth.btservice.AdapterService; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -212,9 +211,8 @@ public class MediaControlProfileTest { mMockMediaData.state = bob.build(); doReturn(mMockMediaData.state).when(mMockMediaPlayerWrapper).getPlaybackState(); - Assert.assertNotEquals( - mMcpServiceCallbacks.onGetCurrentTrackPosition(), - MediaControlGattServiceInterface.TRACK_POSITION_UNAVAILABLE); + assertThat(mMcpServiceCallbacks.onGetCurrentTrackPosition()) + .isNotEqualTo(MediaControlGattServiceInterface.TRACK_POSITION_UNAVAILABLE); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java index 4aca68e62e..a959a4a764 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java @@ -17,7 +17,6 @@ package com.android.bluetooth.tbs; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -40,7 +39,6 @@ import com.android.bluetooth.btservice.AdapterService; import com.google.common.primitives.Bytes; -import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -62,9 +60,18 @@ import java.util.UUID; @MediumTest @RunWith(AndroidJUnit4.class) public class TbsGattTest { - private BluetoothAdapter mAdapter; - private BluetoothDevice mFirstDevice; - private BluetoothDevice mSecondDevice; + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule(); + + @Mock private AdapterService mAdapterService; + @Mock private BluetoothGattServerProxy mGattServer; + @Mock private TbsGatt.Callback mCallback; + @Mock private TbsService mService; + @Captor private ArgumentCaptor<BluetoothGattService> mGattServiceCaptor; + + private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter(); + private final BluetoothDevice mFirstDevice = TestUtils.getTestDevice(mAdapter, 0); + private final BluetoothDevice mSecondDevice = TestUtils.getTestDevice(mAdapter, 1); private Integer mCurrentCcid; private String mCurrentUci; @@ -74,50 +81,19 @@ public class TbsGattTest { private TbsGatt mTbsGatt; - @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); - - @Mock private AdapterService mAdapterService; - @Mock private BluetoothGattServerProxy mMockGattServer; - @Mock private TbsGatt.Callback mMockTbsGattCallback; - @Mock private TbsService mMockTbsService; - - @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule(); - - @Captor private ArgumentCaptor<BluetoothGattService> mGattServiceCaptor; - @Before - public void setUp() throws Exception { + public void setUp() { if (Looper.myLooper() == null) { Looper.prepare(); } - getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(); - - TestUtils.setAdapterService(mAdapterService); - mAdapter = BluetoothAdapter.getDefaultAdapter(); - - doReturn(true).when(mMockGattServer).addService(any(BluetoothGattService.class)); - doReturn(true).when(mMockGattServer).open(any(BluetoothGattServerCallback.class)); + doReturn(true).when(mGattServer).addService(any(BluetoothGattService.class)); + doReturn(true).when(mGattServer).open(any(BluetoothGattServerCallback.class)); doReturn(BluetoothDevice.ACCESS_ALLOWED) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); - mTbsGatt = new TbsGatt(mMockTbsService); - mTbsGatt.setBluetoothGattServerForTesting(mMockGattServer); - - mFirstDevice = TestUtils.getTestDevice(mAdapter, 0); - mSecondDevice = TestUtils.getTestDevice(mAdapter, 1); - - when(mMockTbsService.getDeviceAuthorization(any(BluetoothDevice.class))) - .thenReturn(BluetoothDevice.ACCESS_ALLOWED); - } - - @After - public void tearDown() throws Exception { - mFirstDevice = null; - mSecondDevice = null; - mTbsGatt = null; - TestUtils.clearAdapterService(mAdapterService); + mTbsGatt = new TbsGatt(mAdapterService, mService, mGattServer); } private void prepareDefaultService() { @@ -136,12 +112,12 @@ public class TbsGattTest { true, mCurrentProviderName, mCurrentTechnology, - mMockTbsGattCallback)) + mCallback)) .isTrue(); verify(mAdapterService).registerBluetoothStateCallback(any(), any()); - verify(mMockGattServer).addService(mGattServiceCaptor.capture()); - doReturn(mGattServiceCaptor.getValue()).when(mMockGattServer).getService(any(UUID.class)); + verify(mGattServer).addService(mGattServiceCaptor.capture()); + doReturn(mGattServiceCaptor.getValue()).when(mGattServer).getService(any(UUID.class)); } private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { @@ -168,9 +144,9 @@ public class TbsGattTest { enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); - verify(mMockGattServer) + verify(mGattServer) .sendResponse(eq(device), eq(1), eq(BluetoothGatt.GATT_SUCCESS), eq(0), any()); - reset(mMockGattServer); + reset(mGattServer); } private void verifySetValue( @@ -288,26 +264,26 @@ public class TbsGattTest { if (shouldNotify) { if (notifyWithValue) { - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged( eq(device), eq(characteristic), eq(false), any()); } else { - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged(eq(device), eq(characteristic), eq(false)); } } else { if (notifyWithValue) { - verify(mMockGattServer, times(0)) + verify(mGattServer, never()) .notifyCharacteristicChanged( eq(device), eq(characteristic), anyBoolean(), any()); } else { - verify(mMockGattServer, times(0)) + verify(mGattServer, never()) .notifyCharacteristicChanged(eq(device), eq(characteristic), anyBoolean()); } } if (clearGattMock) { - reset(mMockGattServer); + reset(mGattServer); } } @@ -575,9 +551,9 @@ public class TbsGattTest { .asList() .containsExactly(requestedOpcode, callIndex, result) .inOrder(); - verify(mMockGattServer, after(2000)) + verify(mGattServer, after(2000)) .notifyCharacteristicChanged(eq(mFirstDevice), eq(characteristic), eq(false)); - reset(mMockGattServer); + reset(mGattServer); callIndex = 0x02; @@ -588,7 +564,7 @@ public class TbsGattTest { .asList() .containsExactly(requestedOpcode, callIndex, result) .inOrder(); - verify(mMockGattServer, after(2000).times(0)) + verify(mGattServer, after(2000).never()) .notifyCharacteristicChanged(any(), any(), anyBoolean()); } @@ -695,7 +671,7 @@ public class TbsGattTest { mTbsGatt.mGattServerCallback.onCharacteristicWriteRequest( mFirstDevice, 1, characteristic, false, true, 0, value); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), @@ -704,7 +680,7 @@ public class TbsGattTest { aryEq(new byte[] {0x00, 0x0A})); // Verify the higher layer callback call - verify(mMockTbsGattCallback) + verify(mCallback) .onCallControlPointRequest(eq(mFirstDevice), eq(0x00), aryEq(new byte[] {0x0A})); } @@ -719,7 +695,7 @@ public class TbsGattTest { mTbsGatt.mGattServerCallback.onCharacteristicWriteRequest( mFirstDevice, 1, characteristic, false, true, 0, value); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), @@ -746,15 +722,15 @@ public class TbsGattTest { mTbsGatt.setInbandRingtoneFlag(mFirstDevice); mTbsGatt.setInbandRingtoneFlag(mFirstDevice); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged( eq(mFirstDevice), eq(characteristic), eq(false), eq(valueBytes)); - reset(mMockGattServer); + reset(mGattServer); mTbsGatt.setInbandRingtoneFlag(mSecondDevice); mTbsGatt.setInbandRingtoneFlag(mSecondDevice); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged( eq(mSecondDevice), eq(characteristic), eq(false), eq(valueBytes)); } @@ -773,18 +749,18 @@ public class TbsGattTest { valueBytes[1] = (byte) ((statusFlagValue >> 8) & 0xFF); mTbsGatt.setInbandRingtoneFlag(mFirstDevice); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged( eq(mFirstDevice), eq(characteristic), eq(false), eq(valueBytes)); - reset(mMockGattServer); + reset(mGattServer); mTbsGatt.setInbandRingtoneFlag(mSecondDevice); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged( eq(mSecondDevice), eq(characteristic), eq(false), eq(valueBytes)); - reset(mMockGattServer); + reset(mGattServer); // clear flag statusFlagValue = 0; @@ -793,14 +769,14 @@ public class TbsGattTest { mTbsGatt.clearInbandRingtoneFlag(mFirstDevice); mTbsGatt.clearInbandRingtoneFlag(mFirstDevice); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged( eq(mFirstDevice), eq(characteristic), eq(false), eq(valueBytes)); - reset(mMockGattServer); + reset(mGattServer); mTbsGatt.clearInbandRingtoneFlag(mSecondDevice); mTbsGatt.clearInbandRingtoneFlag(mSecondDevice); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged( eq(mSecondDevice), eq(characteristic), eq(false), eq(valueBytes)); } @@ -822,10 +798,10 @@ public class TbsGattTest { mTbsGatt.setSilentModeFlag(); mTbsGatt.setSilentModeFlag(); mTbsGatt.setSilentModeFlag(); - verify(mMockGattServer, times(2)) + verify(mGattServer, times(2)) .notifyCharacteristicChanged(any(), eq(characteristic), eq(false), eq(valueBytes)); - reset(mMockGattServer); + reset(mGattServer); statusFlagValue = TbsGatt.STATUS_FLAG_INBAND_RINGTONE_ENABLED @@ -835,17 +811,17 @@ public class TbsGattTest { mTbsGatt.setInbandRingtoneFlag(mFirstDevice); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged( eq(mFirstDevice), eq(characteristic), eq(false), eq(valueBytes)); - reset(mMockGattServer); + reset(mGattServer); mTbsGatt.setInbandRingtoneFlag(mSecondDevice); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged( eq(mSecondDevice), eq(characteristic), eq(false), eq(valueBytes)); - reset(mMockGattServer); + reset(mGattServer); statusFlagValue = TbsGatt.STATUS_FLAG_INBAND_RINGTONE_ENABLED; valueBytes[0] = (byte) (statusFlagValue & 0xFF); @@ -855,7 +831,7 @@ public class TbsGattTest { mTbsGatt.clearSilentModeFlag(); mTbsGatt.clearSilentModeFlag(); mTbsGatt.clearSilentModeFlag(); - verify(mMockGattServer, times(2)) + verify(mGattServer, times(2)) .notifyCharacteristicChanged(any(), eq(characteristic), eq(false), eq(valueBytes)); } @@ -867,7 +843,7 @@ public class TbsGattTest { mTbsGatt.mGattServerCallback.onCharacteristicReadRequest( mFirstDevice, 1, 0, characteristic); // Verify the higher layer callback call - verify(mMockTbsGattCallback).isInbandRingtoneEnabled(eq(mFirstDevice)); + verify(mCallback).isInbandRingtoneEnabled(eq(mFirstDevice)); } @Test @@ -881,31 +857,31 @@ public class TbsGattTest { // Check with no configuration mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), eq(BluetoothGatt.GATT_SUCCESS), eq(0), eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)); - reset(mMockGattServer); + reset(mGattServer); // Check with notifications enabled configureNotifications(mFirstDevice, characteristic, true); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), eq(BluetoothGatt.GATT_SUCCESS), eq(0), eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)); - reset(mMockGattServer); + reset(mGattServer); // Check with notifications disabled configureNotifications(mFirstDevice, characteristic, false); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), @@ -925,7 +901,7 @@ public class TbsGattTest { // Check with no configuration mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), @@ -934,79 +910,79 @@ public class TbsGattTest { eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mSecondDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mSecondDevice), eq(1), eq(BluetoothGatt.GATT_SUCCESS), eq(0), eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)); - reset(mMockGattServer); + reset(mGattServer); // Check with notifications enabled for first device configureNotifications(mFirstDevice, characteristic, true); verifySetValue(characteristic, 4, true, mFirstDevice, true); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), eq(BluetoothGatt.GATT_SUCCESS), eq(0), eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)); - reset(mMockGattServer); + reset(mGattServer); // Check if second device is still not subscribed for notifications and will not get it verifySetValue(characteristic, 5, false, mSecondDevice, false); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged(eq(mFirstDevice), eq(characteristic), eq(false)); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mSecondDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mSecondDevice), eq(1), eq(BluetoothGatt.GATT_SUCCESS), eq(0), eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)); - reset(mMockGattServer); + reset(mGattServer); // Check with notifications enabled for first and second device configureNotifications(mSecondDevice, characteristic, true); verifySetValue(characteristic, 6, true, mSecondDevice, false); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged(eq(mFirstDevice), eq(characteristic), eq(false)); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mSecondDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mSecondDevice), eq(1), eq(BluetoothGatt.GATT_SUCCESS), eq(0), eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)); - reset(mMockGattServer); + reset(mGattServer); // Disable notification for first device, check if second will get notification configureNotifications(mFirstDevice, characteristic, false); verifySetValue(characteristic, 7, false, mFirstDevice, false); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged(eq(mSecondDevice), eq(characteristic), eq(false)); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), eq(BluetoothGatt.GATT_SUCCESS), eq(0), eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)); - reset(mMockGattServer); + reset(mGattServer); // Check with notifications disabled of both device configureNotifications(mSecondDevice, characteristic, false); verifySetValue(characteristic, 4, false, mFirstDevice, false); - verify(mMockGattServer, times(0)) + verify(mGattServer, never()) .notifyCharacteristicChanged(eq(mSecondDevice), eq(characteristic), eq(false)); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mSecondDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mSecondDevice), eq(1), @@ -1023,13 +999,13 @@ public class TbsGattTest { getCharacteristic(TbsGatt.UUID_BEARER_TECHNOLOGY); doReturn(BluetoothDevice.ACCESS_REJECTED) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); mTbsGatt.mGattServerCallback.onCharacteristicReadRequest( mFirstDevice, 1, 0, characteristic); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), @@ -1047,12 +1023,12 @@ public class TbsGattTest { configureNotifications(mFirstDevice, characteristic, true); configureNotifications(mSecondDevice, characteristic, true); - doReturn(mGattServiceCaptor.getValue()).when(mMockGattServer).getService(any(UUID.class)); + doReturn(mGattServiceCaptor.getValue()).when(mGattServer).getService(any(UUID.class)); assertThat(mGattServiceCaptor.getValue()).isNotNull(); // Leave it as unauthorized yet doReturn(BluetoothDevice.ACCESS_REJECTED) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); int statusFlagValue = TbsGatt.STATUS_FLAG_SILENT_MODE_ENABLED; @@ -1068,18 +1044,18 @@ public class TbsGattTest { valueBytes[0] = (byte) (statusFlagValue & 0xFF); valueBytes[1] = (byte) ((statusFlagValue >> 8) & 0xFF); mTbsGatt.setSilentModeFlag(); - verify(mMockGattServer, times(0)) + verify(mGattServer, never()) .notifyCharacteristicChanged(any(), eq(characteristic), eq(false), eq(valueBytes)); // Expect a single notification for the just authorized device doReturn(BluetoothDevice.ACCESS_ALLOWED) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); assertThat(mGattServiceCaptor.getValue()).isNotNull(); mTbsGatt.onDeviceAuthorizationSet(mFirstDevice); - verify(mMockGattServer, times(0)) + verify(mGattServer, never()) .notifyCharacteristicChanged(any(), eq(characteristic2), eq(false)); - verify(mMockGattServer) + verify(mGattServer) .notifyCharacteristicChanged(any(), eq(characteristic), eq(false), eq(valueBytes)); } @@ -1091,13 +1067,13 @@ public class TbsGattTest { getCharacteristic(TbsGatt.UUID_BEARER_TECHNOLOGY); doReturn(BluetoothDevice.ACCESS_UNKNOWN) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); mTbsGatt.mGattServerCallback.onCharacteristicReadRequest( mFirstDevice, 1, 0, characteristic); - verify(mMockTbsService, times(0)).onDeviceUnauthorized(eq(mFirstDevice)); + verify(mService, never()).onDeviceUnauthorized(eq(mFirstDevice)); } @Test @@ -1108,7 +1084,7 @@ public class TbsGattTest { getCharacteristic(TbsGatt.UUID_CALL_CONTROL_POINT); doReturn(BluetoothDevice.ACCESS_REJECTED) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); byte[] value = @@ -1119,7 +1095,7 @@ public class TbsGattTest { mTbsGatt.mGattServerCallback.onCharacteristicWriteRequest( mFirstDevice, 1, characteristic, false, true, 0, value); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), @@ -1136,7 +1112,7 @@ public class TbsGattTest { getCharacteristic(TbsGatt.UUID_CALL_CONTROL_POINT); doReturn(BluetoothDevice.ACCESS_UNKNOWN) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); byte[] value = @@ -1147,7 +1123,7 @@ public class TbsGattTest { mTbsGatt.mGattServerCallback.onCharacteristicWriteRequest( mFirstDevice, 1, characteristic, false, true, 0, value); - verify(mMockTbsService).onDeviceUnauthorized(eq(mFirstDevice)); + verify(mService).onDeviceUnauthorized(eq(mFirstDevice)); } @Test @@ -1160,12 +1136,12 @@ public class TbsGattTest { assertThat(descriptor).isNotNull(); doReturn(BluetoothDevice.ACCESS_REJECTED) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), @@ -1184,12 +1160,12 @@ public class TbsGattTest { assertThat(descriptor).isNotNull(); doReturn(BluetoothDevice.ACCESS_UNKNOWN) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor); - verify(mMockTbsService, times(0)).onDeviceUnauthorized(eq(mFirstDevice)); + verify(mService, never()).onDeviceUnauthorized(eq(mFirstDevice)); } @Test @@ -1202,7 +1178,7 @@ public class TbsGattTest { assertThat(descriptor).isNotNull(); doReturn(BluetoothDevice.ACCESS_REJECTED) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); byte[] value = @@ -1213,7 +1189,7 @@ public class TbsGattTest { mTbsGatt.mGattServerCallback.onDescriptorWriteRequest( mFirstDevice, 1, descriptor, false, true, 0, value); - verify(mMockGattServer) + verify(mGattServer) .sendResponse( eq(mFirstDevice), eq(1), @@ -1232,7 +1208,7 @@ public class TbsGattTest { assertThat(descriptor).isNotNull(); doReturn(BluetoothDevice.ACCESS_UNKNOWN) - .when(mMockTbsService) + .when(mService) .getDeviceAuthorization(any(BluetoothDevice.class)); byte[] value = @@ -1243,6 +1219,6 @@ public class TbsGattTest { mTbsGatt.mGattServerCallback.onDescriptorWriteRequest( mFirstDevice, 1, descriptor, false, true, 0, value); - verify(mMockTbsService, times(0)).onDeviceUnauthorized(eq(mFirstDevice)); + verify(mService, never()).onDeviceUnauthorized(eq(mFirstDevice)); } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java index d10db3e3e5..345dd8a5ee 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java @@ -33,7 +33,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.TestUtils; import com.android.bluetooth.le_audio.LeAudioService; -import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -46,6 +45,7 @@ import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.UUID; @@ -53,33 +53,27 @@ import java.util.UUID; @MediumTest @RunWith(AndroidJUnit4.class) public class TbsGenericTest { - private BluetoothAdapter mAdapter; - private BluetoothDevice mCurrentDevice; - - private TbsGeneric mTbsGeneric; - @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock private TbsGatt mTbsGatt; + @Mock private IBluetoothLeCallControlCallback mIBluetoothLeCallControlCallback; + @Captor private ArgumentCaptor<Integer> mGtbsCcidCaptor; + @Captor private ArgumentCaptor<String> mGtbsUciCaptor; - private @Mock TbsGatt mTbsGatt; - private @Mock IBluetoothLeCallControlCallback mIBluetoothLeCallControlCallback; - private @Captor ArgumentCaptor<Integer> mGtbsCcidCaptor; - private @Captor ArgumentCaptor<String> mGtbsUciCaptor; - private @Captor ArgumentCaptor<List> mDefaultGtbsUriSchemesCaptor = - ArgumentCaptor.forClass(List.class); - private @Captor ArgumentCaptor<String> mDefaultGtbsProviderNameCaptor; - private @Captor ArgumentCaptor<Integer> mDefaultGtbsTechnologyCaptor; + @Captor + private ArgumentCaptor<List> mDefaultGtbsUriSchemesCaptor = ArgumentCaptor.forClass(List.class); - private @Captor ArgumentCaptor<TbsGatt.Callback> mTbsGattCallback; - private static Context mContext; - - @Before - public void setUp() throws Exception { + @Captor private ArgumentCaptor<String> mDefaultGtbsProviderNameCaptor; + @Captor private ArgumentCaptor<Integer> mDefaultGtbsTechnologyCaptor; + @Captor private ArgumentCaptor<TbsGatt.Callback> mTbsGattCallback; - mAdapter = BluetoothAdapter.getDefaultAdapter(); - mContext = getInstrumentation().getTargetContext(); + private final Context mContext = getInstrumentation().getTargetContext(); + private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter(); + private final BluetoothDevice mDevice = TestUtils.getTestDevice(mAdapter, 32); - getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(); + private TbsGeneric mTbsGeneric; + @Before + public void setUp() { // Default TbsGatt mock behavior doReturn(true) .when(mTbsGatt) @@ -106,15 +100,8 @@ public class TbsGenericTest { doReturn(true).when(mTbsGatt).clearIncomingCall(); doReturn(true).when(mTbsGatt).setCallFriendlyName(anyInt(), anyString()); doReturn(true).when(mTbsGatt).clearFriendlyName(); - doReturn(mContext).when(mTbsGatt).getContext(); - - mTbsGeneric = new TbsGeneric(); - mTbsGeneric.init(mTbsGatt); - } - @After - public void tearDown() throws Exception { - mTbsGeneric = null; + mTbsGeneric = new TbsGeneric(mContext, mTbsGatt); } private Integer prepareTestBearer() { @@ -150,14 +137,13 @@ public class TbsGenericTest { @Test public void testSetClearInbandRingtone() { - mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0); prepareTestBearer(); - mTbsGeneric.setInbandRingtoneSupport(mCurrentDevice); - verify(mTbsGatt).setInbandRingtoneFlag(mCurrentDevice); + mTbsGeneric.setInbandRingtoneSupport(mDevice); + verify(mTbsGatt).setInbandRingtoneFlag(mDevice); - mTbsGeneric.clearInbandRingtoneSupport(mCurrentDevice); - verify(mTbsGatt).clearInbandRingtoneFlag(mCurrentDevice); + mTbsGeneric.clearInbandRingtoneSupport(mDevice); + verify(mTbsGatt).clearInbandRingtoneFlag(mDevice); } @Test @@ -362,7 +348,6 @@ public class TbsGenericTest { @Test public void testCallAccept() { - mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0); Integer ccid = prepareTestBearer(); reset(mTbsGatt); @@ -392,8 +377,7 @@ public class TbsGenericTest { args[0] = (byte) (callIndex & 0xFF); mTbsGattCallback .getValue() - .onCallControlPointRequest( - mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT, args); + .onCallControlPointRequest(mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT, args); ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class); @@ -405,7 +389,7 @@ public class TbsGenericTest { } assertThat(callUuidCaptor.getValue().getUuid()).isEqualTo(callUuid); // Active device should be changed - verify(leAudioService).setActiveDevice(mCurrentDevice); + verify(leAudioService).setActiveDevice(mDevice); // Respond with requestComplete... mTbsGeneric.requestResult( @@ -415,7 +399,7 @@ public class TbsGenericTest { // ..and verify if GTBS control point is updated to notifier the peer about the result verify(mTbsGatt) .setCallControlPointResult( - eq(mCurrentDevice), + eq(mDevice), eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT), eq(callIndex), eq(BluetoothLeCallControl.RESULT_SUCCESS)); @@ -423,7 +407,6 @@ public class TbsGenericTest { @Test public void testCallTerminate() { - mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0); Integer ccid = prepareTestBearer(); reset(mTbsGatt); @@ -451,7 +434,7 @@ public class TbsGenericTest { mTbsGattCallback .getValue() .onCallControlPointRequest( - mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE, args); + mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE, args); ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class); @@ -471,7 +454,7 @@ public class TbsGenericTest { // ..and verify if GTBS control point is updated to notifier the peer about the result verify(mTbsGatt) .setCallControlPointResult( - eq(mCurrentDevice), + eq(mDevice), eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE), eq(callIndex), eq(BluetoothLeCallControl.RESULT_SUCCESS)); @@ -479,7 +462,6 @@ public class TbsGenericTest { @Test public void testCallHold() { - mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0); Integer ccid = prepareTestBearer(); reset(mTbsGatt); @@ -507,7 +489,7 @@ public class TbsGenericTest { mTbsGattCallback .getValue() .onCallControlPointRequest( - mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD, args); + mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD, args); ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class); @@ -527,7 +509,7 @@ public class TbsGenericTest { // ..and verify if GTBS control point is updated to notifier the peer about the result verify(mTbsGatt) .setCallControlPointResult( - eq(mCurrentDevice), + eq(mDevice), eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD), eq(callIndex), eq(BluetoothLeCallControl.RESULT_SUCCESS)); @@ -535,7 +517,6 @@ public class TbsGenericTest { @Test public void testCallRetrieve() { - mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0); Integer ccid = prepareTestBearer(); reset(mTbsGatt); @@ -563,7 +544,7 @@ public class TbsGenericTest { mTbsGattCallback .getValue() .onCallControlPointRequest( - mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE, args); + mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE, args); ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class); @@ -583,7 +564,7 @@ public class TbsGenericTest { // ..and verify if GTBS control point is updated to notifier the peer about the result verify(mTbsGatt) .setCallControlPointResult( - eq(mCurrentDevice), + eq(mDevice), eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE), eq(callIndex), eq(BluetoothLeCallControl.RESULT_SUCCESS)); @@ -591,7 +572,6 @@ public class TbsGenericTest { @Test public void testCallOriginate() { - mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0); Integer ccid = prepareTestBearer(); reset(mTbsGatt); @@ -603,9 +583,7 @@ public class TbsGenericTest { mTbsGattCallback .getValue() .onCallControlPointRequest( - mCurrentDevice, - TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE, - uri.getBytes()); + mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE, uri.getBytes()); ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class); @@ -617,7 +595,7 @@ public class TbsGenericTest { } // Active device should be changed - verify(leAudioService).setActiveDevice(mCurrentDevice); + verify(leAudioService).setActiveDevice(mDevice); // Respond with requestComplete... mTbsGeneric.requestResult( @@ -634,7 +612,7 @@ public class TbsGenericTest { // ..and verify if GTBS control point is updated to notifier the peer about the result verify(mTbsGatt) .setCallControlPointResult( - eq(mCurrentDevice), + eq(mDevice), eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE), anyInt(), eq(BluetoothLeCallControl.RESULT_SUCCESS)); @@ -642,7 +620,6 @@ public class TbsGenericTest { @Test public void testCallJoin() { - mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0); Integer ccid = prepareTestBearer(); reset(mTbsGatt); @@ -678,8 +655,7 @@ public class TbsGenericTest { } mTbsGattCallback .getValue() - .onCallControlPointRequest( - mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN, args); + .onCallControlPointRequest(mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN, args); ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<List<ParcelUuid>> callUuidCaptor = ArgumentCaptor.forClass(List.class); @@ -703,9 +679,85 @@ public class TbsGenericTest { // ..and verify if GTBS control point is updated to notifier the peer about the result verify(mTbsGatt) .setCallControlPointResult( - eq(mCurrentDevice), + eq(mDevice), eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN), anyInt(), eq(BluetoothLeCallControl.RESULT_SUCCESS)); } + + @Test + public void testCallOperationsBlockedForBroadcastReceiver() { + Integer ccid = prepareTestBearer(); + reset(mTbsGatt); + + LeAudioService leAudioService = mock(LeAudioService.class); + mTbsGeneric.setLeAudioServiceForTesting(leAudioService); + + // Prepare the incoming call + UUID callUuid = UUID.randomUUID(); + List<BluetoothLeCall> tbsCalls = new ArrayList<>(); + tbsCalls.add( + new BluetoothLeCall( + callUuid, + "tel:987654321", + "aFriendlyCaller", + BluetoothLeCall.STATE_INCOMING, + 0)); + mTbsGeneric.currentCallsList(ccid, tbsCalls); + + ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class); + verify(mTbsGatt).setCallState(currentCallsCaptor.capture()); + Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue(); + assertThat(capturedCurrentCalls.size()).isEqualTo(1); + Integer callIndex = capturedCurrentCalls.entrySet().iterator().next().getKey(); + reset(mTbsGatt); + + doReturn(new HashSet<>(Arrays.asList(mDevice))) + .when(leAudioService) + .getLocalBroadcastReceivers(); + + doReturn(false).when(leAudioService).isPrimaryDevice(mDevice); + + // Verify call accept + byte args[] = new byte[1]; + args[0] = (byte) (callIndex & 0xFF); + mTbsGattCallback + .getValue() + .onCallControlPointRequest( + mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT, args); + + // Active device should not be changed + verify(leAudioService, never()).setActiveDevice(mDevice); + // Verify if GTBS control point is updated to notify the peer about the result + verify(mTbsGatt) + .setCallControlPointResult( + eq(mDevice), + eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT), + eq(0), + eq(TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE)); + + // Verify call terminate + tbsCalls.clear(); + tbsCalls.add( + new BluetoothLeCall( + callUuid, + "tel:987654321", + "aFriendlyCaller", + BluetoothLeCall.STATE_ACTIVE, + 0)); + mTbsGeneric.currentCallsList(ccid, tbsCalls); + + mTbsGattCallback + .getValue() + .onCallControlPointRequest( + mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE, args); + + // Verify if GTBS control point is updated to notify the peer about the result + verify(mTbsGatt) + .setCallControlPointResult( + eq(mDevice), + eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE), + eq(0), + eq(TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE)); + } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java index 69d4407b23..94e1db83b1 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java @@ -77,7 +77,6 @@ import com.android.bluetooth.le_audio.LeAudioService; import org.hamcrest.Matcher; import org.hamcrest.core.AllOf; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -1125,13 +1124,13 @@ public class VolumeControlServiceTest { mBinder.setDeviceVolume(mDevice, deviceOneVolume, false, mAttributionSource); inOrderNative.verify(mNativeInterface).setVolume(mDevice, deviceOneVolume); assertThat(mService.getDeviceVolume(mDevice)).isEqualTo(deviceOneVolume); - Assert.assertNotEquals(deviceOneVolume, mService.getDeviceVolume(mDeviceTwo)); + assertThat(mService.getDeviceVolume(mDeviceTwo)).isNotEqualTo(deviceOneVolume); inOrderNative.verify(mNativeInterface, never()).setGroupVolume(anyInt(), anyInt()); mBinder.setDeviceVolume(mDeviceTwo, deviceTwoVolume, false, mAttributionSource); inOrderNative.verify(mNativeInterface).setVolume(mDeviceTwo, deviceTwoVolume); assertThat(mService.getDeviceVolume(mDeviceTwo)).isEqualTo(deviceTwoVolume); - Assert.assertNotEquals(deviceTwoVolume, mService.getDeviceVolume(mDevice)); + assertThat(mService.getDeviceVolume(mDevice)).isNotEqualTo(deviceTwoVolume); inOrderNative.verify(mNativeInterface, never()).setGroupVolume(anyInt(), anyInt()); } diff --git a/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py b/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py index 42ef33f68b..5f68da54f7 100644 --- a/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py +++ b/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py @@ -92,6 +92,7 @@ class Dongle(enum.Enum): DEFAULT = "default" LAIRD_BL654 = "laird_bl654" CSR_RCK_PTS_DONGLE = "csr_rck_pts_dongle" + INTEL_BE200 = "intel_be200" class RootCanal: diff --git a/android/pandora/mmi2grpc/mmi2grpc/avrcp.py b/android/pandora/mmi2grpc/mmi2grpc/avrcp.py index 9fbc55d56c..c64eb2113f 100644 --- a/android/pandora/mmi2grpc/mmi2grpc/avrcp.py +++ b/android/pandora/mmi2grpc/mmi2grpc/avrcp.py @@ -117,6 +117,7 @@ class AVRCPProxy(ProfileProxy): Action: Make sure the IUT is in a connectable state. """ + self.mediaplayer.ResetQueue() return "OK" @assert_description @@ -1080,3 +1081,73 @@ class AVRCPProxy(ProfileProxy): """ return "OK" + + @assert_description + def TSC_OBEX_MMI_iut_accept_slc_connect_l2cap(self, **kwargs): + """ + Please accept the l2cap channel connection for an OBEX connection. + """ + + return "OK" + + @assert_description + def TSC_OBEX_MMI_iut_accept_connect(self, **kwargs): + """ + Please accept the OBEX CONNECT REQ. + """ + + return "OK" + + + @assert_description + def TSC_AVRCP_mmi_user_queue_cover_art_element(self, **kwargs): + """ + Take action to play a media element with cover art. Press 'Ok' when + ready. + """ + self.mediaplayer.Play() + + return "OK" + + @assert_description + def TSC_AVRCP_mmi_iut_reject_invalid_get_img(self, **kwargs): + """ + Take action to reject the invalid 'get-img' request sent by the tester. + """ + + return "OK" + + @assert_description + def TSC_BIP_MMI_iut_accept_get_img_properties(self, **kwargs): + """ + Take action to accept the GetImgProperties operation from the tester. + """ + + return "OK" + + @assert_description + def TSC_BIP_MMI_iut_accept_get_img(self, **kwargs): + """ + Take action to accept the GetImg operation from the tester. + """ + + return "OK" + + @assert_description + def TSC_OBEX_MMI_tester_verify_sent_file_or_folder(self, **kwargs): + """ + Was the currently displayed file or folder sent by the IUT? + """ + + return "OK" + + @assert_description + def TSC_AVRCP_mmi_user_queue_no_cover_art_element(self, **kwargs): + """ + Take action to play a media element that does not have any cover art + with it. Press 'Ok' when ready. + """ + self.mediaplayer.UpdateQueue() + self.mediaplayer.PlayUpdated() + + return "OK" diff --git a/android/pandora/server/configs/pts_bot_tests_config.json b/android/pandora/server/configs/pts_bot_tests_config.json index 758d21f07c..9a80a7471e 100644 --- a/android/pandora/server/configs/pts_bot_tests_config.json +++ b/android/pandora/server/configs/pts_bot_tests_config.json @@ -129,6 +129,11 @@ "AVDTP/SRC/INT/SIG/SMG/BV-31-C", "AVDTP/SRC/INT/SIG/SYN/BV-05-C", "AVDTP/SRC/INT/TRA/BTR/BV-01-C", + "AVRCP/TG/CA/BI-03-C", + "AVRCP/TG/CA/BI-05-C", + "AVRCP/TG/CA/BI-07-C", + "AVRCP/TG/CA/BI-10-C", + "AVRCP/TG/CA/BV-16-C", "AVRCP/CT/CEC/BV-02-I", "AVRCP/CT/CRC/BV-02-I", "AVRCP/TG/CEC/BV-01-I", @@ -769,6 +774,21 @@ "AVDTP/SRC/INT/SIG/SMG/BV-23-C", "AVDTP/SRC/INT/SIG/SMG/BV-33-C", "AVDTP/SRC/INT/SIG/SMG/ESR05/BV-13-C", + "AVRCP/TG/CA/BI-01-C", + "AVRCP/TG/CA/BI-04-C", + "AVRCP/TG/CA/BI-06-C", + "AVRCP/TG/CA/BI-08-C", + "AVRCP/TG/CA/BI-09-C", + "AVRCP/TG/CA/BV-01-I", + "AVRCP/TG/CA/BV-02-C", + "AVRCP/TG/CA/BV-02-I", + "AVRCP/TG/CA/BV-03-I", + "AVRCP/TG/CA/BV-04-C", + "AVRCP/TG/CA/BV-06-C", + "AVRCP/TG/CA/BV-08-C", + "AVRCP/TG/CA/BV-10-C", + "AVRCP/TG/CA/BV-12-C", + "AVRCP/TG/CA/BV-14-C", "AVRCP/CT/CEC/BV-01-I", "AVRCP/CT/CRC/BV-01-I", "AVRCP/CT/PTH/BV-01-C", @@ -1566,6 +1586,7 @@ "TSPC_AVRCP_7_64": true, "TSPC_AVRCP_7_65": true, "TSPC_AVRCP_7_66": true, + "TSPC_AVRCP_7_67": true, "TSPC_AVRCP_7b_4": true, "TSPC_AVRCP_8_19": true, "TSPC_AVRCP_8_20": true, @@ -3210,12 +3231,25 @@ "SM": {}, "SPP": {}, "SUM ICS": {}, - "VCP": { - } + "VCP": {} }, "flags": [ { "flags": [ + "set_addressed_player", + "browsing_refactor", + "avrcp_16_default" + ], + "tests": [ + "AVRCP/TG/CA/BI-03-C", + "AVRCP/TG/CA/BI-05-C", + "AVRCP/TG/CA/BI-07-C", + "AVRCP/TG/CA/BI-10-C", + "AVRCP/TG/CA/BV-16-C" + ] + }, + { + "flags": [ "leaudio_allow_leaudio_only_devices", "enable_hap_by_default" ], @@ -3252,6 +3286,69 @@ "VCP/VC/SPE/BI-19-C", "VCP/VC/SPE/BI-20-C" ] + }, + { + "flags": [ + "avdt_accept_open_timeout_ms" + ], + "tests": [ + "A2DP/SNK/SYN/BV-01-C" + ] + } + ], + "system_properties": [ + { + "system_properties": { + "bluetooth.profile.a2dp.sink.enabled": "true", + "bluetooth.profile.a2dp.source.enabled": "false" + }, + "tests": [ + "A2DP/SNK", + "AVCTP/CT", + "AVDTP/SNK", + "AVRCP/CT/CEC", + "AVRCP/CT/CRC", + "AVRCP/CT/PTH", + "AVRCP/CT/PTT" + ] + }, + { + "system_properties": { + "bluetooth.profile.a2dp.sink.enabled": "false", + "bluetooth.profile.a2dp.source.enabled": "true" + }, + "tests": [ + "A2DP/SRC", + "AVCTP/TG", + "AVDTP/SRC", + "AVRCP/TG" + ] + }, + { + "system_properties": { + "bluetooth.profile.hfp.hf.enabled": "true", + "bluetooth.profile.hfp.ag.enabled": "false" + }, + "tests": [ + "HFP/HF" + ] + }, + { + "system_properties": { + "bluetooth.profile.hfp.hf.enabled": "false", + "bluetooth.profile.hfp.ag.enabled": "true" + }, + "tests": [ + "HFP/AG" + ] + }, + { + "system_properties": { + "bluetooth.a2dp.avdt_accept_open_timeout_ms": "15000" + }, + "tests": [ + "A2DP/SNK/SYN/BV-01-C" + ] } ] } diff --git a/android/pandora/server/configs/pts_bot_tests_config_auto.json b/android/pandora/server/configs/pts_bot_tests_config_auto.json index abc188046c..425b79c8e8 100644 --- a/android/pandora/server/configs/pts_bot_tests_config_auto.json +++ b/android/pandora/server/configs/pts_bot_tests_config_auto.json @@ -375,5 +375,67 @@ "SPP": {}, "VCP": {} }, - "flags": {} + "flags": { + "flags": [ + "avdt_accept_open_timeout_ms" + ], + "tests": [ + "A2DP/SNK/SYN/BV-01-C" + ] + }, + "system_properties": [ + { + "system_properties": { + "bluetooth.profile.a2dp.sink.enabled": "true", + "bluetooth.profile.a2dp.source.enabled": "false" + }, + "tests": [ + "A2DP/SNK", + "AVCTP/CT", + "AVDTP/SNK", + "AVRCP/CT/CEC", + "AVRCP/CT/CRC", + "AVRCP/CT/PTH", + "AVRCP/CT/PTT" + ] + }, + { + "system_properties": { + "bluetooth.profile.a2dp.sink.enabled": "false", + "bluetooth.profile.a2dp.source.enabled": "true" + }, + "tests": [ + "A2DP/SRC", + "AVCTP/TG", + "AVDTP/SRC", + "AVRCP/TG" + ] + }, + { + "system_properties": { + "bluetooth.profile.hfp.hf.enabled": "true", + "bluetooth.profile.hfp.ag.enabled": "false" + }, + "tests": [ + "HFP/HF" + ] + }, + { + "system_properties": { + "bluetooth.profile.hfp.hf.enabled": "false", + "bluetooth.profile.hfp.ag.enabled": "true" + }, + "tests": [ + "HFP/AG" + ] + }, + { + "system_properties": { + "bluetooth.a2dp.avdt_accept_open_timeout_ms": "15000" + }, + "tests": [ + "A2DP/SNK/SYN/BV-01-C" + ] + } + ] } diff --git a/android/pandora/server/src/A2dp.kt b/android/pandora/server/src/A2dp.kt index 6708dd1c46..0b5a0d6640 100644 --- a/android/pandora/server/src/A2dp.kt +++ b/android/pandora/server/src/A2dp.kt @@ -41,7 +41,6 @@ import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter @@ -110,10 +109,6 @@ class A2dp(val context: Context) : A2DPImplBase(), Closeable { } } - // TODO: b/234891800, AVDTP start request sometimes never sent if playback starts too - // early. - delay(2000L) - val source = Source.newBuilder().setCookie(ByteString.copyFrom(device.getAddress(), "UTF-8")) OpenSourceResponse.newBuilder().setSource(source).build() @@ -147,10 +142,6 @@ class A2dp(val context: Context) : A2DPImplBase(), Closeable { } } - // TODO: b/234891800, AVDTP start request sometimes never sent if playback starts too - // early. - delay(2000L) - val source = Source.newBuilder().setCookie(ByteString.copyFrom(device.getAddress(), "UTF-8")) WaitSourceResponse.newBuilder().setSource(source).build() diff --git a/android/pandora/server/src/MediaPlayer.kt b/android/pandora/server/src/MediaPlayer.kt index 81d6a8a78a..a2c981f881 100644 --- a/android/pandora/server/src/MediaPlayer.kt +++ b/android/pandora/server/src/MediaPlayer.kt @@ -55,6 +55,13 @@ class MediaPlayer(val context: Context) : MediaPlayerImplBase(), Closeable { } } + override fun playUpdated(request: Empty, responseObserver: StreamObserver<Empty>) { + grpcUnary<Empty>(scope, responseObserver) { + MediaPlayerBrowserService.instance.playUpdated() + Empty.getDefaultInstance() + } + } + override fun stop(request: Empty, responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(scope, responseObserver) { MediaPlayerBrowserService.instance.stop() @@ -111,6 +118,13 @@ class MediaPlayer(val context: Context) : MediaPlayerImplBase(), Closeable { } } + override fun resetQueue(request: Empty, responseObserver: StreamObserver<Empty>) { + grpcUnary<Empty>(scope, responseObserver) { + MediaPlayerBrowserService.instance.resetQueue() + Empty.getDefaultInstance() + } + } + override fun getShuffleMode( request: Empty, responseObserver: StreamObserver<GetShuffleModeResponse> diff --git a/android/pandora/server/src/MediaPlayerBrowserService.kt b/android/pandora/server/src/MediaPlayerBrowserService.kt index b7a358f79c..eaab8a9bbe 100644 --- a/android/pandora/server/src/MediaPlayerBrowserService.kt +++ b/android/pandora/server/src/MediaPlayerBrowserService.kt @@ -17,6 +17,7 @@ package com.android.pandora import android.content.Intent +import android.graphics.Bitmap import android.media.MediaPlayer import android.os.Bundle import android.support.v4.media.* @@ -43,6 +44,7 @@ class MediaPlayerBrowserService : MediaBrowserServiceCompat() { private var metadataItems = mutableMapOf<String, MediaMetadataCompat>() private var queue = mutableListOf<MediaSessionCompat.QueueItem>() private var currentTrack = -1 + private val testIcon = Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888) override fun onCreate() { super.onCreate() @@ -114,6 +116,12 @@ class MediaPlayerBrowserService : MediaBrowserServiceCompat() { mediaSession.setMetadata(metadataItems.get("" + currentTrack)) } + fun playUpdated() { + currentTrack = NEW_QUEUE_ITEM_INDEX + setPlaybackState(PlaybackStateCompat.STATE_PLAYING) + mediaSession.setMetadata(metadataItems.get("" + currentTrack)) + } + fun stop() { setPlaybackState(PlaybackStateCompat.STATE_STOPPED) mediaSession.setMetadata(null) @@ -170,12 +178,23 @@ class MediaPlayerBrowserService : MediaBrowserServiceCompat() { .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, NEW_QUEUE_ITEM_INDEX.toLong()) .build() val mediaItem = MediaItem(metaData.description, MediaItem.FLAG_PLAYABLE) + metadataItems.put("" + NEW_QUEUE_ITEM_INDEX, metaData) queue.add( MediaSessionCompat.QueueItem(mediaItem.description, NEW_QUEUE_ITEM_INDEX.toLong()) ) mediaSession.setQueue(queue) } + fun resetQueue() { + if (metadataItems.contains("" + NEW_QUEUE_ITEM_INDEX)) { + metadataItems.remove("" + NEW_QUEUE_ITEM_INDEX) + queue.removeLast() + mediaSession.setQueue(queue) + stop() + currentTrack = QUEUE_START_INDEX + } + } + fun getShuffleMode(): Int { val controller = mediaSession.getController() return controller.getShuffleMode() @@ -261,6 +280,7 @@ class MediaPlayerBrowserService : MediaBrowserServiceCompat() { ) .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, item.toLong()) .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, QUEUE_SIZE.toLong()) + .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, testIcon) .build() val mediaItem = MediaItem(metaData.description, MediaItem.FLAG_PLAYABLE) mediaItems.add(mediaItem) diff --git a/android/pandora/test/a2dp/signaling_channel.py b/android/pandora/test/a2dp/signaling_channel.py new file mode 100644 index 0000000000..0fbda414c8 --- /dev/null +++ b/android/pandora/test/a2dp/signaling_channel.py @@ -0,0 +1,200 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# https://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. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +from __future__ import annotations + +from bumble.device import Connection +try: + from packets import avdtp as avdt_packet_module + from packets.avdtp import * +except ImportError: + from .packets import avdtp as avdt_packet_module + from .packets.avdtp import * +from pyee import EventEmitter +from typing import Union + +import asyncio +import bumble.avdtp as avdtp +import bumble.l2cap as l2cap +import logging + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + +avdt_packet_module.print = lambda *args, **kwargs: logger.debug(" ".join(map(str, args))) + + +class Any: + """Helper class that will match all other values. + Use an element of this class in expected packets to match any value + returned by the AVDTP signaling.""" + + def __eq__(self, other) -> bool: + return True + + def __format__(self, format_spec: str) -> str: + return "_" + + def __len__(self) -> int: + return 1 + + def show(self, prefix: str = "") -> str: + return prefix + "_" + + +class SignalingChannel(EventEmitter): + connection: Connection + signaling_channel: Optional[l2cap.ClassicChannel] = None + transport_channel: Optional[l2cap.ClassicChannel] = None + avdtp_server: Optional[l2cap.ClassicChannelServer] = None + role: Optional[str] = None + + def __init__(self, connection: Connection): + super().__init__() + self.connection = connection + self.signaling_queue = asyncio.Queue() + self.transport_queue = asyncio.Queue() + + @classmethod + async def initiate(cls, connection: Connection) -> SignalingChannel: + channel = cls(connection) + await channel._initiate_signaling_channel() + return channel + + @classmethod + def accept(cls, connection: Connection) -> SignalingChannel: + channel = cls(connection) + channel._accept_signaling_channel() + return channel + + async def disconnect(self): + if not self.signaling_channel: + raise ValueError("No connected signaling channel") + await self.signaling_channel.disconnect() + self.signaling_channel = None + + async def initiate_transport_channel(self): + if self.transport_channel: + raise ValueError("RTP L2CAP channel already exists") + self.transport_channel = await self.connection.create_l2cap_channel( + l2cap.ClassicChannelSpec(psm=avdtp.AVDTP_PSM)) + + async def disconnect_transport_channel(self): + if not self.transport_channel: + raise ValueError("No connected RTP channel") + await self.transport_channel.disconnect() + self.transport_channel = None + + async def expect_signal(self, expected_sig: Union[SignalingPacket, type], timeout: float = 3) -> SignalingPacket: + packet = await asyncio.wait_for(self.signaling_queue.get(), timeout=timeout) + sig = SignalingPacket.parse_all(packet) + + if isinstance(expected_sig, type) and not isinstance(sig, expected_sig): + logger.error("Received unexpected signal") + logger.error(f"Expected signal: {expected_sig.__class__.__name__}") + logger.error("Received signal:") + sig.show() + raise ValueError(f"Received unexpected signal") + + if isinstance(expected_sig, SignalingPacket) and sig != expected_sig: + logger.error("Received unexpected signal") + logger.error("Expected signal:") + expected_sig.show() + logger.error("Received signal:") + sig.show() + raise ValueError(f"Received unexpected signal") + + logger.debug(f"<<< {self.connection.self_address} {self.role} received signal: <<<") + sig.show() + return sig + + async def expect_media(self, timeout: float = 3.0) -> bytes: + packet = await asyncio.wait_for(self.transport_queue.get(), timeout=timeout) + logger.debug(f"<<< {self.connection.self_address} {self.role} received media <<<") + logger.debug(f"RTP Packet: {packet.hex()}") + return packet + + def send_signal(self, packet: SignalingPacket): + logger.debug(f">>> {self.connection.self_address} {self.role} sending signal: >>>") + packet.show() + self.signaling_channel.send_pdu(packet.serialize()) + + def send_media(self, packet: bytes): + logger.debug(f">>> {self.connection.self_address} {self.role} sending media >>>") + self.transport_channel.send_pdu(packet) + + async def _initiate_signaling_channel(self): + if self.signaling_channel: + raise ValueError("Signaling L2CAP channel already exists") + self.role = "initiator" + self.signaling_channel = await self.connection.create_l2cap_channel(spec=l2cap.ClassicChannelSpec( + psm=avdtp.AVDTP_PSM)) + # Register to receive PDUs from the channel + self.signaling_channel.sink = self._on_pdu + + def _accept_signaling_channel(self): + if self.avdtp_server: + raise ValueError("L2CAP server already exists") + self.role = "acceptor" + avdtp_server = self.connection.device.l2cap_channel_manager.servers.get(avdtp.AVDTP_PSM) + if not avdtp_server: + self.avdtp_server = self.connection.device.create_l2cap_server(spec=l2cap.ClassicChannelSpec( + psm=avdtp.AVDTP_PSM)) + else: + self.avdtp_server = avdtp_server + self.avdtp_server.on('connection', self._on_l2cap_connection) + + def _on_l2cap_connection(self, channel: l2cap.ClassicChannel): + logger.info(f"Incoming L2CAP channel: {channel}") + + if not self.signaling_channel: + + def _on_channel_open(): + logger.info(f"Signaling opened on channel {self.signaling_channel}") + # Register to receive PDUs from the channel + self.signaling_channel.sink = self._on_pdu + self.emit('connection') + + def _on_channel_close(): + logger.info("Signaling channel closed") + self.signaling_channel = None + + self.signaling_channel = channel + self.signaling_channel.on('open', _on_channel_open) + self.signaling_channel.on('close', _on_channel_close) + elif not self.transport_channel: + + def _on_channel_open(): + logger.info(f"RTP opened on channel {self.transport_channel}") + # Register to receive PDUs from the channel + self.transport_channel.sink = self._on_avdtp_packet + + def _on_channel_close(): + logger.info('RTP channel closed') + self.transport_channel = None + + self.transport_channel = channel + self.transport_channel.on('open', _on_channel_open) + self.transport_channel.on('close', _on_channel_close) + + def _on_pdu(self, pdu: bytes): + self.signaling_queue.put_nowait(pdu) + + def _on_avdtp_packet(self, packet): + self.transport_queue.put_nowait(packet) diff --git a/android/pandora/test/a2dp/signaling_channel_test.py b/android/pandora/test/a2dp/signaling_channel_test.py new file mode 100644 index 0000000000..daf5c8f40b --- /dev/null +++ b/android/pandora/test/a2dp/signaling_channel_test.py @@ -0,0 +1,378 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# https://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. +""" +Bumble tests for SignalingChannel implementation. + +Create venv and upgrade pip: + + python -m venv .venv + source .venv/bin/activate + python -m pip install --upgrade pip + +Install the required dependencies using pip: + + pip install pyee pytest bumble + +Run the tests: + python /path/signaling_channel_test.py +""" + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging +import os +import pytest +import bumble.avdtp as avdtp +from bumble.a2dp import (A2DP_SBC_CODEC_TYPE, SbcMediaCodecInformation) +from bumble.controller import Controller +from bumble.core import BT_BR_EDR_TRANSPORT +from bumble.device import Device +from bumble.host import Host +from bumble.link import LocalLink +from bumble.transport import AsyncPipeSink +from packets.avdtp import * +from signaling_channel import SignalingChannel, Any + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +class TwoDevices: + + def __init__(self): + self.connections = [None, None] + + addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0'] + self.link = LocalLink() + self.controllers = [ + Controller('C1', link=self.link, public_address=addresses[0]), + Controller('C2', link=self.link, public_address=addresses[1]), + ] + self.devices = [ + Device( + address=addresses[0], + host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])), + ), + Device( + address=addresses[1], + host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])), + ), + ] + + self.paired = [None, None] + + def on_connection(self, which, connection): + self.connections[which] = connection + + def on_paired(self, which, keys): + self.paired[which] = keys + + +# ----------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_self_connection(): + # Create two devices, each with a controller, attached to the same link + two_devices = TwoDevices() + + # Attach listeners + two_devices.devices[0].on('connection', lambda connection: two_devices.on_connection(0, connection)) + two_devices.devices[1].on('connection', lambda connection: two_devices.on_connection(1, connection)) + + # Enable Classic connections + two_devices.devices[0].classic_enabled = True + two_devices.devices[1].classic_enabled = True + + # Start + await two_devices.devices[0].power_on() + await two_devices.devices[1].power_on() + + # Connect the two devices + await asyncio.gather( + two_devices.devices[0].connect(two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT), + two_devices.devices[1].accept(two_devices.devices[0].public_address), + ) + + # Check the post conditions + assert two_devices.connections[0] is not None + assert two_devices.connections[1] is not None + + +# ----------------------------------------------------------------------------- +def sink_codec_capabilities(): + return avdtp.MediaCodecCapabilities( + media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE, + media_codec_type=A2DP_SBC_CODEC_TYPE, + media_codec_information=SbcMediaCodecInformation.from_bytes(bytes([255, 255, 2, 53])), + ) + + +# ----------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_signaling_channel_as_source(): + any = Any() + + two_devices = TwoDevices() + # Enable Classic connections + two_devices.devices[0].classic_enabled = True + two_devices.devices[1].classic_enabled = True + await two_devices.devices[0].power_on() + await two_devices.devices[1].power_on() + + def on_rtp_packet(packet): + rtp_packets.append(packet) + if len(rtp_packets) == rtp_packets_expected: + rtp_packets_fully_received.set_result(None) + + device_1_avdt_sink = None + avdtp_future = asyncio.get_running_loop().create_future() + + def on_avdtp_connection(server): + logger.info("AVDTP Opened") + nonlocal device_1_avdt_sink + device_1_avdt_sink = server.add_sink(sink_codec_capabilities()) + device_1_avdt_sink.on('rtp_packet', on_rtp_packet) + nonlocal avdtp_future + avdtp_future.set_result(None) + + # Create a listener to wait for AVDTP connections + listener = avdtp.Listener.for_device(two_devices.devices[1]) + listener.on('connection', on_avdtp_connection) + + async def make_connection(): + connections = await asyncio.gather( + two_devices.devices[0].connect(two_devices.devices[1].public_address, BT_BR_EDR_TRANSPORT), + two_devices.devices[1].accept(two_devices.devices[0].public_address), + ) + return connections[0] + + connection = await make_connection() + + channel_int = await SignalingChannel.initiate(connection) + + channel_int.send_signal(DiscoverCommand()) + + result = await channel_int.expect_signal( + DiscoverResponse(transaction_label=any, seid_information=[SeidInformation(acp_seid=1, tsep=Tsep.SINK)])) + + acp_seid = result.seid_information[0].acp_seid + + channel_int.send_signal(GetAllCapabilitiesCommand(acp_seid=acp_seid)) + + result = await channel_int.expect_signal( + GetAllCapabilitiesResponse(transaction_label=any, + service_capabilities=[ + MediaTransportCapability(), + MediaCodecCapability(service_category=ServiceCategory.MEDIA_CODEC, + media_codec_specific_information_elements=[255, 255, 2, 53]) + ])) + + channel_int.send_signal( + SetConfigurationCommand(acp_seid=acp_seid, service_capabilities=[result.service_capabilities[0]])) + + await channel_int.expect_signal(SetConfigurationResponse(transaction_label=any)) + + channel_int.send_signal(OpenCommand(acp_seid=acp_seid)) + + await channel_int.expect_signal(OpenResponse(transaction_label=any)) + + await asyncio.wait_for(avdtp_future, timeout=10.0) + + assert device_1_avdt_sink.in_use == 1 + assert device_1_avdt_sink.stream is not None + assert device_1_avdt_sink.stream.state == avdtp.AVDTP_OPEN_STATE + + async def generate_packets(packet_count): + sequence_number = 0 + timestamp = 0 + for i in range(packet_count): + payload = bytes([sequence_number % 256]) + packet = avdtp.MediaPacket(2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, payload) + packet.timestamp_seconds = timestamp / 44100 + timestamp += 10 + sequence_number += 1 + yield packet + + # # Send packets using a pump object + rtp_packets_fully_received = asyncio.get_running_loop().create_future() + rtp_packets_expected = 3 + rtp_packets = [] + pump = avdtp.MediaPacketPump(generate_packets(3)) + + await channel_int.initiate_transport_channel() + + channel_int.send_signal(StartCommand(acp_seid=acp_seid)) + + await channel_int.expect_signal(StartResponse(transaction_label=any)) + + assert device_1_avdt_sink.in_use == 1 + assert device_1_avdt_sink.stream is not None + assert device_1_avdt_sink.stream.state == avdtp.AVDTP_STREAMING_STATE + + await pump.start(channel_int.transport_channel) + + await rtp_packets_fully_received + + await pump.stop() + + channel_int.send_signal(CloseCommand(acp_seid=acp_seid)) + + await channel_int.expect_signal(CloseResponse(transaction_label=any)) + + await channel_int.disconnect_transport_channel() + + assert device_1_avdt_sink.in_use == 0 + assert device_1_avdt_sink.stream.state == avdtp.AVDTP_IDLE_STATE + + +# ----------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_signaling_channel_as_sink(): + any = Any() + + two_devices = TwoDevices() + # Enable Classic connections + two_devices.devices[0].classic_enabled = True + two_devices.devices[1].classic_enabled = True + await two_devices.devices[0].power_on() + await two_devices.devices[1].power_on() + + dev_0_dev_1_conn, dev_1_dev_0_conn = await asyncio.gather( + two_devices.devices[0].connect(two_devices.devices[1].public_address, BT_BR_EDR_TRANSPORT), + two_devices.devices[1].accept(two_devices.devices[0].public_address), + ) + + channel_acp = SignalingChannel.accept(dev_1_dev_0_conn) + + avdtp_future = asyncio.get_running_loop().create_future() + + def on_avdtp_connection(): + logger.info(f" AVDTP Opened") + nonlocal avdtp_future + avdtp_future.set_result(None) + + channel_acp.on('connection', on_avdtp_connection) + + channel_int = await SignalingChannel.initiate(dev_0_dev_1_conn) + + channel_int.send_signal(DiscoverCommand()) + + cmd = await channel_acp.expect_signal(DiscoverCommand(transaction_label=any)) + + seid_information = [SeidInformation(tsep=Tsep.SINK, media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE)] + + channel_acp.send_signal(DiscoverResponse(transaction_label=cmd.transaction_label, + seid_information=seid_information)) + + result = await channel_int.expect_signal( + DiscoverResponse( + seid_information=[SeidInformation(acp_seid=0x0, tsep=Tsep.SINK, media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE)])) + + int_to_acp_seid = result.seid_information[0].acp_seid + + channel_int.send_signal(GetAllCapabilitiesCommand(acp_seid=int_to_acp_seid)) + + cmd = await channel_acp.expect_signal(GetAllCapabilitiesCommand(acp_seid=int_to_acp_seid, transaction_label=any)) + + acceptor_service_capabilities = [ + MediaTransportCapability(), + MediaCodecCapability(service_category=ServiceCategory.MEDIA_CODEC, + media_codec_specific_information_elements=[255, 255, 2, 53]) + ] + + channel_acp.send_signal( + GetAllCapabilitiesResponse(transaction_label=cmd.transaction_label, + service_capabilities=acceptor_service_capabilities)) + + result = await channel_int.expect_signal( + GetAllCapabilitiesResponse(transaction_label=any, + service_capabilities=[ + MediaTransportCapability(), + MediaCodecCapability(service_category=ServiceCategory.MEDIA_CODEC, + media_codec_specific_information_elements=[255, 255, 2, 53]) + ])) + + channel_int.send_signal( + SetConfigurationCommand(acp_seid=int_to_acp_seid, service_capabilities=[result.service_capabilities[0]])) + + cmd = await channel_acp.expect_signal( + SetConfigurationCommand(transaction_label=any, + acp_seid=int_to_acp_seid, + service_capabilities=[result.service_capabilities[0]])) + + channel_acp.send_signal(SetConfigurationResponse(transaction_label=cmd.transaction_label)) + + await channel_int.expect_signal(SetConfigurationResponse(transaction_label=any)) + + channel_int.send_signal(OpenCommand(acp_seid=int_to_acp_seid)) + + cmd = await channel_acp.expect_signal(OpenCommand(transaction_label=any, acp_seid=int_to_acp_seid)) + + channel_acp.send_signal(OpenResponse(transaction_label=cmd.transaction_label)) + + await channel_int.expect_signal(OpenResponse(transaction_label=any)) + + await asyncio.wait_for(avdtp_future, timeout=10.0) + + rtp_packets_expected = 3 + received_rtp_packets = [] + source_packets = [ + avdtp.MediaPacket(2, 0, 0, 0, i, i * 10, 0, [], 96, bytes([i])) for i in range(rtp_packets_expected) + ] + + await channel_int.initiate_transport_channel() + + channel_int.send_signal(StartCommand(acp_seid=int_to_acp_seid)) + + cmd = await channel_acp.expect_signal(StartCommand(transaction_label=any, acp_seid=int_to_acp_seid)) + + channel_acp.send_signal(StartResponse(transaction_label=cmd.transaction_label)) + + await channel_int.expect_signal(StartResponse(transaction_label=any)) + + channel_int.send_media(bytes(source_packets[0])) + channel_int.send_media(bytes(source_packets[1])) + channel_int.send_media(bytes(source_packets[2])) + + for _ in range(rtp_packets_expected): + received_rtp_packets.append(await channel_acp.expect_media()) + assert channel_acp.transport_queue.empty() + + channel_int.send_signal(CloseCommand(acp_seid=int_to_acp_seid)) + + cmd = await channel_acp.expect_signal(CloseCommand(transaction_label=any, acp_seid=int_to_acp_seid)) + + channel_acp.send_signal(CloseResponse(transaction_label=cmd.transaction_label)) + + await channel_int.expect_signal(CloseResponse(transaction_label=any)) + + await channel_int.disconnect_transport_channel() + await channel_int.disconnect() + + +# ----------------------------------------------------------------------------- +async def run_test_self(): + await test_self_connection() + await test_signaling_channel_as_source() + await test_signaling_channel_as_sink() + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper()) + asyncio.run(run_test_self()) diff --git a/android/pandora/test/a2dp_test.py b/android/pandora/test/a2dp_test.py index 8404c28200..a1efc015c3 100644 --- a/android/pandora/test/a2dp_test.py +++ b/android/pandora/test/a2dp_test.py @@ -20,7 +20,8 @@ import itertools import logging import numpy as np -from a2dp.packets import avdtp +from a2dp.packets.avdtp import * +from a2dp.signaling_channel import Any, SignalingChannel from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices, pandora_snippet, enableFlag from bumble.a2dp import ( A2DP_MPEG_2_4_AAC_CODEC_TYPE, @@ -40,8 +41,6 @@ from bumble.avctp import AVCTP_PSM from bumble.avdtp import ( AVDTP_AUDIO_MEDIA_TYPE, AVDTP_BAD_STATE_ERROR, - AVDTP_CLOSING_STATE, - AVDTP_IDLE_STATE, AVDTP_OPEN_STATE, AVDTP_PSM, AVDTP_STREAMING_STATE, @@ -292,6 +291,94 @@ class A2dpTest(base_test.BaseTestClass): # type: ignore[misc] assert_equal(self.ref1.a2dp_sink.stream.state, AVDTP_OPEN_STATE) @avatar.asynchronous + async def test_signaling_channel_and_streaming(self) -> None: + """Basic A2DP connection and streaming with SignalingChannel used by acceptor device test. + + 1. Pair and Connect RD1 + 2. Setup the acceptor expectations on signalling channel + 2. Start streaming + 4. Stop streaming + """ + any = Any() + + # Connect and pair RD1. + dut_ref1, ref1_dut = await asyncio.gather( + initiate_pairing(self.dut, self.ref1.address), + accept_pairing(self.ref1, self.dut.address), + ) + + connection = pandora_snippet.get_raw_connection(device=self.ref1, connection=ref1_dut) + channel = SignalingChannel.accept(connection) + + async def acceptor_avdt_open(channel: SignalingChannel): + + avdtp_future = asyncio.get_running_loop().create_future() + + def on_avdtp_connection(): + logger.info(f"AVDTP Opened") + nonlocal avdtp_future + avdtp_future.set_result(None) + + channel.on('connection', on_avdtp_connection) + + cmd = await channel.expect_signal(DiscoverCommand(transaction_label=any)) + + seid_information = [SeidInformation(acp_seid=0x01, tsep=Tsep.SINK, media_type=AVDTP_AUDIO_MEDIA_TYPE)] + + channel.send_signal( + DiscoverResponse(transaction_label=cmd.transaction_label, seid_information=seid_information)) + + cmd = await channel.expect_signal(GetAllCapabilitiesCommand(acp_seid=any, transaction_label=any)) + + acceptor_service_capabilities = [ + MediaTransportCapability(), + MediaCodecCapability(service_category=ServiceCategory.MEDIA_CODEC, + media_codec_specific_information_elements=[255, 255, 2, 53]) + ] + + channel.send_signal( + GetAllCapabilitiesResponse(transaction_label=cmd.transaction_label, + service_capabilities=acceptor_service_capabilities)) + + cmd = await channel.expect_signal( + SetConfigurationCommand(transaction_label=any, + acp_seid=any, + int_seid=any, + service_capabilities=[MediaTransportCapability(), any])) + + channel.send_signal(SetConfigurationResponse(transaction_label=cmd.transaction_label)) + + cmd = await channel.expect_signal(OpenCommand(transaction_label=any, acp_seid=any)) + + channel.send_signal(OpenResponse(transaction_label=cmd.transaction_label)) + + await asyncio.wait_for(avdtp_future, timeout=10.0) + + # Connect AVDTP to RD1. + _, dut_ref1_source = await asyncio.gather(acceptor_avdt_open(channel), open_source(self.dut, dut_ref1)) + + async def acceptor_avdt_start(channel: SignalingChannel): + cmd = await channel.expect_signal(StartCommand(transaction_label=any, acp_seid=any)) + + channel.send_signal(StartResponse(transaction_label=cmd.transaction_label)) + + # Start streaming to RD1. + await asyncio.gather(self.dut.a2dp.Start(source=dut_ref1_source), acceptor_avdt_start(channel)) + + audio = AudioSignal(self.dut.a2dp, dut_ref1_source, 0.8, 44100) + + # Verify that at least one audio frame is received on the transport channel. + await asyncio.wait_for(channel.expect_media(), 5.0) + + async def acceptor_avdt_suspend(channel: SignalingChannel): + cmd = await channel.expect_signal(SuspendCommand(transaction_label=any, acp_seid=any)) + + channel.send_signal(SuspendResponse(transaction_label=cmd.transaction_label)) + + # Stop streaming to RD1. + await asyncio.gather(self.dut.a2dp.Suspend(source=dut_ref1_source), acceptor_avdt_suspend(channel)) + + @avatar.asynchronous async def test_avdtp_autoconnect_when_only_avctp_connected(self) -> None: """Test AVDTP automatically connects if peer device connects only AVCTP. diff --git a/flags/Android.bp b/flags/Android.bp index 8a1dc66552..80a57891a5 100644 --- a/flags/Android.bp +++ b/flags/Android.bp @@ -31,6 +31,7 @@ aconfig_declarations { "hid.aconfig", "l2cap.aconfig", "le_advertising.aconfig", + "le_scanning.aconfig", "leaudio.aconfig", "mapclient.aconfig", "mcp.aconfig", diff --git a/flags/BUILD.gn b/flags/BUILD.gn index 9f96d85933..da5aa8621b 100644 --- a/flags/BUILD.gn +++ b/flags/BUILD.gn @@ -24,6 +24,7 @@ aconfig("bluetooth_flags_c_lib") { "hid.aconfig", "l2cap.aconfig", "le_advertising.aconfig", + "le_scanning.aconfig", "leaudio.aconfig", "mapclient.aconfig", "mcp.aconfig", diff --git a/flags/active_device_manager.aconfig b/flags/active_device_manager.aconfig index 68ece5e73b..b2438be452 100644 --- a/flags/active_device_manager.aconfig +++ b/flags/active_device_manager.aconfig @@ -19,4 +19,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "adm_remove_handling_wired" + namespace: "bluetooth" + description: "ActiveDeviceManager doesn't need to handle adding and removing wired devices." + bug: "390372480" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/flags/bta_dm.aconfig b/flags/bta_dm.aconfig index d91f3168b7..287a0f0187 100644 --- a/flags/bta_dm.aconfig +++ b/flags/bta_dm.aconfig @@ -9,13 +9,6 @@ flag { } flag { - name: "bta_dm_discover_both" - namespace: "bluetooth" - description: "perform both LE and Classic service discovery simulteanously on capable devices" - bug: "339217881" -} - -flag { name: "cancel_open_discovery_client" namespace: "bluetooth" description: "Cancel connection from discovery client correctly" diff --git a/flags/framework.aconfig b/flags/framework.aconfig index 757148b7df..6464f91927 100644 --- a/flags/framework.aconfig +++ b/flags/framework.aconfig @@ -86,3 +86,13 @@ flag { description: "Make BluetoothDevice.ACTION_KEY_MISSING into public API" bug: "379729762" } + +flag { + name: "set_component_available_fix" + namespace: "bluetooth" + description: "Ensure the state in PackageManager has DISABLED to ENABLED to trigger PACKAGE_CHANGED" + bug: "391084450" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/flags/gap.aconfig b/flags/gap.aconfig index 9b139eead2..5da5144aa1 100644 --- a/flags/gap.aconfig +++ b/flags/gap.aconfig @@ -271,3 +271,23 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "fix_bluetooth_gatt_getting_duplicate_services" + namespace: "bluetooth" + description: "Fixes BluetoothGatt getting duplicate GATT services" + bug: "391773937" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "batch_scan_optimization" + namespace: "bluetooth" + description: "Optimized batch scan for less wakeups" + bug: "392132489" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/flags/gatt.aconfig b/flags/gatt.aconfig index 1b9bfb516e..e054fb3f7a 100644 --- a/flags/gatt.aconfig +++ b/flags/gatt.aconfig @@ -18,3 +18,14 @@ flag { bug: "384794418" is_exported: true } + +flag { + name: "advertise_thread" + namespace: "bluetooth" + description: "Run all advertise functions on a single thread" + bug: "391508617" + metadata { + purpose: PURPOSE_BUGFIX + } +} + diff --git a/flags/hfp.aconfig b/flags/hfp.aconfig index 7b81b483b4..026c3b22b3 100644 --- a/flags/hfp.aconfig +++ b/flags/hfp.aconfig @@ -100,16 +100,6 @@ flag { } flag { - name: "hfp_allow_volume_change_without_sco" - namespace: "bluetooth" - description: "Allow Audio Fwk to change SCO volume when HFP profile is connected and SCO not connected" - bug: "362313390" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "choose_wrong_hfp_codec_in_specific_config" namespace: "bluetooth" description: "Flag to fix codec selection in nego when the peer device only support NB and SWB." diff --git a/flags/le_scanning.aconfig b/flags/le_scanning.aconfig new file mode 100644 index 0000000000..0b4985e45e --- /dev/null +++ b/flags/le_scanning.aconfig @@ -0,0 +1,12 @@ +package: "com.android.bluetooth.flags" +container: "com.android.bt" + +flag { + name: "scan_results_in_main_thread" + namespace: "bluetooth" + description: "Use main thread for handling scan results" + bug: "392693506" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/flags/leaudio.aconfig b/flags/leaudio.aconfig index e7b0b2abcf..03cc9a4c02 100644 --- a/flags/leaudio.aconfig +++ b/flags/leaudio.aconfig @@ -451,3 +451,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "leaudio_disable_broadcast_for_hap_device" + namespace: "bluetooth" + description: "Disable broadcast feature for HAP device" + bug: "391702876" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/flags/security.aconfig b/flags/security.aconfig index ddfd78611c..006c51f307 100644 --- a/flags/security.aconfig +++ b/flags/security.aconfig @@ -9,6 +9,16 @@ flag { } flag { + name: "key_missing_ble_peripheral" + namespace: "bluetooth" + description: "Key missing broadcast for LE devices in peripheral role" + bug: "392895615" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "key_missing_as_ordered_broadcast" namespace: "bluetooth" description: "Key missing broadcast would be send as ordered broadcast" diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt index 5a506f21fb..60657262e3 100644 --- a/framework/api/system-current.txt +++ b/framework/api/system-current.txt @@ -53,7 +53,7 @@ package android.bluetooth { method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void disableOptionalCodecs(@NonNull android.bluetooth.BluetoothDevice); method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void enableOptionalCodecs(@NonNull android.bluetooth.BluetoothDevice); method @Nullable @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public android.bluetooth.BufferConstraints getBufferConstraints(); - method @Nullable @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public android.bluetooth.BluetoothCodecStatus getCodecStatus(@NonNull android.bluetooth.BluetoothDevice); + method @Nullable @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}, conditional=true) public android.bluetooth.BluetoothCodecStatus getCodecStatus(@NonNull android.bluetooth.BluetoothDevice); method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int getConnectionPolicy(@NonNull android.bluetooth.BluetoothDevice); method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int getDynamicBufferSupport(); method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int isOptionalCodecsEnabled(@NonNull android.bluetooth.BluetoothDevice); diff --git a/framework/java/android/bluetooth/BluetoothA2dp.java b/framework/java/android/bluetooth/BluetoothA2dp.java index 5bcd0789ab..54f2c702aa 100644 --- a/framework/java/android/bluetooth/BluetoothA2dp.java +++ b/framework/java/android/bluetooth/BluetoothA2dp.java @@ -724,20 +724,23 @@ public final class BluetoothA2dp implements BluetoothProfile { /** * Gets the current codec status (configuration and capability). * + * <p>This method requires the calling app to have the {@link + * android.Manifest.permission#BLUETOOTH_CONNECT} permission. Additionally, an app must either + * have the {@link android.Manifest.permission#BLUETOOTH_PRIVILEGED} or be associated with the + * Companion Device manager (see {@link android.companion.CompanionDeviceManager#associate( + * AssociationRequest, android.companion.CompanionDeviceManager.Callback, Handler)}) + * * @param device the remote Bluetooth device. * @return the current codec status * @hide */ @SystemApi - @Nullable @RequiresLegacyBluetoothPermission @RequiresBluetoothConnectPermission @RequiresPermission( - allOf = { - BLUETOOTH_CONNECT, - BLUETOOTH_PRIVILEGED, - }) - public BluetoothCodecStatus getCodecStatus(@NonNull BluetoothDevice device) { + allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}, + conditional = true) + public @Nullable BluetoothCodecStatus getCodecStatus(@NonNull BluetoothDevice device) { if (DBG) Log.d(TAG, "getCodecStatus(" + device + ")"); verifyDeviceNotNull(device, "getCodecStatus"); final IBluetoothA2dp service = getService(); diff --git a/framework/java/android/bluetooth/BluetoothGatt.java b/framework/java/android/bluetooth/BluetoothGatt.java index e67d823c7e..a69c8ba792 100644 --- a/framework/java/android/bluetooth/BluetoothGatt.java +++ b/framework/java/android/bluetooth/BluetoothGatt.java @@ -37,6 +37,8 @@ import android.os.ParcelUuid; import android.os.RemoteException; import android.util.Log; +import com.android.bluetooth.flags.Flags; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -243,6 +245,9 @@ public final class BluetoothGatt implements BluetoothProfile { + " unregistering"); } unregisterApp(); + if (Flags.unregisterGattClientDisconnected()) { + mCallback = null; + } return; } if (VDBG) { @@ -274,7 +279,7 @@ public final class BluetoothGatt implements BluetoothProfile { try { // autoConnect is inverse of "isDirect" mService.clientConnect( - mClientIf, + clientIf, mDevice.getAddress(), mDevice.getAddressType(), !mAutoConnect, @@ -361,6 +366,8 @@ public final class BluetoothGatt implements BluetoothProfile { * @hide */ @Override + @RequiresBluetoothConnectPermission + @RequiresPermission(BLUETOOTH_CONNECT) public void onClientConnectionState( int status, int clientIf, boolean connected, String address) { if (DBG) { @@ -381,6 +388,10 @@ public final class BluetoothGatt implements BluetoothProfile { ? BluetoothProfile.STATE_CONNECTED : BluetoothProfile.STATE_DISCONNECTED; + if (Flags.unregisterGattClientDisconnected() && !connected && !mAutoConnect) { + unregisterApp(); + } + runOrQueueCallback( new Runnable() { @Override @@ -433,6 +444,10 @@ public final class BluetoothGatt implements BluetoothProfile { s.setDevice(mDevice); } + if (Flags.fixBluetoothGattGettingDuplicateServices()) { + mServices.clear(); + } + mServices.addAll(services); // Fix references to included services, as they doesn't point to right objects. @@ -493,16 +508,18 @@ public final class BluetoothGatt implements BluetoothProfile { mDeviceBusy = false; } + int clientIf = mClientIf; if ((status == GATT_INSUFFICIENT_AUTHENTICATION || status == GATT_INSUFFICIENT_ENCRYPTION) - && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) { + && (mAuthRetryState != AUTH_RETRY_STATE_MITM) + && (clientIf > 0)) { try { final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE) ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM; mService.readCharacteristic( - mClientIf, address, handle, authReq, mAttributionSource); + clientIf, address, handle, authReq, mAttributionSource); mAuthRetryState++; return; } catch (RemoteException e) { @@ -573,10 +590,13 @@ public final class BluetoothGatt implements BluetoothProfile { ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM; int requestStatus = BluetoothStatusCodes.ERROR_UNKNOWN; - for (int i = 0; i < WRITE_CHARACTERISTIC_MAX_RETRIES; i++) { + int clientIf = mClientIf; + for (int i = 0; + (i < WRITE_CHARACTERISTIC_MAX_RETRIES) && (clientIf > 0); + i++) { requestStatus = mService.writeCharacteristic( - mClientIf, + clientIf, address, handle, characteristic.getWriteType(), @@ -679,16 +699,18 @@ public final class BluetoothGatt implements BluetoothProfile { BluetoothGattDescriptor descriptor = getDescriptorById(mDevice, handle); if (descriptor == null) return; + int clientIf = mClientIf; if ((status == GATT_INSUFFICIENT_AUTHENTICATION || status == GATT_INSUFFICIENT_ENCRYPTION) - && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) { + && (mAuthRetryState != AUTH_RETRY_STATE_MITM) + && (clientIf > 0)) { try { final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE) ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM; mService.readDescriptor( - mClientIf, address, handle, authReq, mAttributionSource); + clientIf, address, handle, authReq, mAttributionSource); mAuthRetryState++; return; } catch (RemoteException e) { @@ -741,16 +763,18 @@ public final class BluetoothGatt implements BluetoothProfile { BluetoothGattDescriptor descriptor = getDescriptorById(mDevice, handle); if (descriptor == null) return; + int clientIf = mClientIf; if ((status == GATT_INSUFFICIENT_AUTHENTICATION || status == GATT_INSUFFICIENT_ENCRYPTION) - && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) { + && (mAuthRetryState != AUTH_RETRY_STATE_MITM) + && (clientIf > 0)) { try { final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE) ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM; mService.writeDescriptor( - mClientIf, address, handle, authReq, value, mAttributionSource); + clientIf, address, handle, authReq, value, mAttributionSource); mAuthRetryState++; return; } catch (RemoteException e) { @@ -1033,6 +1057,10 @@ public final class BluetoothGatt implements BluetoothProfile { if (DBG) Log.d(TAG, "close()"); unregisterApp(); + if (Flags.unregisterGattClientDisconnected()) { + mCallback = null; + } + mConnState = CONN_STATE_CLOSED; mAuthRetryState = AUTH_RETRY_STATE_IDLE; } @@ -1166,7 +1194,9 @@ public final class BluetoothGatt implements BluetoothProfile { if (DBG) Log.d(TAG, "unregisterApp() - mClientIf=" + mClientIf); try { - mCallback = null; + if (!Flags.unregisterGattClientDisconnected()) { + mCallback = null; + } mService.unregisterClient(mClientIf, mAttributionSource); mClientIf = 0; } catch (RemoteException e) { @@ -1229,10 +1259,11 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public void disconnect() { if (DBG) Log.d(TAG, "cancelOpen() - device: " + mDevice); - if (mService == null || mClientIf == 0) return; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return; try { - mService.clientDisconnect(mClientIf, mDevice.getAddress(), mAttributionSource); + mService.clientDisconnect(clientIf, mDevice.getAddress(), mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); } @@ -1250,6 +1281,40 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresBluetoothConnectPermission @RequiresPermission(BLUETOOTH_CONNECT) public boolean connect() { + int clientIf = mClientIf; + if (mService == null) return false; + if (clientIf == 0) { + if (!Flags.unregisterGattClientDisconnected()) { + return false; + } + synchronized (mStateLock) { + if (mConnState != CONN_STATE_IDLE) { + return false; + } + mConnState = CONN_STATE_CONNECTING; + } + + UUID uuid = UUID.randomUUID(); + if (DBG) Log.d(TAG, "reconnect from connect(), UUID=" + uuid); + + try { + mService.registerClient( + new ParcelUuid(uuid), + mBluetoothGattCallback, + /* eatt_support= */ false, + mAttributionSource); + } catch (RemoteException e) { + Log.e(TAG, "", e); + synchronized (mStateLock) { + mConnState = CONN_STATE_IDLE; + } + Log.e(TAG, "Failed to register callback"); + return false; + } + + return true; + } + try { if (DBG) { Log.d(TAG, "connect(void) - device: " + mDevice + ", auto=" + mAutoConnect); @@ -1257,7 +1322,7 @@ public final class BluetoothGatt implements BluetoothProfile { // autoConnect is inverse of "isDirect" mService.clientConnect( - mClientIf, + clientIf, mDevice.getAddress(), mDevice.getAddressType(), !mAutoConnect, @@ -1293,9 +1358,12 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresBluetoothConnectPermission @RequiresPermission(BLUETOOTH_CONNECT) public void setPreferredPhy(int txPhy, int rxPhy, int phyOptions) { + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return; + try { mService.clientSetPreferredPhy( - mClientIf, mDevice.getAddress(), txPhy, rxPhy, phyOptions, mAttributionSource); + clientIf, mDevice.getAddress(), txPhy, rxPhy, phyOptions, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); } @@ -1308,8 +1376,11 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresBluetoothConnectPermission @RequiresPermission(BLUETOOTH_CONNECT) public void readPhy() { + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return; + try { - mService.clientReadPhy(mClientIf, mDevice.getAddress(), mAttributionSource); + mService.clientReadPhy(clientIf, mDevice.getAddress(), mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); } @@ -1340,12 +1411,16 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public boolean discoverServices() { if (DBG) Log.d(TAG, "discoverServices() - device: " + mDevice); - if (mService == null || mClientIf == 0) return false; - mServices.clear(); + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; + + if (!Flags.fixBluetoothGattGettingDuplicateServices()) { + mServices.clear(); + } try { - mService.discoverServices(mClientIf, mDevice.getAddress(), mAttributionSource); + mService.discoverServices(clientIf, mDevice.getAddress(), mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); return false; @@ -1367,13 +1442,16 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public boolean discoverServiceByUuid(UUID uuid) { if (DBG) Log.d(TAG, "discoverServiceByUuid() - device: " + mDevice); - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; - mServices.clear(); + if (!Flags.fixBluetoothGattGettingDuplicateServices()) { + mServices.clear(); + } try { mService.discoverServiceByUuid( - mClientIf, mDevice.getAddress(), new ParcelUuid(uuid), mAttributionSource); + clientIf, mDevice.getAddress(), new ParcelUuid(uuid), mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); return false; @@ -1447,7 +1525,8 @@ public final class BluetoothGatt implements BluetoothProfile { } if (VDBG) Log.d(TAG, "readCharacteristic() - uuid: " + characteristic.getUuid()); - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; BluetoothGattService service = characteristic.getService(); if (service == null) return false; @@ -1462,7 +1541,7 @@ public final class BluetoothGatt implements BluetoothProfile { try { mService.readCharacteristic( - mClientIf, + clientIf, device.getAddress(), characteristic.getInstanceId(), AUTHENTICATION_NONE, @@ -1494,7 +1573,8 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public boolean readUsingCharacteristicUuid(UUID uuid, int startHandle, int endHandle) { if (VDBG) Log.d(TAG, "readUsingCharacteristicUuid() - uuid: " + uuid); - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; synchronized (mDeviceBusyLock) { if (mDeviceBusy) return false; @@ -1503,7 +1583,7 @@ public final class BluetoothGatt implements BluetoothProfile { try { mService.readUsingCharacteristicUuid( - mClientIf, + clientIf, mDevice.getAddress(), new ParcelUuid(uuid), startHandle, @@ -1601,7 +1681,8 @@ public final class BluetoothGatt implements BluetoothProfile { == 0) { return BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED; } - if (mService == null || mClientIf == 0) { + int clientIf = mClientIf; + if (mService == null || clientIf == 0) { return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND; } @@ -1627,7 +1708,7 @@ public final class BluetoothGatt implements BluetoothProfile { for (int i = 0; i < WRITE_CHARACTERISTIC_MAX_RETRIES; i++) { requestStatus = mService.writeCharacteristic( - mClientIf, + clientIf, device.getAddress(), characteristic.getInstanceId(), writeType, @@ -1674,7 +1755,8 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public boolean readDescriptor(BluetoothGattDescriptor descriptor) { if (VDBG) Log.d(TAG, "readDescriptor() - uuid: " + descriptor.getUuid()); - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic(); if (characteristic == null) return false; @@ -1692,7 +1774,7 @@ public final class BluetoothGatt implements BluetoothProfile { try { mService.readDescriptor( - mClientIf, + clientIf, device.getAddress(), descriptor.getInstanceId(), AUTHENTICATION_NONE, @@ -1755,7 +1837,8 @@ public final class BluetoothGatt implements BluetoothProfile { throw new IllegalArgumentException("value must not be null"); } if (VDBG) Log.d(TAG, "writeDescriptor() - uuid: " + descriptor.getUuid()); - if (mService == null || mClientIf == 0) { + int clientIf = mClientIf; + if (mService == null || clientIf == 0) { return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND; } @@ -1781,7 +1864,7 @@ public final class BluetoothGatt implements BluetoothProfile { try { return mService.writeDescriptor( - mClientIf, + clientIf, device.getAddress(), descriptor.getInstanceId(), AUTHENTICATION_NONE, @@ -1818,10 +1901,11 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public boolean beginReliableWrite() { if (VDBG) Log.d(TAG, "beginReliableWrite() - device: " + mDevice); - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; try { - mService.beginReliableWrite(mClientIf, mDevice.getAddress(), mAttributionSource); + mService.beginReliableWrite(clientIf, mDevice.getAddress(), mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); return false; @@ -1846,7 +1930,8 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public boolean executeReliableWrite() { if (VDBG) Log.d(TAG, "executeReliableWrite() - device: " + mDevice); - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; synchronized (mDeviceBusyLock) { if (mDeviceBusy) return false; @@ -1854,7 +1939,7 @@ public final class BluetoothGatt implements BluetoothProfile { } try { - mService.endReliableWrite(mClientIf, mDevice.getAddress(), true, mAttributionSource); + mService.endReliableWrite(clientIf, mDevice.getAddress(), true, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); synchronized (mDeviceBusyLock) { @@ -1877,10 +1962,11 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public void abortReliableWrite() { if (VDBG) Log.d(TAG, "abortReliableWrite() - device: " + mDevice); - if (mService == null || mClientIf == 0) return; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return; try { - mService.endReliableWrite(mClientIf, mDevice.getAddress(), false, mAttributionSource); + mService.endReliableWrite(clientIf, mDevice.getAddress(), false, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); } @@ -1921,7 +2007,8 @@ public final class BluetoothGatt implements BluetoothProfile { + " enable: " + enable); } - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; BluetoothGattService service = characteristic.getService(); if (service == null) return false; @@ -1931,7 +2018,7 @@ public final class BluetoothGatt implements BluetoothProfile { try { mService.registerForNotification( - mClientIf, + clientIf, device.getAddress(), characteristic.getInstanceId(), enable, @@ -1954,10 +2041,11 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public boolean refresh() { if (DBG) Log.d(TAG, "refresh() - device: " + mDevice); - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; try { - mService.refreshDevice(mClientIf, mDevice.getAddress(), mAttributionSource); + mService.refreshDevice(clientIf, mDevice.getAddress(), mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); return false; @@ -1979,10 +2067,11 @@ public final class BluetoothGatt implements BluetoothProfile { @RequiresPermission(BLUETOOTH_CONNECT) public boolean readRemoteRssi() { if (DBG) Log.d(TAG, "readRssi() - device: " + mDevice); - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; try { - mService.readRemoteRssi(mClientIf, mDevice.getAddress(), mAttributionSource); + mService.readRemoteRssi(clientIf, mDevice.getAddress(), mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); return false; @@ -2014,10 +2103,11 @@ public final class BluetoothGatt implements BluetoothProfile { if (DBG) { Log.d(TAG, "configureMTU() - device: " + mDevice + " mtu: " + mtu); } - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; try { - mService.configureMTU(mClientIf, mDevice.getAddress(), mtu, mAttributionSource); + mService.configureMTU(clientIf, mDevice.getAddress(), mtu, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); return false; @@ -2047,11 +2137,12 @@ public final class BluetoothGatt implements BluetoothProfile { } if (DBG) Log.d(TAG, "requestConnectionPriority() - params: " + connectionPriority); - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; try { mService.connectionParameterUpdate( - mClientIf, mDevice.getAddress(), connectionPriority, mAttributionSource); + clientIf, mDevice.getAddress(), connectionPriority, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "", e); return false; @@ -2098,11 +2189,12 @@ public final class BluetoothGatt implements BluetoothProfile { + ", max_ce=" + maxConnectionEventLen); } - if (mService == null || mClientIf == 0) return false; + int clientIf = mClientIf; + if (mService == null || clientIf == 0) return false; try { mService.leConnectionUpdate( - mClientIf, + clientIf, mDevice.getAddress(), minConnectionInterval, maxConnectionInterval, @@ -2148,12 +2240,13 @@ public final class BluetoothGatt implements BluetoothProfile { if (DBG) { Log.d(TAG, "requestsubrateMode(" + subrateMode + ")"); } - if (mService == null || mClientIf == 0) { + int clientIf = mClientIf; + if (mService == null || clientIf == 0) { return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED; } try { - return mService.subrateModeRequest(mClientIf, mDevice, subrateMode, mAttributionSource); + return mService.subrateModeRequest(clientIf, mDevice, subrateMode, mAttributionSource); } catch (RemoteException e) { logRemoteException(TAG, e); return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED; diff --git a/framework/java/android/bluetooth/BluetoothSocket.java b/framework/java/android/bluetooth/BluetoothSocket.java index 97bdf595a0..cde23baf4d 100644 --- a/framework/java/android/bluetooth/BluetoothSocket.java +++ b/framework/java/android/bluetooth/BluetoothSocket.java @@ -978,6 +978,8 @@ public final class BluetoothSocket implements Closeable { if (mL2capBuffer.remaining() == 0) { if (VDBG) Log.v(TAG, "l2cap buffer empty, refilling..."); if (fillL2capRxBuffer() == -1) { + Log.d(TAG, "socket EOF, returning -1"); + mSocketState = SocketState.CLOSED; return -1; } } @@ -994,6 +996,7 @@ public final class BluetoothSocket implements Closeable { ret = mSocketIS.read(b, offset, length); } if (ret < 0) { + mSocketState = SocketState.CLOSED; throw new IOException("bt socket closed, read return: " + ret); } if (VDBG) Log.d(TAG, "read out: " + mSocketIS + " ret: " + ret); diff --git a/framework/tests/bumble/src/android/bluetooth/DckL2capTest.kt b/framework/tests/bumble/src/android/bluetooth/DckL2capTest.kt index e658e8c645..ea41c55ee0 100644 --- a/framework/tests/bumble/src/android/bluetooth/DckL2capTest.kt +++ b/framework/tests/bumble/src/android/bluetooth/DckL2capTest.kt @@ -226,6 +226,51 @@ public class DckL2capTest() : Closeable { Log.d(TAG, "testReceive: done") } + @Test + @VirtualOnly + fun testReadReturnOnRemoteSocketDisconnect() { + Log.d(TAG, "testReadReturnonSocketDisconnect: Connect L2CAP") + var bluetoothSocket: BluetoothSocket? + val l2capServer = bluetoothAdapter.listenUsingInsecureL2capChannel() + val socketFlow = flow { emit(l2capServer.accept()) } + val connectResponse = createAndConnectL2capChannelWithBumble(l2capServer.psm) + runBlocking { + bluetoothSocket = socketFlow.first() + assertThat(connectResponse.hasChannel()).isTrue() + } + + val inputStream = bluetoothSocket!!.inputStream + + // block on read() on server thread + val readThread = Thread { + Log.d(TAG, "testReadReturnOnRemoteSocketDisconnect: Receive data on Android") + val ret = inputStream.read() + Log.d(TAG, "testReadReturnOnRemoteSocketDisconnect: read returns : " + ret) + Log.d( + TAG, + "testReadReturnOnRemoteSocketDisconnect: isConnected() : " + + bluetoothSocket!!.isConnected(), + ) + assertThat(ret).isEqualTo(-1) + assertThat(bluetoothSocket!!.isConnected()).isFalse() + } + readThread.start() + // check that socket is still connected + assertThat(bluetoothSocket!!.isConnected()).isTrue() + + // read() would be blocking till underlying l2cap is disconnected + Thread.sleep(1000 * 10) + Log.d(TAG, "testReadReturnOnRemoteSocketDisconnect: disconnect after 10 secs") + val disconnectRequest = + DisconnectRequest.newBuilder().setChannel(connectResponse.channel).build() + val disconnectResponse = mBumble.l2capBlocking().disconnect(disconnectRequest) + assertThat(disconnectResponse.hasSuccess()).isTrue() + inputStream.close() + bluetoothSocket?.close() + l2capServer.close() + Log.d(TAG, "testReadReturnOnRemoteSocketDisconnect: done") + } + private fun createAndConnectL2capChannelWithBumble(psm: Int): ConnectResponse { Log.d(TAG, "createAndConnectL2capChannelWithBumble") val remoteDevice = diff --git a/framework/tests/bumble/src/android/bluetooth/GattClientTest.java b/framework/tests/bumble/src/android/bluetooth/GattClientTest.java index c9fa50bbd9..64ada411d6 100644 --- a/framework/tests/bumble/src/android/bluetooth/GattClientTest.java +++ b/framework/tests/bumble/src/android/bluetooth/GattClientTest.java @@ -720,6 +720,44 @@ public class GattClientTest { } } + @Test + @RequiresFlagsEnabled(Flags.FLAG_UNREGISTER_GATT_CLIENT_DISCONNECTED) + public void connectAndDisconnectManyClientsWithoutClose() throws Exception { + advertiseWithBumble(); + + List<BluetoothGatt> gatts = new ArrayList<>(); + try { + for (int i = 0; i < 100; i++) { + BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); + InOrder inOrder = inOrder(gattCallback); + + BluetoothGatt gatt = mRemoteLeDevice.connectGatt(mContext, false, gattCallback); + gatts.add(gatt); + + inOrder.verify(gattCallback, timeout(1000)) + .onConnectionStateChange(any(), anyInt(), eq(STATE_CONNECTED)); + + gatt.disconnect(); + inOrder.verify(gattCallback, timeout(1000)) + .onConnectionStateChange( + any(), anyInt(), eq(BluetoothProfile.STATE_DISCONNECTED)); + + gatt.connect(); + inOrder.verify(gattCallback, timeout(1000)) + .onConnectionStateChange(any(), anyInt(), eq(STATE_CONNECTED)); + + gatt.disconnect(); + inOrder.verify(gattCallback, timeout(1000)) + .onConnectionStateChange( + any(), anyInt(), eq(BluetoothProfile.STATE_DISCONNECTED)); + } + } finally { + for (BluetoothGatt gatt : gatts) { + gatt.close(); + } + } + } + private void createLeBondAndWaitBonding(BluetoothDevice device) { advertiseWithBumble(); mHost.createBondAndVerify(device); diff --git a/framework/tests/bumble/src/android/bluetooth/LeScanningTest.java b/framework/tests/bumble/src/android/bluetooth/LeScanningTest.java index 506586bbe3..9ee8c279f1 100644 --- a/framework/tests/bumble/src/android/bluetooth/LeScanningTest.java +++ b/framework/tests/bumble/src/android/bluetooth/LeScanningTest.java @@ -68,7 +68,7 @@ import java.util.stream.Stream; @RunWith(TestParameterInjector.class) public class LeScanningTest { private static final String TAG = "LeScanningTest"; - private static final int TIMEOUT_SCANNING_MS = 2000; + private static final int TIMEOUT_SCANNING_MS = 3000; private static final String TEST_UUID_STRING = "00001805-0000-1000-8000-00805f9b34fb"; private static final String TEST_ADDRESS_RANDOM_STATIC = "F0:43:A8:23:10:11"; private static final String ACTION_DYNAMIC_RECEIVER_SCAN_RESULT = @@ -376,6 +376,7 @@ public class LeScanningTest { // Test against UUIDs that are close to TEST_UUID_STRING, one that has a few bits unset and one // that has an extra bit set. @Test + @VirtualOnly public void startBleScan_withServiceData_uuidDoesntMatch( @TestParameter({"00001800", "00001815"}) String uuid) { advertiseWithBumbleWithServiceData(); diff --git a/framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java b/framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java index 44c6564803..d289b7c43e 100644 --- a/framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java +++ b/framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java @@ -22,12 +22,8 @@ import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; @@ -43,10 +39,8 @@ import android.bluetooth.StreamObserverSpliterator; import android.bluetooth.Utils; import android.bluetooth.test_utils.BlockingBluetoothAdapter; import android.bluetooth.test_utils.EnableBluetoothRule; -import android.content.BroadcastReceiver; +import android.bluetooth.pairing.utils.IntentReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.os.ParcelUuid; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; @@ -63,19 +57,15 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import io.grpc.stub.StreamObserver; -import org.hamcrest.Matcher; import org.hamcrest.Matchers; -import org.hamcrest.core.AllOf; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.mockito.hamcrest.MockitoHamcrest; import pandora.GattProto; import pandora.HostProto.AdvertiseRequest; @@ -90,9 +80,6 @@ import pandora.SecurityProto.SecureRequest; import pandora.SecurityProto.SecureResponse; import java.time.Duration; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -127,12 +114,9 @@ public class PairingTest { public final EnableBluetoothRule mEnableBluetoothRule = new EnableBluetoothRule(false /* enableTestMode */, true /* toggleBluetooth */); - private final Map<String, Integer> mActionRegistrationCounts = new HashMap<>(); private final StreamObserverSpliterator<PairingEvent> mPairingEventStreamObserver = new StreamObserverSpliterator<>(); - @Mock private BroadcastReceiver mReceiver; @Mock private BluetoothProfile.ServiceListener mProfileServiceListener; - private InOrder mInOrder = null; private BluetoothDevice mBumbleDevice; private BluetoothDevice mRemoteLeDevice; private BluetoothHidHost mHidService; @@ -142,30 +126,6 @@ public class PairingTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - doAnswer( - inv -> { - Log.d( - TAG, - "onReceive(): intent=" + Arrays.toString(inv.getArguments())); - Intent intent = inv.getArgument(1); - String action = intent.getAction(); - if (BluetoothDevice.ACTION_UUID.equals(action)) { - ParcelUuid[] uuids = - intent.getParcelableArrayExtra( - BluetoothDevice.EXTRA_UUID, ParcelUuid.class); - Log.d(TAG, "onReceive(): UUID=" + Arrays.toString(uuids)); - } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { - int bondState = - intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1); - Log.d(TAG, "onReceive(): bondState=" + bondState); - } - return null; - }) - .when(mReceiver) - .onReceive(any(), any()); - - mInOrder = inOrder(mReceiver); - // Get profile proxies mHidService = (BluetoothHidHost) getProfileProxy(BluetoothProfile.HID_HOST); mHfpService = (BluetoothHeadset) getProfileProxy(BluetoothProfile.HEADSET); @@ -175,28 +135,54 @@ public class PairingTest { sAdapter.getRemoteLeDevice( Utils.BUMBLE_RANDOM_ADDRESS, BluetoothDevice.ADDRESS_TYPE_RANDOM); + /* + * Note: Since there was no IntentReceiver registered, passing the instance as + * NULL in testStep_RemoveBond(). But, if there is an instance already present, that + * must be passed instead of NULL. + */ for (BluetoothDevice device : sAdapter.getBondedDevices()) { - removeBond(device); + testStep_RemoveBond(null, device); } } @After public void tearDown() throws Exception { Set<BluetoothDevice> bondedDevices = sAdapter.getBondedDevices(); + + /* + * Note: Since there was no IntentReceiver registered, passing the instance as + * NULL in testStep_RemoveBond(). But, if there is an instance already present, that + * must be passed instead of NULL. + */ if (bondedDevices.contains(mBumbleDevice)) { - removeBond(mBumbleDevice); + testStep_RemoveBond(null, mBumbleDevice); } if (bondedDevices.contains(mRemoteLeDevice)) { - removeBond(mRemoteLeDevice); + testStep_RemoveBond(null, mRemoteLeDevice); } mBumbleDevice = null; mRemoteLeDevice = null; - if (getTotalActionRegistrationCounts() > 0) { - sTargetContext.unregisterReceiver(mReceiver); - mActionRegistrationCounts.clear(); - } } + /** All the test function goes here */ + + /** + * Process of writing a test function + * + * 1. Create an IntentReceiver object first with following way: + * IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + * BluetoothDevice.ACTION_1, + * BluetoothDevice.ACTION_2) + * .setIntentListener(--) // optional + * .setIntentTimeout(--) // optional + * .build(); + * 2. Use the intentReceiver instance for all Intent related verification, and pass + * the same instance to all the helper/testStep functions which has similar Intent + * requirements. + * 3. Once all the verification is done, call `intentReceiver.close()` before returning + * from the function. + */ + /** * Test a simple BR/EDR just works pairing flow in the follow steps: * @@ -211,8 +197,10 @@ public class PairingTest { */ @Test public void testBrEdrPairing_phoneInitiatedBrEdrInquiryOnlyJustWorks() { - registerIntentActions( - BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST); + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + BluetoothDevice.ACTION_BOND_STATE_CHANGED, + BluetoothDevice.ACTION_PAIRING_REQUEST) + .build(); StreamObserver<PairingEventAnswer> pairingEventAnswerObserver = mBumble.security() @@ -220,12 +208,12 @@ public class PairingTest { .onPairing(mPairingEventStreamObserver); assertThat(mBumbleDevice.createBond()).isTrue(); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra( @@ -238,15 +226,12 @@ public class PairingTest { pairingEventAnswerObserver.onNext( PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build()); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)); - verifyNoMoreInteractions(mReceiver); - - unregisterIntentActions( - BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST); + intentReceiver.close(); } /** @@ -266,8 +251,10 @@ public class PairingTest { @Test @RequiresFlagsEnabled({Flags.FLAG_IGNORE_UNRELATED_CANCEL_BOND}) public void testBrEdrPairing_cancelBond_forUnrelatedDevice() { - registerIntentActions( - BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST); + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + BluetoothDevice.ACTION_BOND_STATE_CHANGED, + BluetoothDevice.ACTION_PAIRING_REQUEST) + .build(); StreamObserver<PairingEventAnswer> pairingEventAnswerObserver = mBumble.security() @@ -275,12 +262,12 @@ public class PairingTest { .onPairing(mPairingEventStreamObserver); assertThat(mBumbleDevice.createBond()).isTrue(); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra( @@ -296,15 +283,12 @@ public class PairingTest { pairingEventAnswerObserver.onNext( PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build()); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)); - verifyNoMoreInteractions(mReceiver); - - unregisterIntentActions( - BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST); + intentReceiver.close(); } /** @@ -322,10 +306,11 @@ public class PairingTest { */ @Test public void testBrEdrPairing_phoneInitiatedBrEdrInquiryOnlyJustWorksWhileSdpConnected() { - registerIntentActions( + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, BluetoothDevice.ACTION_ACL_CONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothDevice.ACTION_PAIRING_REQUEST); + BluetoothDevice.ACTION_PAIRING_REQUEST) + .build(); StreamObserver<PairingEventAnswer> pairingEventAnswerObserver = mBumble.security() @@ -335,17 +320,17 @@ public class PairingTest { // Start SDP. This will create an ACL connection before the bonding starts. assertThat(mBumbleDevice.fetchUuidsWithSdp(BluetoothDevice.TRANSPORT_BREDR)).isTrue(); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_ACL_CONNECTED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); assertThat(mBumbleDevice.createBond()).isTrue(); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra( @@ -358,17 +343,12 @@ public class PairingTest { pairingEventAnswerObserver.onNext( PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build()); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)); - verifyNoMoreInteractions(mReceiver); - - unregisterIntentActions( - BluetoothDevice.ACTION_ACL_CONNECTED, - BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothDevice.ACTION_PAIRING_REQUEST); + intentReceiver.close(); } /** @@ -398,11 +378,13 @@ public class PairingTest { @Test @RequiresFlagsEnabled({Flags.FLAG_PREVENT_DUPLICATE_UUID_INTENT}) public void testCancelBondLe_WithGattServiceDiscovery() { - registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + BluetoothDevice.ACTION_BOND_STATE_CHANGED) + .build(); // Outgoing GATT service discovery and incoming LE pairing in parallel StreamObserverSpliterator<SecureResponse> responseObserver = - helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(); + helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(intentReceiver); // Cancel pairing from Android assertThat(mBumbleDevice.cancelBondProcess()).isTrue(); @@ -412,14 +394,12 @@ public class PairingTest { // Pairing should be cancelled in a moment instead of timing out in 30 // seconds - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)); - verifyNoMoreInteractions(mReceiver); - - unregisterIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + intentReceiver.close(); } /** @@ -449,11 +429,13 @@ public class PairingTest { @Test @RequiresFlagsEnabled({Flags.FLAG_PREVENT_DUPLICATE_UUID_INTENT}) public void testBondLe_WithGattServiceDiscovery() { - registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + BluetoothDevice.ACTION_BOND_STATE_CHANGED) + .build(); // Outgoing GATT service discovery and incoming LE pairing in parallel StreamObserverSpliterator<SecureResponse> responseObserver = - helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(); + helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(intentReceiver); // Approve pairing from Android assertThat(mBumbleDevice.setPairingConfirmation(true)).isTrue(); @@ -462,14 +444,12 @@ public class PairingTest { assertThat(secureResponse.hasSuccess()).isTrue(); // Ensure that pairing succeeds - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)); - verifyNoMoreInteractions(mReceiver); - - unregisterIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + intentReceiver.close(); } /** @@ -495,9 +475,11 @@ public class PairingTest { */ @Test public void testBondLe_Reconnect() { - registerIntentActions(BluetoothDevice.ACTION_ACL_CONNECTED); + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + BluetoothDevice.ACTION_ACL_CONNECTED) + .build(); - testStep_BondLe(mBumbleDevice, OwnAddressType.PUBLIC); + testStep_BondLe(intentReceiver, mBumbleDevice, OwnAddressType.PUBLIC); assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice); testStep_restartBt(); @@ -521,12 +503,12 @@ public class PairingTest { .build()); assertThat(mBumbleDevice.connect()).isEqualTo(BluetoothStatusCodes.SUCCESS); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_ACL_CONNECTED), hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); - verifyNoMoreInteractions(mReceiver); - unregisterIntentActions(BluetoothDevice.ACTION_ACL_CONNECTED); + + intentReceiver.close(); } /** @@ -559,98 +541,6 @@ public class PairingTest { } } - private void doTestIdentityAddressWithType( - BluetoothDevice device, OwnAddressType ownAddressType) { - BluetoothAddress identityAddress = device.getIdentityAddressWithType(); - assertThat(identityAddress.getAddress()).isNull(); - assertThat(identityAddress.getAddressType()) - .isEqualTo(BluetoothDevice.ADDRESS_TYPE_UNKNOWN); - - testStep_BondLe(device, ownAddressType); - assertThat(sAdapter.getBondedDevices()).contains(device); - - identityAddress = device.getIdentityAddressWithType(); - assertThat(identityAddress.getAddress()).isEqualTo(device.getAddress()); - assertThat(identityAddress.getAddressType()) - .isEqualTo( - ownAddressType == OwnAddressType.RANDOM - ? BluetoothDevice.ADDRESS_TYPE_RANDOM - : BluetoothDevice.ADDRESS_TYPE_PUBLIC); - } - - private void testStep_BondLe(BluetoothDevice device, OwnAddressType ownAddressType) { - registerIntentActions( - BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothDevice.ACTION_ACL_CONNECTED, - BluetoothDevice.ACTION_PAIRING_REQUEST); - - mBumble.gattBlocking() - .registerService( - GattProto.RegisterServiceRequest.newBuilder() - .setService( - GattProto.GattServiceParams.newBuilder() - .setUuid(BATTERY_UUID.toString()) - .build()) - .build()); - mBumble.gattBlocking() - .registerService( - GattProto.RegisterServiceRequest.newBuilder() - .setService( - GattProto.GattServiceParams.newBuilder() - .setUuid(HOGP_UUID.toString()) - .build()) - .build()); - - mBumble.hostBlocking() - .advertise( - AdvertiseRequest.newBuilder() - .setLegacy(true) - .setConnectable(true) - .setOwnAddressType(ownAddressType) - .build()); - - StreamObserver<PairingEventAnswer> pairingEventAnswerObserver = - mBumble.security() - .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) - .onPairing(mPairingEventStreamObserver); - - assertThat(device.createBond(BluetoothDevice.TRANSPORT_LE)).isTrue(); - - verifyIntentReceivedUnordered( - hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), - hasExtra(BluetoothDevice.EXTRA_DEVICE, device), - hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)); - verifyIntentReceived( - hasAction(BluetoothDevice.ACTION_ACL_CONNECTED), - hasExtra(BluetoothDevice.EXTRA_DEVICE, device), - hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE)); - verifyIntentReceivedUnordered( - hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), - hasExtra(BluetoothDevice.EXTRA_DEVICE, device), - hasExtra( - BluetoothDevice.EXTRA_PAIRING_VARIANT, - BluetoothDevice.PAIRING_VARIANT_CONSENT)); - - // Approve pairing from Android - assertThat(device.setPairingConfirmation(true)).isTrue(); - - PairingEvent pairingEvent = mPairingEventStreamObserver.iterator().next(); - assertThat(pairingEvent.hasJustWorks()).isTrue(); - pairingEventAnswerObserver.onNext( - PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build()); - - // Ensure that pairing succeeds - verifyIntentReceived( - hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), - hasExtra(BluetoothDevice.EXTRA_DEVICE, device), - hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)); - - unregisterIntentActions( - BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothDevice.ACTION_ACL_CONNECTED, - BluetoothDevice.ACTION_PAIRING_REQUEST); - } - /** * Test if bonded BR/EDR device can reconnect after BT restart * @@ -674,9 +564,11 @@ public class PairingTest { */ @Test public void testBondBredr_Reconnect() { - registerIntentActions(BluetoothDevice.ACTION_ACL_CONNECTED); + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + BluetoothDevice.ACTION_ACL_CONNECTED) + .build(); - testStep_BondBredr(); + testStep_BondBredr(intentReceiver); assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice); testStep_restartBt(); @@ -689,12 +581,12 @@ public class PairingTest { .build(); mBumble.hostBlocking().setConnectabilityMode(request); assertThat(mBumbleDevice.connect()).isEqualTo(BluetoothStatusCodes.SUCCESS); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_ACL_CONNECTED), hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); - verifyNoMoreInteractions(mReceiver); - unregisterIntentActions(BluetoothDevice.ACTION_ACL_CONNECTED); + + intentReceiver.close(); } /** @@ -720,27 +612,27 @@ public class PairingTest { @Test @RequiresFlagsEnabled({Flags.FLAG_WAIT_FOR_DISCONNECT_BEFORE_UNBOND}) public void testRemoveBondLe_WhenConnected() { - registerIntentActions( - BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED); + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + BluetoothDevice.ACTION_ACL_DISCONNECTED, + BluetoothDevice.ACTION_BOND_STATE_CHANGED) + .build(); - testStep_BondLe(mBumbleDevice, OwnAddressType.PUBLIC); + testStep_BondLe(intentReceiver, mBumbleDevice, OwnAddressType.PUBLIC); assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice); assertThat(mBumbleDevice.removeBond()).isTrue(); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_ACL_DISCONNECTED), hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)); assertThat(sAdapter.getBondedDevices()).doesNotContain(mBumbleDevice); - verifyNoMoreInteractions(mReceiver); - unregisterIntentActions( - BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED); + intentReceiver.close(); } /** @@ -766,27 +658,27 @@ public class PairingTest { @Test @RequiresFlagsEnabled({Flags.FLAG_WAIT_FOR_DISCONNECT_BEFORE_UNBOND}) public void testRemoveBondBredr_WhenConnected() { - registerIntentActions( - BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED); + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + BluetoothDevice.ACTION_ACL_DISCONNECTED, + BluetoothDevice.ACTION_BOND_STATE_CHANGED) + .build(); - testStep_BondBredr(); + testStep_BondBredr(intentReceiver); assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice); assertThat(mBumbleDevice.removeBond()).isTrue(); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_ACL_DISCONNECTED), hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)); assertThat(sAdapter.getBondedDevices()).doesNotContain(mBumbleDevice); - verifyNoMoreInteractions(mReceiver); - unregisterIntentActions( - BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED); + intentReceiver.close(); } /** @@ -813,54 +705,51 @@ public class PairingTest { */ @Test public void testRemoveBondLe_WhenDisconnected() { - registerIntentActions( + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); + BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED) + .build(); - testStep_BondLe(mBumbleDevice, OwnAddressType.PUBLIC); + testStep_BondLe(intentReceiver, mBumbleDevice, OwnAddressType.PUBLIC); assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice); // Wait for profiles to get connected - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothHidHost.EXTRA_STATE, BluetoothHidHost.STATE_CONNECTING)); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothHidHost.EXTRA_STATE, BluetoothHidHost.STATE_CONNECTED)); // Disconnect Bumble assertThat(mBumbleDevice.disconnect()).isEqualTo(BluetoothStatusCodes.SUCCESS); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothHidHost.EXTRA_STATE, BluetoothHidHost.STATE_DISCONNECTING)); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothHidHost.EXTRA_STATE, BluetoothHidHost.STATE_DISCONNECTED)); // Wait for ACL to get disconnected - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_ACL_DISCONNECTED), hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); // Remove bond assertThat(mBumbleDevice.removeBond()).isTrue(); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)); assertThat(sAdapter.getBondedDevices()).doesNotContain(mBumbleDevice); - verifyNoMoreInteractions(mReceiver); - unregisterIntentActions( - BluetoothDevice.ACTION_ACL_DISCONNECTED, - BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); + intentReceiver.close(); } /** @@ -887,10 +776,11 @@ public class PairingTest { */ @Test public void testRemoveBondBredr_WhenDisconnected() { - registerIntentActions( + IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); + BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED) + .build(); // Disable all profiles other than A2DP as profile connections take too long assertThat( @@ -902,15 +792,15 @@ public class PairingTest { mBumbleDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)) .isTrue(); - testStep_BondBredr(); + testStep_BondBredr(intentReceiver); assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice); // Wait for profiles to get connected - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED), hasExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_CONNECTING), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED), hasExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_CONNECTED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); @@ -921,58 +811,78 @@ public class PairingTest { future.completeOnTimeout(null, TEST_DELAY_MS, TimeUnit.MILLISECONDS).join(); // Disconnect all profiles assertThat(mBumbleDevice.disconnect()).isEqualTo(BluetoothStatusCodes.SUCCESS); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED), hasExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_DISCONNECTING), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED), hasExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_DISCONNECTED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); // Wait for the ACL to get disconnected - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_ACL_DISCONNECTED), hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice)); // Remove bond assertThat(mBumbleDevice.removeBond()).isTrue(); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)); assertThat(sAdapter.getBondedDevices()).doesNotContain(mBumbleDevice); - verifyNoMoreInteractions(mReceiver); - unregisterIntentActions( - BluetoothDevice.ACTION_ACL_DISCONNECTED, - BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); + intentReceiver.close(); } - private void testStep_BondBredr() { - registerIntentActions( + /** Helper/testStep functions goes here */ + + /** + * Process of writing a helper/test_step function. + * + * 1. All the helper functions should have IntentReceiver instance passed as an + * argument to them (if any intents needs to be registered). + * 2. The caller (if a test function) can initiate a fresh instance of IntentReceiver + * and use it for all subsequent helper/testStep functions. + * 3. The helper function should first register all required intent actions through the + * helper -> IntentReceiver.updateNewIntentActionsInParentReceiver() + * which either modifies the intentReceiver instance, or creates + * one (if the caller has passed a `null`). + * 4. At the end, all functions should call `intentReceiver.close()` which either + * unregisters the recent actions, or frees the original instance as per the call. + */ + + private void testStep_BondBredr(IntentReceiver parentIntentReceiver) { + IntentReceiver intentReceiver = + IntentReceiver.updateNewIntentActionsInParentReceiver( + parentIntentReceiver, + sTargetContext, BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_ACL_CONNECTED, BluetoothDevice.ACTION_PAIRING_REQUEST); StreamObserver<PairingEventAnswer> pairingEventAnswerObserver = mBumble.security() - .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) + .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(), + TimeUnit.MILLISECONDS) .onPairing(mPairingEventStreamObserver); - assertThat(mBumbleDevice.createBond(BluetoothDevice.TRANSPORT_BREDR)).isTrue(); + assertThat(mBumbleDevice.createBond(BluetoothDevice.TRANSPORT_BREDR)). + isTrue(); - verifyIntentReceivedUnordered( + intentReceiver.verifyReceived( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), - hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)); - verifyIntentReceived( + hasExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.BOND_BONDING)); + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_ACL_CONNECTED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), - hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR)); - verifyIntentReceivedUnordered( + hasExtra(BluetoothDevice.EXTRA_TRANSPORT, + BluetoothDevice.TRANSPORT_BREDR)); + intentReceiver.verifyReceived( hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra( @@ -985,18 +895,18 @@ public class PairingTest { PairingEvent pairingEvent = mPairingEventStreamObserver.iterator().next(); assertThat(pairingEvent.hasJustWorks()).isTrue(); pairingEventAnswerObserver.onNext( - PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build()); + PairingEventAnswer.newBuilder().setEvent(pairingEvent) + .setConfirm(true).build()); // Ensure that pairing succeeds - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), - hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)); + hasExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.BOND_BONDED)); - unregisterIntentActions( - BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothDevice.ACTION_ACL_CONNECTED, - BluetoothDevice.ACTION_PAIRING_REQUEST); + /* Unregisters all intent actions registered in this function */ + intentReceiver.close(); } private void testStep_restartBt() { @@ -1006,9 +916,13 @@ public class PairingTest { /* Starts outgoing GATT service discovery and incoming LE pairing in parallel */ private StreamObserverSpliterator<SecureResponse> - helper_OutgoingGattServiceDiscoveryWithIncomingLePairing() { - // Setup intent filters - registerIntentActions( + helper_OutgoingGattServiceDiscoveryWithIncomingLePairing( + IntentReceiver parentIntentReceiver) { + // Register new actions specific to this helper function + IntentReceiver intentReceiver = + IntentReceiver.updateNewIntentActionsInParentReceiver( + parentIntentReceiver, + sTargetContext, BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST, BluetoothDevice.ACTION_UUID, @@ -1027,7 +941,8 @@ public class PairingTest { } // Start GATT service discovery, this will establish LE ACL - assertThat(mBumbleDevice.fetchUuidsWithSdp(BluetoothDevice.TRANSPORT_LE)).isTrue(); + assertThat(mBumbleDevice.fetchUuidsWithSdp(BluetoothDevice.TRANSPORT_LE)) + .isTrue(); // Make Bumble connectable AdvertiseResponse advertiseResponse = @@ -1041,12 +956,13 @@ public class PairingTest { .next(); // Todo: Unexpected empty ACTION_UUID intent is generated - verifyIntentReceivedUnordered(hasAction(BluetoothDevice.ACTION_UUID)); + intentReceiver.verifyReceived(hasAction(BluetoothDevice.ACTION_UUID)); // Wait for connection on Android - verifyIntentReceivedUnordered( + intentReceiver.verifyReceived( hasAction(BluetoothDevice.ACTION_ACL_CONNECTED), - hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE)); + hasExtra(BluetoothDevice.EXTRA_TRANSPORT, + BluetoothDevice.TRANSPORT_LE)); // Start pairing from Bumble StreamObserverSpliterator<SecureResponse> responseObserver = @@ -1061,11 +977,12 @@ public class PairingTest { // Wait for incoming pairing notification on Android // TODO: Order of these events is not deterministic - verifyIntentReceivedUnordered( + intentReceiver.verifyReceived( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), - hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)); - verifyIntentReceivedUnordered( + hasExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.BOND_BONDING)); + intentReceiver.verifyReceived( hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra( @@ -1076,7 +993,7 @@ public class PairingTest { assertThat(mBumbleDevice.setPairingConfirmation(true)).isTrue(); // Wait for pairing approval notification on Android - verifyIntentReceivedUnordered( + intentReceiver.verifyReceived( 2, hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), @@ -1086,134 +1003,142 @@ public class PairingTest { // Wait for GATT service discovery to complete on Android // so that ACTION_UUID is received here. - verifyIntentReceivedUnordered( + intentReceiver.verifyReceived( hasAction(BluetoothDevice.ACTION_UUID), - hasExtra(BluetoothDevice.EXTRA_UUID, Matchers.hasItemInArray(BATTERY_UUID))); - - unregisterIntentActions( - BluetoothDevice.ACTION_BOND_STATE_CHANGED, - BluetoothDevice.ACTION_PAIRING_REQUEST, - BluetoothDevice.ACTION_UUID, - BluetoothDevice.ACTION_ACL_CONNECTED); + hasExtra(BluetoothDevice.EXTRA_UUID, + Matchers.hasItemInArray(BATTERY_UUID))); + intentReceiver.close(); return responseObserver; } - private void removeBond(BluetoothDevice device) { - registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + private void testStep_RemoveBond(IntentReceiver parentIntentReceiver, + BluetoothDevice device) { + IntentReceiver intentReceiver = + IntentReceiver.updateNewIntentActionsInParentReceiver( + parentIntentReceiver, + sTargetContext, + BluetoothDevice.ACTION_BOND_STATE_CHANGED); assertThat(device.removeBond()).isTrue(); - verifyIntentReceived( + intentReceiver.verifyReceivedOrdered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, device), - hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)); + hasExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.BOND_NONE)); - unregisterIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + intentReceiver.close(); } - @SafeVarargs - private void verifyIntentReceived(Matcher<Intent>... matchers) { - mInOrder.verify(mReceiver, timeout(BOND_INTENT_TIMEOUT.toMillis())) - .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers))); + private BluetoothProfile getProfileProxy(int profile) { + sAdapter.getProfileProxy(sTargetContext, mProfileServiceListener, profile); + ArgumentCaptor<BluetoothProfile> proxyCaptor = + ArgumentCaptor.forClass(BluetoothProfile.class); + verify(mProfileServiceListener, timeout(BOND_INTENT_TIMEOUT.toMillis())) + .onServiceConnected(eq(profile), proxyCaptor.capture()); + return proxyCaptor.getValue(); } - @SafeVarargs - private void verifyIntentReceivedUnordered(int num, Matcher<Intent>... matchers) { - verify(mReceiver, timeout(BOND_INTENT_TIMEOUT.toMillis()).times(num)) - .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers))); - } + private void testStep_BondLe(IntentReceiver parentIntentReceiver, + BluetoothDevice device, OwnAddressType ownAddressType) { + IntentReceiver intentReceiver = + IntentReceiver.updateNewIntentActionsInParentReceiver( + parentIntentReceiver, + sTargetContext, + BluetoothDevice.ACTION_BOND_STATE_CHANGED, + BluetoothDevice.ACTION_ACL_CONNECTED, + BluetoothDevice.ACTION_PAIRING_REQUEST); - @SafeVarargs - private void verifyIntentReceivedUnordered(Matcher<Intent>... matchers) { - verifyIntentReceivedUnordered(1, matchers); - } + mBumble.gattBlocking() + .registerService( + GattProto.RegisterServiceRequest.newBuilder() + .setService( + GattProto.GattServiceParams.newBuilder() + .setUuid(BATTERY_UUID.toString()) + .build()) + .build()); + mBumble.gattBlocking() + .registerService( + GattProto.RegisterServiceRequest.newBuilder() + .setService( + GattProto.GattServiceParams.newBuilder() + .setUuid(HOGP_UUID.toString()) + .build()) + .build()); - /** - * Helper function to add reference count to registered intent actions - * - * @param actions new intent actions to add. If the array is empty, it is a no-op. - */ - private void registerIntentActions(String... actions) { - if (actions.length == 0) { - return; - } - if (getTotalActionRegistrationCounts() > 0) { - Log.d(TAG, "registerIntentActions(): unregister ALL intents"); - sTargetContext.unregisterReceiver(mReceiver); - } - for (String action : actions) { - mActionRegistrationCounts.merge(action, 1, Integer::sum); - } - IntentFilter filter = new IntentFilter(); - mActionRegistrationCounts.entrySet().stream() - .filter(entry -> entry.getValue() > 0) - .forEach( - entry -> { - Log.d( - TAG, - "registerIntentActions(): Registering action = " - + entry.getKey()); - filter.addAction(entry.getKey()); - }); - sTargetContext.registerReceiver(mReceiver, filter); - } + mBumble.hostBlocking() + .advertise( + AdvertiseRequest.newBuilder() + .setLegacy(true) + .setConnectable(true) + .setOwnAddressType(ownAddressType) + .build()); - /** - * Helper function to reduce reference count to registered intent actions If total reference - * count is zero after removal, no broadcast receiver will be registered. - * - * @param actions intent actions to be removed. If some action is not registered, it is no-op - * for that action. If the actions array is empty, it is also a no-op. - */ - private void unregisterIntentActions(String... actions) { - if (actions.length == 0) { - return; - } - if (getTotalActionRegistrationCounts() <= 0) { - return; - } - Log.d(TAG, "unregisterIntentActions(): unregister ALL intents"); - sTargetContext.unregisterReceiver(mReceiver); - for (String action : actions) { - if (!mActionRegistrationCounts.containsKey(action)) { - continue; - } - mActionRegistrationCounts.put(action, mActionRegistrationCounts.get(action) - 1); - if (mActionRegistrationCounts.get(action) <= 0) { - mActionRegistrationCounts.remove(action); - } - } - if (getTotalActionRegistrationCounts() > 0) { - IntentFilter filter = new IntentFilter(); - mActionRegistrationCounts.entrySet().stream() - .filter(entry -> entry.getValue() > 0) - .forEach( - entry -> { - Log.d( - TAG, - "unregisterIntentActions(): Registering action = " - + entry.getKey()); - filter.addAction(entry.getKey()); - }); - sTargetContext.registerReceiver(mReceiver, filter); - } - } + StreamObserver<PairingEventAnswer> pairingEventAnswerObserver = + mBumble.security() + .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(), + TimeUnit.MILLISECONDS) + .onPairing(mPairingEventStreamObserver); - /** - * Get sum of reference count from all registered actions - * - * @return sum of reference count from all registered actions - */ - private int getTotalActionRegistrationCounts() { - return mActionRegistrationCounts.values().stream().reduce(0, Integer::sum); + assertThat(device.createBond(BluetoothDevice.TRANSPORT_LE)).isTrue(); + + intentReceiver.verifyReceived( + hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), + hasExtra(BluetoothDevice.EXTRA_DEVICE, device), + hasExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.BOND_BONDING)); + intentReceiver.verifyReceivedOrdered( + hasAction(BluetoothDevice.ACTION_ACL_CONNECTED), + hasExtra(BluetoothDevice.EXTRA_DEVICE, device), + hasExtra(BluetoothDevice.EXTRA_TRANSPORT, + BluetoothDevice.TRANSPORT_LE)); + intentReceiver.verifyReceived( + hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), + hasExtra(BluetoothDevice.EXTRA_DEVICE, device), + hasExtra( + BluetoothDevice.EXTRA_PAIRING_VARIANT, + BluetoothDevice.PAIRING_VARIANT_CONSENT)); + + // Approve pairing from Android + assertThat(device.setPairingConfirmation(true)).isTrue(); + + PairingEvent pairingEvent = mPairingEventStreamObserver.iterator().next(); + assertThat(pairingEvent.hasJustWorks()).isTrue(); + pairingEventAnswerObserver.onNext( + PairingEventAnswer.newBuilder().setEvent(pairingEvent) + .setConfirm(true).build()); + + // Ensure that pairing succeeds + intentReceiver.verifyReceivedOrdered( + hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), + hasExtra(BluetoothDevice.EXTRA_DEVICE, device), + hasExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.BOND_BONDED)); + + intentReceiver.close(); } - private BluetoothProfile getProfileProxy(int profile) { - sAdapter.getProfileProxy(sTargetContext, mProfileServiceListener, profile); - ArgumentCaptor<BluetoothProfile> proxyCaptor = - ArgumentCaptor.forClass(BluetoothProfile.class); - verify(mProfileServiceListener, timeout(BOND_INTENT_TIMEOUT.toMillis())) - .onServiceConnected(eq(profile), proxyCaptor.capture()); - return proxyCaptor.getValue(); + private void doTestIdentityAddressWithType(BluetoothDevice device, + OwnAddressType ownAddressType) { + BluetoothAddress identityAddress = device.getIdentityAddressWithType(); + assertThat(identityAddress.getAddress()).isNull(); + assertThat(identityAddress.getAddressType()) + .isEqualTo(BluetoothDevice.ADDRESS_TYPE_UNKNOWN); + + /* + * Note: Since there was no IntentReceiver registered, passing the + * instance as NULL. But, if there is an instance already present, that + * must be passed instead of NULL. + */ + testStep_BondLe(null, device, ownAddressType); + assertThat(sAdapter.getBondedDevices()).contains(device); + + identityAddress = device.getIdentityAddressWithType(); + assertThat(identityAddress.getAddress()).isEqualTo(device.getAddress()); + assertThat(identityAddress.getAddressType()) + .isEqualTo( + ownAddressType == OwnAddressType.RANDOM + ? BluetoothDevice.ADDRESS_TYPE_RANDOM + : BluetoothDevice.ADDRESS_TYPE_PUBLIC); } } diff --git a/framework/tests/bumble/src/android/bluetooth/pairing/utils/IntentReceiver.java b/framework/tests/bumble/src/android/bluetooth/pairing/utils/IntentReceiver.java new file mode 100644 index 0000000000..ef8ab310dd --- /dev/null +++ b/framework/tests/bumble/src/android/bluetooth/pairing/utils/IntentReceiver.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth.pairing.utils; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +import com.google.common.collect.Iterators; +import org.hamcrest.Matcher; +import org.hamcrest.core.AllOf; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.hamcrest.MockitoHamcrest; + +import java.time.Duration; +import java.util.Arrays; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +/** + * IntentReceiver helps in managing the Intents received through the Broadcast + * receiver, with specific intent actions registered. + * It uses Builder pattern for instance creation, and also allows setting up + * a custom listener's onReceive(). + * + * Use the following way to create an instance of the IntentReceiver. + * IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext, + * BluetoothDevice.ACTION_1, + * BluetoothDevice.ACTION_2) + * .setIntentListener(--) // optional + * .setIntentTimeout(--) // optional + * .build(); + * + * Ordered and unordered verification mechanisms are also provided through public methods. + */ + +public class IntentReceiver { + private static final String TAG = IntentReceiver.class.getSimpleName(); + + /** Interface for listening & processing the received intents */ + public interface IntentListener { + /** + * Callback for receiving intents + * + * @param intent Received intent + */ + void onReceive(Intent intent); + } + + @Mock private BroadcastReceiver mReceiver; + + /** Intent timeout value, can be configured through constructor, or setter method */ + private final Duration mIntentTimeout; + + /** To verify the received intents in-order */ + private final InOrder mInOrder; + private final Context mContext; + private final String[] mIntentStrings; + private final Deque<IntentFilter> mDqIntentFilter; + /* + * Note: Since we are using Builder pattern, also add new variables added + * to the Builder class + */ + + /** Listener for the received intents */ + private final IntentListener mIntentListener; + + /** + * Creates an Intent receiver from the builder instance + * Note: This is a private constructor, so always prepare IntentReceiver's + * instance through Builder(). + * + * @param builder Pre-built builder instance + */ + private IntentReceiver(Builder builder) { + this.mIntentTimeout = builder.mIntentTimeout; + this.mContext = builder.mContext; + this.mIntentStrings = builder.mIntentStrings; + this.mIntentListener = builder.mIntentListener; + + /* Perform other calls required for instantiation */ + MockitoAnnotations.initMocks(this); + mInOrder = inOrder(mReceiver); + mDqIntentFilter = new ArrayDeque<>(); + mDqIntentFilter.addFirst(prepareIntentFilter(mIntentStrings)); + + setupListener(); + registerReceiver(); + } + + /** Private constructor to avoid creation of IntentReceiver instance directly */ + private IntentReceiver() { + mIntentTimeout = null; + mInOrder = null; + mContext = null; + mIntentStrings = null; + mDqIntentFilter = null; + mIntentListener = null; + } + + /** + * Builder class which helps in avoiding overloading constructors (as the class grows) + * Usage: + * new IntentReceiver.Builder(ARGS) + * .setterMethods() **Optional calls, as these are default params + * .build(); + */ + public static class Builder { + /** + * Add all the instance variables from IntentReceiver, + * which needs to be initiated from the constructor, + * with either default, or user provided value. + */ + private final Context mContext; + private final String[] mIntentStrings; + + /** Non-final variables as there are setters available */ + private Duration mIntentTimeout; + private IntentListener mIntentListener; + + /** + * Private default constructor to avoid creation of Builder default + * instance directly as we need some instance variables to be initiated + * with user defined values. + */ + private Builder() { + mContext = null; + mIntentStrings = null; + } + + /** + * Creates a Builder instance with following required params + * + * @param context Context + * @param intentStrings Array of intents to filter and register + */ + public Builder(@NonNull Context context, String... intentStrings) { + mContext = context; + mIntentStrings = requireNonNull(intentStrings, + "IntentReceiver.Builder(): Intent string cannot be null"); + + if (mIntentStrings.length == 0) { + throw new RuntimeException("IntentReceiver.Builder(): No intents to register"); + } + + /* Default values for remaining vars */ + mIntentTimeout = Duration.ofSeconds(10); + mIntentListener = null; + } + + public Builder setIntentListener(IntentListener intentListener) { + mIntentListener = intentListener; + return this; + } + + public Builder setIntentTimeout(Duration intentTimeout) { + mIntentTimeout = intentTimeout; + return this; + } + + /** + * Builds and returns the IntentReceiver object with all the passed, + * and default params supplied to Builder(). + */ + public IntentReceiver build() { + return new IntentReceiver(this); + } + } + + /** + * Verifies if the intent is received in order + * + * @param matchers Matchers + */ + public void verifyReceivedOrdered(Matcher<Intent>... matchers) { + mInOrder.verify(mReceiver, timeout(mIntentTimeout.toMillis())) + .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers))); + } + + /** + * Verifies if requested number of intents are received (unordered) + * + * @param num Number of intents + * @param matchers Matchers + */ + public void verifyReceived(int num, Matcher<Intent>... matchers) { + verify(mReceiver, timeout(mIntentTimeout.toMillis()).times(num)) + .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers))); + } + + /** + * Verifies if the intent is received (unordered) + * + * @param matchers Matchers + */ + public void verifyReceived(Matcher<Intent>... matchers) { + verifyReceived(1, matchers); + } + + /** + * This function will make sure that the instance is properly cleared + * based on the registered actions. + * Note: This function MUST be called before returning from the caller function, + * as this either unregisters the latest registered actions, or free resources. + */ + public void close() { + Log.d(TAG, "close(): " + mDqIntentFilter.size()); + + /* More than 1 IntentFilters are present */ + if(mDqIntentFilter.size() > 1) { + /* + * It represents there are IntentFilters present to be rolled back. + * So, unregister and roll back to previous IntentFilter. + */ + unregisterRecentAllIntentActions(); + } + else { + /* + * It represents that this close() is called in the scope of creation of + * the object, and hence there is only 1 IntentFilter which is present. + * So, we can safely close this instance. + */ + verifyNoMoreInteractions(); + unregisterReceiver(); + } + } + + /** + * Registers the new actions passed as argument. + * 1. Unregister the receiver, and in turn old IntentFilter. + * 2. Creates a new IntentFilter from the String[], and treat that as latest. + * 3. Registers the new IntentFilter with the receiver to the current context. + */ + public void registerIntentActions(String... intentStrings) { + IntentFilter intentFilter = prepareIntentFilter(intentStrings); + + unregisterReceiver(); + /* Pushes the new intentFilter to top to make it the latest registered */ + mDqIntentFilter.addFirst(intentFilter); + registerReceiver(); + } + + /** + * Helper function to register intent actions, and get the IntentReceiver + * instance. + * + * @param parentIntentReceiver IntentReceiver instance from the parent test caller + * This should be `null` if there is no parent IntentReceiver instance. + * @param targetContext Context instance + * @param intentStrings Intent actions string array + * + * This should be used to register new intent actions in a testStep + * function always. + */ + public static IntentReceiver updateNewIntentActionsInParentReceiver( + IntentReceiver parentIntentReceiver, Context targetContext, String... intentStrings) { + /* + * If parentIntentReceiver is NULL, it indicates that the caller + * is a fresh test/testStep and a new IntentReceiver will be returned. + * else, update the intent actions and return the same instance. + */ + // Create a new instance for the current test/testStep function. + if(parentIntentReceiver == null) + return new IntentReceiver.Builder(targetContext, intentStrings) + .build(); + + /* Update the intent actions in the parent IntentReceiver instance */ + parentIntentReceiver.registerIntentActions(intentStrings); + return parentIntentReceiver; + } + + /** Helper functions are added below, usually private */ + + /** Registers the listener for the received intents, and perform a custom logic as required */ + private void setupListener() { + doAnswer( + inv -> { + Log.d( + TAG, + "onReceive(): intent=" + + Arrays.toString(inv.getArguments())); + + if (mIntentListener == null) return null; + + Intent intent = inv.getArgument(1); + + /* Custom `onReceive` will be provided by the caller */ + mIntentListener.onReceive(intent); + return null; + }) + .when(mReceiver) + .onReceive(any(), any()); + } + + private IntentFilter prepareIntentFilter(String... intentStrings) { + IntentFilter intentFilter = new IntentFilter(); + for (String intentString : intentStrings) { + intentFilter.addAction(intentString); + } + + return intentFilter; + } + + /** + * Registers the latest intent filter which is at the deque.peekFirst() + * Note: The mDqIntentFilter must not be empty here. + */ + private void registerReceiver() { + Log.d(TAG, "registerReceiver(): Registering for intents: " + + getActionsFromIntentFilter(mDqIntentFilter.peekFirst())); + + /* ArrayDeque should not be empty at all while registering a receiver */ + assertThat(mDqIntentFilter.isEmpty()).isFalse(); + mContext.registerReceiver(mReceiver, + (IntentFilter)mDqIntentFilter.peekFirst()); + } + + /** + * Unregisters the receiver from the list of active receivers. + * Also, we can now re-use the same receiver, or register a new + * receiver with the same or different intent filter, the old + * registration is no longer valid. + * Source: Intents and intent filters (Android Developers) + */ + private void unregisterReceiver() { + Log.d(TAG, "unregisterReceiver()"); + mContext.unregisterReceiver(mReceiver); + } + + /** Verifies that no more intents are received */ + private void verifyNoMoreInteractions() { + Log.d(TAG, "verifyNoMoreInteractions()"); + Mockito.verifyNoMoreInteractions(mReceiver); + } + + /** + * Registers the new actions passed as argument. + * 1. Unregister the receiver, and in turn new IntentFilter. + * 2. Pops the new IntentFilter to roll-back to the old one. + * 3. Registers the old IntentFilter with the receiver to the current context. + */ + private void unregisterRecentAllIntentActions() { + assertThat(mDqIntentFilter.isEmpty()).isFalse(); + + unregisterReceiver(); + /* Restores the previous intent filter, and discard the latest */ + mDqIntentFilter.removeFirst(); + registerReceiver(); + } + + /** + * Helper function to get the actions from the IntentFilter + * + * @param intentFilter IntentFilter instance + * + * This is a helper function to get the actions from the IntentFilter, + * and return as a String. + */ + private String getActionsFromIntentFilter( + IntentFilter intentFilter) { + Iterator<String> iterator = intentFilter.actionsIterator(); + StringBuilder allIntentActions = new StringBuilder(); + while (iterator.hasNext()) { + allIntentActions.append(iterator.next() + ", "); + } + + return allIntentActions.toString(); + } +}
\ No newline at end of file diff --git a/offload/hal/Android.bp b/offload/hal/Android.bp new file mode 100644 index 0000000000..91324d0d94 --- /dev/null +++ b/offload/hal/Android.bp @@ -0,0 +1,48 @@ +// Copyright 2025, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +rust_library { + name: "libbluetooth_offload_hal", + vendor_available: true, + crate_name: "bluetooth_offload_hal", + crate_root: "lib.rs", + edition: "2021", + rustlibs: [ + "android.hardware.bluetooth-V1-rust", + "libbinder_rs", + "libbluetooth_offload_hci", + "liblog_rust", + "liblogger", + ], + visibility: [ + "//hardware/interfaces/bluetooth:__subpackages__", + "//packages/modules/Bluetooth/offload:__subpackages__", + ], +} + +cc_library_headers { + name: "libbluetooth_offload_hal_headers", + vendor_available: true, + host_supported: true, + export_include_dirs: [ + "include", + ], + visibility: [ + "//hardware/interfaces/bluetooth:__subpackages__", + ], +} diff --git a/offload/hal/ffi.rs b/offload/hal/ffi.rs new file mode 100644 index 0000000000..762e3c9d6b --- /dev/null +++ b/offload/hal/ffi.rs @@ -0,0 +1,279 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::{ffi::c_void, slice}; +use std::sync::{Mutex, RwLock}; + +/// Callbacks from C to Rust +/// `handle` is allocated as an `Option<T: Callbacks>`; It must be valid from the +/// `CInterface.initialize()` call to the `CInterface.close()` call. This value +/// is returned as the first parameter to all other functions. +/// To prevent scheduling issues from the HAL Implementer, we enforce the validity +/// until the end of `Ffi<T>` instance; aka until the end of process life. +#[repr(C)] +#[allow(dead_code)] +pub struct CCallbacks { + handle: *const c_void, + initialization_complete: unsafe extern "C" fn(*mut c_void, CStatus), + event_received: unsafe extern "C" fn(*mut c_void, *const u8, usize), + acl_received: unsafe extern "C" fn(*mut c_void, *const u8, usize), + sco_received: unsafe extern "C" fn(*mut c_void, *const u8, usize), + iso_received: unsafe extern "C" fn(*mut c_void, *const u8, usize), +} + +/// C Interface called from Rust +/// `handle` is a pointer initialized by the C code and passed to all other functions. +/// `callbacks` is only valid during the `initialize()` call. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct CInterface { + handle: *mut c_void, + initialize: unsafe extern "C" fn(handle: *mut c_void, callbacks: *const CCallbacks), + close: unsafe extern "C" fn(handle: *mut c_void), + send_command: unsafe extern "C" fn(handle: *mut c_void, data: *const u8, len: usize), + send_acl: unsafe extern "C" fn(handle: *mut c_void, data: *const u8, len: usize), + send_sco: unsafe extern "C" fn(handle: *mut c_void, data: *const u8, len: usize), + send_iso: unsafe extern "C" fn(handle: *mut c_void, data: *const u8, len: usize), +} + +//SAFETY: CInterface is safe to send between threads because we require the C code +// which initialises it to only use pointers to functions which are safe +// to call from any thread. +unsafe impl Send for CInterface {} + +#[repr(C)] +#[allow(dead_code)] +#[derive(Debug, PartialEq)] +pub(crate) enum CStatus { + Success, + AlreadyInitialized, + UnableToOpenInterface, + HardwareInitializationError, + Unknown, +} + +pub(crate) trait Callbacks: DataCallbacks { + fn initialization_complete(&self, status: CStatus); +} + +pub(crate) trait DataCallbacks: Send + Sync { + fn event_received(&self, data: &[u8]); + fn acl_received(&self, data: &[u8]); + fn sco_received(&self, data: &[u8]); + fn iso_received(&self, data: &[u8]); +} + +pub(crate) struct Ffi<T: Callbacks> { + intf: Mutex<CInterface>, + wrapper: RwLock<Option<T>>, +} + +impl<T: Callbacks> Ffi<T> { + pub(crate) fn new(intf: CInterface) -> Self { + Self { intf: Mutex::new(intf), wrapper: RwLock::new(None) } + } + + pub(crate) fn initialize(&self, client: T) { + let intf = self.intf.lock().unwrap(); + self.set_client(client); + + // SAFETY: The C Code has initialized the `CInterface` with a valid + // function pointer. + unsafe { + (intf.initialize)(intf.handle, &CCallbacks::new(&self.wrapper)); + } + } + + pub(crate) fn send_command(&self, data: &[u8]) { + let intf = self.intf.lock().unwrap(); + + // SAFETY: The C Code has initialized the `CInterface` with a valid + // function pointer and an initialized `handle`. + unsafe { + (intf.send_command)(intf.handle, data.as_ptr(), data.len()); + } + } + + pub(crate) fn send_acl(&self, data: &[u8]) { + let intf = self.intf.lock().unwrap(); + + // SAFETY: The C Code has initialized the `CInterface` with a valid + // function pointer and an initialized `handle`. + unsafe { + (intf.send_acl)(intf.handle, data.as_ptr(), data.len()); + } + } + + pub(crate) fn send_iso(&self, data: &[u8]) { + let intf = self.intf.lock().unwrap(); + + // SAFETY: The C Code has initialized the `CInterface` with a valid + // function pointer and an initialized `handle`. + unsafe { + (intf.send_iso)(intf.handle, data.as_ptr(), data.len()); + } + } + + pub(crate) fn send_sco(&self, data: &[u8]) { + let intf = self.intf.lock().unwrap(); + + // SAFETY: The C Code has initialized the `CInterface` with a valid + // function pointer and an initialized `handle`. + unsafe { + (intf.send_sco)(intf.handle, data.as_ptr(), data.len()); + } + } + + pub(crate) fn close(&self) { + let intf = self.intf.lock().unwrap(); + + // SAFETY: The C Code has initialized the `CInterface` with a valid + // function pointer and an initialized `handle`. + unsafe { + (intf.close)(intf.handle); + } + self.remove_client(); + } + + fn set_client(&self, client: T) { + *self.wrapper.write().unwrap() = Some(client); + } + + fn remove_client(&self) { + *self.wrapper.write().unwrap() = None; + } +} + +impl CCallbacks { + fn new<T: Callbacks>(wrapper: &RwLock<Option<T>>) -> Self { + Self { + handle: (wrapper as *const RwLock<Option<T>>).cast(), + initialization_complete: Self::initialization_complete::<T>, + event_received: Self::event_received::<T>, + acl_received: Self::acl_received::<T>, + sco_received: Self::sco_received::<T>, + iso_received: Self::iso_received::<T>, + } + } + + /// #Safety + /// + /// `handle` must be a valid pointer previously passed to the corresponding `initialize()`, + /// and not yet destroyed (this is in fact an `RwLock<Option<T>>`). + unsafe fn unwrap_client<T: Callbacks, F: FnOnce(&T)>(handle: *mut c_void, f: F) { + let wrapper: *const RwLock<Option<T>> = handle.cast(); + + // SAFETY: The `handle` points the `RwLock<Option<T>>` wrapper object; it was allocated + // at the creation of the `Ffi` object and remain alive until its destruction. + if let Some(client) = unsafe { &*(*wrapper).read().unwrap() } { + f(client); + } else { + log::error!("FFI Callback called in bad state"); + } + } + + /// #Safety + /// + /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle` + unsafe extern "C" fn initialization_complete<T: Callbacks>( + handle: *mut c_void, + status: CStatus, + ) { + // SAFETY: The vendor HAL returns `handle` pointing `wrapper` object which has + // the same lifetime as the base `Ffi` instance. + unsafe { + Self::unwrap_client(handle, |client: &T| client.initialization_complete(status)); + } + } + + /// #Safety + /// + /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle`. + /// `data` must be a valid pointer to at least `len` bytes of memory, which remains valid and + /// is not mutated for the duration of this call. + unsafe extern "C" fn event_received<T: Callbacks>( + handle: *mut c_void, + data: *const u8, + len: usize, + ) { + // SAFETY: The C code returns `handle` pointing `wrapper` object which has + // the same lifetime as the base `Ffi` instance. `data` points to a buffer + // of `len` bytes valid until the function returns. + unsafe { + Self::unwrap_client(handle, |client: &T| { + client.event_received(slice::from_raw_parts(data, len)) + }); + } + } + + /// #Safety + /// + /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle`. + /// `data` must be a valid pointer to at least `len` bytes of memory, which remains valid and + /// is not mutated for the duration of this call. + unsafe extern "C" fn acl_received<T: Callbacks>( + handle: *mut c_void, + data: *const u8, + len: usize, + ) { + // SAFETY: The C code returns `handle` pointing `wrapper` object which has + // the same lifetime as the base `Ffi` instance. `data` points to a buffer + // of `len` bytes valid until the function returns. + unsafe { + Self::unwrap_client(handle, |client: &T| { + client.acl_received(slice::from_raw_parts(data, len)) + }); + } + } + + /// #Safety + /// + /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle`. + /// `data` must be a valid pointer to at least `len` bytes of memory, which remains valid and + /// is not mutated for the duration of this call. + unsafe extern "C" fn sco_received<T: Callbacks>( + handle: *mut c_void, + data: *const u8, + len: usize, + ) { + // SAFETY: The C code returns `handle` pointing `wrapper` object which has + // the same lifetime as the base `Ffi` instance. `data` points to a buffer + // of `len` bytes valid until the function returns. + unsafe { + Self::unwrap_client(handle, |client: &T| { + client.sco_received(slice::from_raw_parts(data, len)) + }); + } + } + + /// #Safety + /// + /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle`. + /// `data` must be a valid pointer to at least `len` bytes of memory, which remains valid and + /// is not mutated for the duration of this call. + unsafe extern "C" fn iso_received<T: Callbacks>( + handle: *mut c_void, + data: *const u8, + len: usize, + ) { + // SAFETY: The C code returns `handle` pointing `wrapper` object which has + // the same lifetime as the base `Ffi` instance. `data` points to a buffer + // of `len` bytes valid until the function returns. + unsafe { + Self::unwrap_client(handle, |client: &T| { + client.iso_received(slice::from_raw_parts(data, len)) + }); + } + } +} diff --git a/offload/hal/include/hal/ffi.h b/offload/hal/include/hal/ffi.h new file mode 100644 index 0000000000..e066c68a09 --- /dev/null +++ b/offload/hal/include/hal/ffi.h @@ -0,0 +1,62 @@ +/** + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +extern "C" { + +#include <stddef.h> +#include <stdint.h> + +/** + * Callabcks from C to Rust + * The given `handle` must be passed as the first parameter of all functions. + * The functions can be called from `hal_interface.initialize()` call to + * `hal_interface.close()` call. + */ + +enum HalStatus { + STATUS_SUCCESS, + STATUS_ALREADY_INITIALIZED, + STATUS_UNABLE_TO_OPEN_INTERFACE, + STATUS_HARDWARE_INITIALIZATION_ERROR, + STATUS_UNKNOWN, +}; + +struct hal_callbacks { + void *handle; + void (*initialization_complete)(const void *handle, enum HalStatus); + void (*event_received)(const void *handle, const uint8_t *data, size_t len); + void (*acl_received)(const void *handle, const uint8_t *data, size_t len); + void (*sco_received)(const void *handle, const uint8_t *data, size_t len); + void (*iso_received)(const void *handle, const uint8_t *data, size_t len); +}; + +/** + * Interface from Rust to C + * The `handle` value is passed as the first parameter of all functions. + * Theses functions can be called from different threads, but NOT concurrently. + * Locking over `handle` is not necessary. + */ + +struct hal_interface { + void *handle; + void (*initialize)(void *handle, const struct hal_callbacks *); + void (*close)(void *handle); + void (*send_command)(void *handle, const uint8_t *data, size_t len); + void (*send_acl)(void *handle, const uint8_t *data, size_t len); + void (*send_sco)(void *handle, const uint8_t *data, size_t len); + void (*send_iso)(void *handle, const uint8_t *data, size_t len); +}; +} diff --git a/offload/hal/lib.rs b/offload/hal/lib.rs new file mode 100644 index 0000000000..76730428fa --- /dev/null +++ b/offload/hal/lib.rs @@ -0,0 +1,22 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! HCI HAL Binder implementation with proxy integration +//! The Binder HAL interface is replicated as C Interface in `ffi` module + +mod ffi; +mod service; + +pub use ffi::{CCallbacks, CInterface}; +pub use service::HciHalProxy; diff --git a/offload/hal/service.rs b/offload/hal/service.rs new file mode 100644 index 0000000000..779be6da8c --- /dev/null +++ b/offload/hal/service.rs @@ -0,0 +1,245 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::ffi::{CInterface, CStatus, Callbacks, DataCallbacks, Ffi}; +use android_hardware_bluetooth::aidl::android::hardware::bluetooth::{ + IBluetoothHci::IBluetoothHci, IBluetoothHciCallbacks::IBluetoothHciCallbacks, Status::Status, +}; +use binder::{DeathRecipient, ExceptionCode, Interface, Result as BinderResult, Strong}; +use bluetooth_offload_hci::{Module, ModuleBuilder}; +use std::sync::{Arc, RwLock}; + +/// Service Implementation of AIDL interface `hardware/interface/bluetoot/aidl`, +/// including a proxy interface usable by third party modules. +pub struct HciHalProxy { + modules: Vec<Box<dyn ModuleBuilder>>, + ffi: Arc<Ffi<FfiCallbacks>>, + state: Arc<RwLock<State>>, +} + +struct FfiCallbacks { + callbacks: Strong<dyn IBluetoothHciCallbacks>, + proxy: Arc<dyn Module>, + state: Arc<RwLock<State>>, +} + +struct SinkModule<T: Callbacks> { + ffi: Arc<Ffi<T>>, + callbacks: Strong<dyn IBluetoothHciCallbacks>, +} + +enum State { + Closed, + Opening { ffi: Arc<Ffi<FfiCallbacks>>, proxy: Arc<dyn Module> }, + Opened { proxy: Arc<dyn Module>, _death_recipient: DeathRecipient }, +} + +impl Interface for HciHalProxy {} + +impl HciHalProxy { + /// Create the HAL Proxy interface binded to the Bluetooth HCI HAL interface. + pub fn new(modules: Vec<Box<dyn ModuleBuilder>>, cintf: CInterface) -> Self { + Self { + modules, + ffi: Arc::new(Ffi::new(cintf)), + state: Arc::new(RwLock::new(State::Closed)), + } + } +} + +impl IBluetoothHci for HciHalProxy { + fn initialize(&self, callbacks: &Strong<dyn IBluetoothHciCallbacks>) -> BinderResult<()> { + let (ffi, callbacks) = { + let mut state = self.state.write().unwrap(); + + if !matches!(*state, State::Closed) { + let _ = callbacks.initializationComplete(Status::ALREADY_INITIALIZED); + return Ok(()); + } + + let mut proxy: Arc<dyn Module> = + Arc::new(SinkModule::new(self.ffi.clone(), callbacks.clone())); + for m in self.modules.iter().rev() { + proxy = m.build(proxy); + } + let callbacks = FfiCallbacks::new(callbacks.clone(), proxy.clone(), self.state.clone()); + + *state = State::Opening { ffi: self.ffi.clone(), proxy: proxy.clone() }; + (self.ffi.clone(), callbacks) + }; + + ffi.initialize(callbacks); + Ok(()) + } + + fn close(&self) -> BinderResult<()> { + *self.state.write().unwrap() = State::Closed; + self.ffi.close(); + Ok(()) + } + + fn sendHciCommand(&self, data: &[u8]) -> BinderResult<()> { + let State::Opened { ref proxy, .. } = *self.state.read().unwrap() else { + return Err(ExceptionCode::ILLEGAL_STATE.into()); + }; + + proxy.out_cmd(data); + Ok(()) + } + + fn sendAclData(&self, data: &[u8]) -> BinderResult<()> { + let State::Opened { ref proxy, .. } = *self.state.read().unwrap() else { + return Err(ExceptionCode::ILLEGAL_STATE.into()); + }; + + proxy.out_acl(data); + Ok(()) + } + + fn sendScoData(&self, data: &[u8]) -> BinderResult<()> { + let State::Opened { ref proxy, .. } = *self.state.read().unwrap() else { + return Err(ExceptionCode::ILLEGAL_STATE.into()); + }; + + proxy.out_sco(data); + Ok(()) + } + + fn sendIsoData(&self, data: &[u8]) -> BinderResult<()> { + let State::Opened { ref proxy, .. } = *self.state.read().unwrap() else { + return Err(ExceptionCode::ILLEGAL_STATE.into()); + }; + + proxy.out_iso(data); + Ok(()) + } +} + +impl<T: Callbacks> SinkModule<T> { + pub(crate) fn new(ffi: Arc<Ffi<T>>, callbacks: Strong<dyn IBluetoothHciCallbacks>) -> Self { + Self { ffi, callbacks } + } +} + +impl<T: Callbacks> Module for SinkModule<T> { + fn next(&self) -> &dyn Module { + unreachable!() + } + + fn out_cmd(&self, data: &[u8]) { + self.ffi.send_command(data); + } + fn out_acl(&self, data: &[u8]) { + self.ffi.send_acl(data); + } + fn out_iso(&self, data: &[u8]) { + self.ffi.send_iso(data); + } + fn out_sco(&self, data: &[u8]) { + self.ffi.send_sco(data); + } + + fn in_evt(&self, data: &[u8]) { + if let Err(e) = self.callbacks.hciEventReceived(data) { + log::error!("Cannot send event to client: {:?}", e); + } + } + fn in_acl(&self, data: &[u8]) { + if let Err(e) = self.callbacks.aclDataReceived(data) { + log::error!("Cannot send ACL to client: {:?}", e); + } + } + fn in_sco(&self, data: &[u8]) { + if let Err(e) = self.callbacks.scoDataReceived(data) { + log::error!("Cannot send SCO to client: {:?}", e); + } + } + fn in_iso(&self, data: &[u8]) { + if let Err(e) = self.callbacks.isoDataReceived(data) { + log::error!("Cannot send ISO to client: {:?}", e); + } + } +} + +impl FfiCallbacks { + fn new( + callbacks: Strong<dyn IBluetoothHciCallbacks>, + proxy: Arc<dyn Module>, + state: Arc<RwLock<State>>, + ) -> Self { + Self { callbacks, proxy, state } + } +} + +impl Callbacks for FfiCallbacks { + fn initialization_complete(&self, status: CStatus) { + let mut state = self.state.write().unwrap(); + match status { + CStatus::Success => { + let State::Opening { ref ffi, ref proxy } = *state else { + panic!("Initialization completed called in bad state"); + }; + + *state = State::Opened { + proxy: proxy.clone(), + _death_recipient: { + let (ffi, state) = (ffi.clone(), self.state.clone()); + DeathRecipient::new(move || { + log::info!("Bluetooth stack has died"); + *state.write().unwrap() = State::Closed; + ffi.close(); + }) + }, + }; + } + + CStatus::AlreadyInitialized => panic!("Initialization completed called in bad state"), + _ => *state = State::Closed, + }; + + if let Err(e) = self.callbacks.initializationComplete(status.into()) { + log::error!("Cannot call-back client: {:?}", e); + } + } +} + +impl DataCallbacks for FfiCallbacks { + fn event_received(&self, data: &[u8]) { + self.proxy.in_evt(data); + } + + fn acl_received(&self, data: &[u8]) { + self.proxy.in_acl(data); + } + + fn sco_received(&self, data: &[u8]) { + self.proxy.in_sco(data); + } + + fn iso_received(&self, data: &[u8]) { + self.proxy.in_iso(data); + } +} + +impl From<CStatus> for Status { + fn from(value: CStatus) -> Self { + match value { + CStatus::Success => Status::SUCCESS, + CStatus::AlreadyInitialized => Status::ALREADY_INITIALIZED, + CStatus::UnableToOpenInterface => Status::UNABLE_TO_OPEN_INTERFACE, + CStatus::HardwareInitializationError => Status::HARDWARE_INITIALIZATION_ERROR, + CStatus::Unknown => Status::UNKNOWN, + } + } +} diff --git a/offload/hci/Android.bp b/offload/hci/Android.bp new file mode 100644 index 0000000000..323e9245b6 --- /dev/null +++ b/offload/hci/Android.bp @@ -0,0 +1,53 @@ +// Copyright 2025, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +rust_proc_macro { + name: "libbluetooth_offload_hci_derive", + crate_name: "bluetooth_offload_hci_derive", + crate_root: "derive/lib.rs", + edition: "2021", + rustlibs: [ + "libproc_macro2", + "libquote", + "libsyn", + ], +} + +rust_defaults { + name: "bluetooth_offload_hci_defaults", + crate_root: "lib.rs", + crate_name: "bluetooth_offload_hci", + edition: "2021", + proc_macros: [ + "libbluetooth_offload_hci_derive", + ], + visibility: [ + "//packages/modules/Bluetooth/offload:__subpackages__", + ], +} + +rust_library { + name: "libbluetooth_offload_hci", + defaults: ["bluetooth_offload_hci_defaults"], + vendor_available: true, +} + +rust_test_host { + name: "libbluetooth_offload_hci_test", + defaults: ["bluetooth_offload_hci_defaults"], +} diff --git a/offload/hci/command.rs b/offload/hci/command.rs new file mode 100644 index 0000000000..a4bbe6f701 --- /dev/null +++ b/offload/hci/command.rs @@ -0,0 +1,544 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::derive::{Read, Write}; +use crate::reader::{Read, Reader}; +use crate::writer::{pack, Write, Writer}; + +/// HCI Command, as defined in Part E - 5.4.1 +#[derive(Debug)] +pub enum Command { + /// 7.3.2 Reset Command + Reset(Reset), + /// 7.8.97 LE Set CIG Parameters + LeSetCigParameters(LeSetCigParameters), + /// 7.8.99 LE Create CIS + LeCreateCis(LeCreateCis), + /// 7.8.100 LE Remove CIG + LeRemoveCig(LeRemoveCig), + /// 7.8.103 LE Create BIG + LeCreateBig(LeCreateBig), + /// 7.8.109 LE Setup ISO Data Path + LeSetupIsoDataPath(LeSetupIsoDataPath), + /// 7.8.110 LE Remove ISO Data Path + LeRemoveIsoDataPath(LeRemoveIsoDataPath), + /// Unknown command + Unknown(OpCode), +} + +/// HCI Command Return Parameters +#[derive(Debug, Read, Write)] +pub enum ReturnParameters { + /// 7.3.2 Reset Command + Reset(ResetComplete), + /// 7.8.2 LE Read Buffer Size [V1] + LeReadBufferSizeV1(LeReadBufferSizeV1Complete), + /// 7.8.2 LE Read Buffer Size [V2] + LeReadBufferSizeV2(LeReadBufferSizeV2Complete), + /// 7.8.97 LE Set CIG Parameters + LeSetCigParameters(LeSetCigParametersComplete), + /// 7.8.100 LE Remove CIG + LeRemoveCig(LeRemoveCigComplete), + /// 7.8.109 LE Setup ISO Data Path + LeSetupIsoDataPath(LeIsoDataPathComplete), + /// 7.8.110 LE Remove ISO Data Path + LeRemoveIsoDataPath(LeIsoDataPathComplete), + /// Unknown command + Unknown(OpCode), +} + +impl Command { + /// Read an HCI Command packet + pub fn from_bytes(data: &[u8]) -> Result<Self, Option<OpCode>> { + fn parse_packet(data: &[u8]) -> Option<(OpCode, Reader)> { + let mut r = Reader::new(data); + let opcode = r.read()?; + let len = r.read_u8()? as usize; + Some((opcode, Reader::new(r.get(len)?))) + } + + let Some((opcode, mut r)) = parse_packet(data) else { + return Err(None); + }; + Self::dispatch_read(opcode, &mut r).ok_or(Some(opcode)) + } + + fn dispatch_read(opcode: OpCode, r: &mut Reader) -> Option<Command> { + Some(match opcode { + Reset::OPCODE => Self::Reset(r.read()?), + LeSetCigParameters::OPCODE => Self::LeSetCigParameters(r.read()?), + LeCreateCis::OPCODE => Self::LeCreateCis(r.read()?), + LeRemoveCig::OPCODE => Self::LeRemoveCig(r.read()?), + LeCreateBig::OPCODE => Self::LeCreateBig(r.read()?), + LeSetupIsoDataPath::OPCODE => Self::LeSetupIsoDataPath(r.read()?), + LeRemoveIsoDataPath::OPCODE => Self::LeRemoveIsoDataPath(r.read()?), + opcode => Self::Unknown(opcode), + }) + } + + fn to_bytes<T: CommandOpCode + Write>(command: &T) -> Vec<u8> { + let mut w = Writer::new(Vec::with_capacity(3 + 255)); + w.write(&T::OPCODE); + w.write_u8(0); + w.write(command); + + let mut vec = w.into_vec(); + vec[2] = (vec.len() - 3).try_into().unwrap(); + vec + } +} + +/// OpCode of HCI Command, as defined in Part E - 5.4.1 +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct OpCode(u16); + +impl OpCode { + /// OpCode from OpCode Group Field (OGF) and OpCode Command Field (OCF). + pub const fn from(ogf: u16, ocf: u16) -> Self { + Self(pack!((ocf, 10), (ogf, 6))) + } +} + +impl From<u16> for OpCode { + fn from(v: u16) -> Self { + OpCode(v) + } +} + +impl Read for OpCode { + fn read(r: &mut Reader) -> Option<Self> { + Some(r.read_u16()?.into()) + } +} + +impl Write for OpCode { + fn write(&self, w: &mut Writer) { + w.write_u16(self.0) + } +} + +/// Define command OpCode +pub trait CommandOpCode { + /// OpCode of the command + const OPCODE: OpCode; +} + +/// Build command from definition +pub trait CommandToBytes: CommandOpCode + Write { + /// Output the HCI Command packet + fn to_bytes(&self) -> Vec<u8> + where + Self: Sized + CommandOpCode + Write; +} + +pub use defs::*; + +#[allow(missing_docs)] +#[rustfmt::skip] +mod defs { + +use super::*; +use crate::derive::CommandToBytes; +use crate::status::*; + +#[cfg(test)] +use crate::{Event, EventToBytes}; + + +// 7.3.2 Reset Command + +impl CommandOpCode for Reset { + const OPCODE: OpCode = OpCode::from(0x03, 0x003); +} + +#[derive(Debug, Read, Write, CommandToBytes)] +pub struct Reset {} + +#[derive(Debug, Read, Write)] +pub struct ResetComplete { + pub status: Status, +} + +#[test] +fn test_reset() { + let dump = [0x03, 0x0c, 0x00]; + let Ok(Command::Reset(c)) = Command::from_bytes(&dump) else { panic!() }; + assert_eq!(c.to_bytes(), &dump[..]); +} + +#[test] +fn test_reset_complete() { + let dump = [0x0e, 0x04, 0x01, 0x03, 0x0c, 0x00]; + let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + let ReturnParameters::Reset(ref p) = e.return_parameters else { panic!() }; + assert_eq!(p.status, Status::Success); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.8.2 LE Read Buffer Size + +impl CommandOpCode for LeReadBufferSizeV1 { + const OPCODE: OpCode = OpCode::from(0x08, 0x002); +} + +#[derive(Debug)] +pub struct LeReadBufferSizeV1; + +#[derive(Debug, Read, Write)] +pub struct LeReadBufferSizeV1Complete { + pub status: Status, + pub le_acl_data_packet_length: u16, + pub total_num_le_acl_data_packets: u8, +} + +#[test] +fn test_le_read_buffer_size_v1_complete() { + let dump = [0x0e, 0x07, 0x01, 0x02, 0x20, 0x00, 0xfb, 0x00, 0x0f]; + let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + let ReturnParameters::LeReadBufferSizeV1(ref p) = e.return_parameters else { panic!() }; + assert_eq!(p.status, Status::Success); + assert_eq!(p.le_acl_data_packet_length, 251); + assert_eq!(p.total_num_le_acl_data_packets, 15); + assert_eq!(e.to_bytes(), &dump[..]); +} + +impl CommandOpCode for LeReadBufferSizeV2 { + const OPCODE: OpCode = OpCode::from(0x08, 0x060); +} + +#[derive(Debug)] +pub struct LeReadBufferSizeV2; + +#[derive(Debug, Read, Write)] +pub struct LeReadBufferSizeV2Complete { + pub status: Status, + pub le_acl_data_packet_length: u16, + pub total_num_le_acl_data_packets: u8, + pub iso_data_packet_length: u16, + pub total_num_iso_data_packets: u8, +} + +#[test] +fn test_le_read_buffer_size_v2_complete() { + let dump = [0x0e, 0x0a, 0x01, 0x60, 0x20, 0x00, 0xfb, 0x00, 0x0f, 0xfd, 0x03, 0x18]; + let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + let ReturnParameters::LeReadBufferSizeV2(ref p) = e.return_parameters else { panic!() }; + assert_eq!(p.status, Status::Success); + assert_eq!(p.le_acl_data_packet_length, 251); + assert_eq!(p.total_num_le_acl_data_packets, 15); + assert_eq!(p.iso_data_packet_length, 1021); + assert_eq!(p.total_num_iso_data_packets, 24); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.8.97 LE Set CIG Parameters + +impl CommandOpCode for LeSetCigParameters { + const OPCODE: OpCode = OpCode::from(0x08, 0x062); +} + +#[derive(Debug, Read, Write, CommandToBytes)] +pub struct LeSetCigParameters { + pub cig_id: u8, + #[N(3)] pub sdu_interval_c_to_p: u32, + #[N(3)] pub sdu_interval_p_to_c: u32, + pub worst_case_sca: u8, + pub packing: u8, + pub framing: u8, + pub max_transport_latency_c_to_p: u16, + pub max_transport_latency_p_to_c: u16, + pub cis: Vec<LeCisInCigParameters>, +} + +#[derive(Debug, Read, Write)] +pub struct LeCisInCigParameters { + pub cis_id: u8, + pub max_sdu_c_to_p: u16, + pub max_sdu_p_to_c: u16, + pub phy_c_to_p: u8, + pub phy_p_to_c: u8, + pub rtn_c_to_p: u8, + pub rtn_p_to_c: u8, +} + +#[test] +fn test_le_set_cig_parameters() { + let dump = [ + 0x62, 0x20, 0x21, 0x01, 0x10, 0x27, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x64, 0x00, 0x05, + 0x00, 0x02, 0x00, 0x78, 0x00, 0x00, 0x00, 0x02, 0x03, 0x0d, 0x00, 0x01, 0x78, 0x00, 0x00, 0x00, + 0x02, 0x03, 0x0d, 0x00 + ]; + let Ok(Command::LeSetCigParameters(c)) = Command::from_bytes(&dump) else { panic!() }; + assert_eq!(c.cig_id, 0x01); + assert_eq!(c.sdu_interval_c_to_p, 10_000); + assert_eq!(c.sdu_interval_p_to_c, 0); + assert_eq!(c.worst_case_sca, 1); + assert_eq!(c.packing, 0); + assert_eq!(c.framing, 0); + assert_eq!(c.max_transport_latency_c_to_p, 100); + assert_eq!(c.max_transport_latency_p_to_c, 5); + assert_eq!(c.cis.len(), 2); + assert_eq!(c.cis[0].cis_id, 0); + assert_eq!(c.cis[0].max_sdu_c_to_p, 120); + assert_eq!(c.cis[0].max_sdu_p_to_c, 0); + assert_eq!(c.cis[0].phy_c_to_p, 0x02); + assert_eq!(c.cis[0].phy_p_to_c, 0x03); + assert_eq!(c.cis[0].rtn_c_to_p, 13); + assert_eq!(c.cis[0].rtn_p_to_c, 0); + assert_eq!(c.cis[1].cis_id, 1); + assert_eq!(c.cis[1].max_sdu_c_to_p, 120); + assert_eq!(c.cis[1].max_sdu_p_to_c, 0); + assert_eq!(c.cis[1].phy_c_to_p, 0x02); + assert_eq!(c.cis[1].phy_p_to_c, 0x03); + assert_eq!(c.cis[1].rtn_c_to_p, 13); + assert_eq!(c.cis[1].rtn_p_to_c, 0); + assert_eq!(c.to_bytes(), &dump[..]); +} + +#[derive(Debug, Read, Write)] +pub struct LeSetCigParametersComplete { + pub status: Status, + pub cig_id: u8, + pub connection_handles: Vec<u16>, +} + +#[test] +fn test_le_set_cig_parameters_complete() { + let dump = [0x0e, 0x0a, 0x01, 0x62, 0x20, 0x00, 0x01, 0x02, 0x60, 0x00, 0x61, 0x00]; + let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + let ReturnParameters::LeSetCigParameters(ref p) = e.return_parameters else { panic!() }; + assert_eq!(p.status, Status::Success); + assert_eq!(p.cig_id, 1); + assert_eq!(p.connection_handles.len(), 2); + assert_eq!(p.connection_handles[0], 0x60); + assert_eq!(p.connection_handles[1], 0x61); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.8.99 LE Create CIS + +impl CommandOpCode for LeCreateCis { + const OPCODE: OpCode = OpCode::from(0x08, 0x064); +} + +#[derive(Debug, Read, Write, CommandToBytes)] +pub struct LeCreateCis { + pub connection_handles: Vec<CisAclConnectionHandle>, +} + +#[derive(Debug, Read, Write)] +pub struct CisAclConnectionHandle { + pub cis: u16, + pub acl: u16, +} + +#[test] +fn test_le_create_cis () { + let dump = [0x64, 0x20, 0x09, 0x02, 0x60, 0x00, 0x40, 0x00, 0x61, 0x00, 0x41, 0x00]; + let Ok(Command::LeCreateCis(c)) = Command::from_bytes(&dump) else { panic!() }; + assert_eq!(c.connection_handles.len(), 2); + assert_eq!(c.connection_handles[0].cis, 0x60); + assert_eq!(c.connection_handles[0].acl, 0x40); + assert_eq!(c.connection_handles[1].cis, 0x61); + assert_eq!(c.connection_handles[1].acl, 0x41); + assert_eq!(c.to_bytes(), &dump[..]); +} + + +// 7.8.100 LE Remove CIG + +impl CommandOpCode for LeRemoveCig { + const OPCODE: OpCode = OpCode::from(0x08, 0x065); +} + +#[derive(Debug, Read, Write, CommandToBytes)] +pub struct LeRemoveCig { + pub cig_id: u8, +} + +#[derive(Debug, Read, Write)] +pub struct LeRemoveCigComplete { + pub status: Status, + pub cig_id: u8, +} + +#[test] +fn test_le_remove_cig() { + let dump = [0x65, 0x20, 0x01, 0x01]; + let Ok(Command::LeRemoveCig(c)) = Command::from_bytes(&dump) else { panic!() }; + assert_eq!(c.cig_id, 0x01); + assert_eq!(c.to_bytes(), &dump[..]); +} + +#[test] +fn test_le_remove_cig_complete() { + let dump = [0x0e, 0x05, 0x01, 0x65, 0x20, 0x00, 0x01]; + let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + let ReturnParameters::LeRemoveCig(ref p) = e.return_parameters else { panic!() }; + assert_eq!(p.status, Status::Success); + assert_eq!(p.cig_id, 0x01); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.8.103 LE Create BIG + +impl CommandOpCode for LeCreateBig { + const OPCODE: OpCode = OpCode::from(0x08, 0x068); +} + +#[derive(Debug, Read, Write, CommandToBytes)] +pub struct LeCreateBig { + pub big_handle: u8, + pub advertising_handle: u8, + pub num_bis: u8, + #[N(3)] pub sdu_interval: u32, + pub max_sdu: u16, + pub max_transport_latency: u16, + pub rtn: u8, + pub phy: u8, + pub packing: u8, + pub framing: u8, + pub encryption: u8, + pub broadcast_code: [u8; 16], +} + +#[test] +fn test_le_create_big() { + let dump = [ + 0x68, 0x20, 0x1f, 0x00, 0x00, 0x02, 0x10, 0x27, 0x00, 0x78, 0x00, 0x3c, 0x00, 0x04, 0x02, 0x00, + 0x00, 0x01, 0x31, 0x32, 0x33, 0x34, 0x31, 0x32, 0x33, 0x34, 0x31, 0x32, 0x33, 0x34, 0x31, 0x32, + 0x33, 0x34 + ]; + let Ok(Command::LeCreateBig(c)) = Command::from_bytes(&dump) else { panic!() }; + assert_eq!(c.big_handle, 0x00); + assert_eq!(c.advertising_handle, 0x00); + assert_eq!(c.num_bis, 2); + assert_eq!(c.sdu_interval, 10_000); + assert_eq!(c.max_sdu, 120); + assert_eq!(c.max_transport_latency, 60); + assert_eq!(c.rtn, 4); + assert_eq!(c.phy, 0x02); + assert_eq!(c.packing, 0x00); + assert_eq!(c.framing, 0x00); + assert_eq!(c.encryption, 1); + assert_eq!(c.broadcast_code, [ + 0x31, 0x32, 0x33, 0x34, 0x31, 0x32, 0x33, 0x34, + 0x31, 0x32, 0x33, 0x34, 0x31, 0x32, 0x33, 0x34 + ]); + assert_eq!(c.to_bytes(), &dump[..]); +} + + +// 7.8.109 LE Setup ISO Data Path + +impl CommandOpCode for LeSetupIsoDataPath { + const OPCODE: OpCode = OpCode::from(0x08, 0x06e); +} + +#[derive(Debug, Read, Write, CommandToBytes)] +pub struct LeSetupIsoDataPath { + pub connection_handle: u16, + pub data_path_direction: LeDataPathDirection, + pub data_path_id: u8, + pub codec_id: LeCodecId, + #[N(3)] pub controller_delay: u32, + pub codec_configuration: Vec<u8>, +} + +#[derive(Debug, PartialEq, Read, Write)] +pub enum LeDataPathDirection { + Input = 0x00, + Output = 0x01, +} + +#[derive(Debug, Read, Write)] +pub struct LeCodecId { + pub coding_format: CodingFormat, + pub company_id: u16, + pub vendor_id: u16, +} + +#[derive(Debug, PartialEq, Read, Write)] +pub enum CodingFormat { + ULawLog = 0x00, + ALawLog = 0x01, + Cvsd = 0x02, + Transparent = 0x03, + LinearPcm = 0x04, + MSbc = 0x05, + Lc3 = 0x06, + G729A = 0x07, + VendorSpecific = 0xff, +} + +#[derive(Debug, Read, Write)] +pub struct LeIsoDataPathComplete { + pub status: Status, + pub connection_handle: u16, +} + +#[test] +fn test_le_setup_iso_data_path() { + let dump = [ + 0x6e, 0x20, 0x0d, 0x60, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]; + let Ok(Command::LeSetupIsoDataPath(c)) = Command::from_bytes(&dump) else { panic!() }; + assert_eq!(c.connection_handle, 0x60); + assert_eq!(c.data_path_direction, LeDataPathDirection::Input); + assert_eq!(c.data_path_id, 0x00); + assert_eq!(c.codec_id.coding_format, CodingFormat::Transparent); + assert_eq!(c.codec_id.company_id, 0); + assert_eq!(c.codec_id.vendor_id, 0); + assert_eq!(c.controller_delay, 0); + assert_eq!(c.codec_configuration.len(), 0); + assert_eq!(c.to_bytes(), &dump[..]); +} + +#[test] +fn test_le_setup_iso_data_path_complete() { + let dump = [0x0e, 0x06, 0x01, 0x6e, 0x20, 0x00, 0x60, 0x00]; + let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + let ReturnParameters::LeSetupIsoDataPath(ref p) = e.return_parameters else { panic!() }; + assert_eq!(p.status, Status::Success); + assert_eq!(p.connection_handle, 0x60); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.8.110 LE Remove ISO Data Path + +impl CommandOpCode for LeRemoveIsoDataPath { + const OPCODE: OpCode = OpCode::from(0x08, 0x06f); +} + +#[derive(Debug, Read, Write, CommandToBytes)] +pub struct LeRemoveIsoDataPath { + pub connection_handle: u16, + pub data_path_direction: u8, +} + +#[test] +fn test_le_remove_iso_data_path() { + let dump = [0x6f, 0x20, 0x03, 0x60, 0x00, 0x01]; + let Ok(Command::LeRemoveIsoDataPath(c)) = Command::from_bytes(&dump) else { panic!() }; + assert_eq!(c.connection_handle, 0x60); + assert_eq!(c.data_path_direction, 0x01); + assert_eq!(c.to_bytes(), &dump[..]); +} + +} diff --git a/offload/hci/data.rs b/offload/hci/data.rs new file mode 100644 index 0000000000..bb4a452a59 --- /dev/null +++ b/offload/hci/data.rs @@ -0,0 +1,204 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::reader::{unpack, Reader}; +use crate::writer::{pack, Write, Writer}; + +/// 5.4.5 ISO Data Packets + +/// Exchange of Isochronous Data between the Host and Controller +#[derive(Debug)] +pub struct IsoData<'a> { + /// Identify the connection + pub connection_handle: u16, + /// Fragmentation of the packet + pub sdu_fragment: IsoSduFragment, + /// Payload + pub payload: &'a [u8], +} + +/// Fragmentation indication of the SDU +#[derive(Debug)] +pub enum IsoSduFragment { + /// First SDU Fragment + First { + /// SDU Header + hdr: IsoSduHeader, + /// Last SDU fragment indication + is_last: bool, + }, + /// Continuous fragment + Continue { + /// Last SDU fragment indication + is_last: bool, + }, +} + +/// SDU Header information, when ISO Data in a first SDU fragment +#[derive(Debug, Default)] +pub struct IsoSduHeader { + /// Optional timestamp in microseconds + pub timestamp: Option<u32>, + /// Sequence number of the SDU + pub sequence_number: u16, + /// Total length of the SDU (sum of all fragments) + pub sdu_length: u16, + /// Only valid from Controller, indicate valid SDU data when 0 + pub status: u16, +} + +impl<'a> IsoData<'a> { + /// Read an HCI ISO Data packet + pub fn from_bytes(data: &'a [u8]) -> Option<Self> { + Self::parse(&mut Reader::new(data)) + } + + /// Output the HCI ISO Data packet + pub fn to_bytes(&self) -> Vec<u8> { + let mut w = Writer::new(Vec::with_capacity(12 + self.payload.len())); + w.write(self); + w.into_vec() + } + + /// New ISO Data packet, including a complete SDU + pub fn new(connection_handle: u16, sequence_number: u16, data: &'a [u8]) -> Self { + Self { + connection_handle, + sdu_fragment: IsoSduFragment::First { + hdr: IsoSduHeader { + sequence_number, + sdu_length: data.len().try_into().unwrap(), + ..Default::default() + }, + is_last: true, + }, + payload: data, + } + } + + fn parse(r: &mut Reader<'a>) -> Option<Self> { + let (connection_handle, pb_flag, ts_present) = unpack!(r.read_u16()?, (12, 2, 1)); + let data_len = unpack!(r.read_u16()?, 14) as usize; + + let sdu_fragment = match pb_flag { + 0b00 => IsoSduFragment::First { + hdr: IsoSduHeader::parse(r, ts_present != 0)?, + is_last: false, + }, + 0b10 => IsoSduFragment::First { + hdr: IsoSduHeader::parse(r, ts_present != 0)?, + is_last: true, + }, + 0b01 => IsoSduFragment::Continue { is_last: false }, + 0b11 => IsoSduFragment::Continue { is_last: true }, + _ => unreachable!(), + }; + let sdu_header_len = Self::sdu_header_len(&sdu_fragment); + if data_len < sdu_header_len { + return None; + } + + Some(Self { connection_handle, sdu_fragment, payload: r.get(data_len - sdu_header_len)? }) + } + + fn sdu_header_len(sdu_fragment: &IsoSduFragment) -> usize { + match sdu_fragment { + IsoSduFragment::First { ref hdr, .. } => 4 * (1 + hdr.timestamp.is_some() as usize), + IsoSduFragment::Continue { .. } => 0, + } + } +} + +impl Write for IsoData<'_> { + fn write(&self, w: &mut Writer) { + let (pb_flag, hdr) = match self.sdu_fragment { + IsoSduFragment::First { ref hdr, is_last: false } => (0b00, Some(hdr)), + IsoSduFragment::First { ref hdr, is_last: true } => (0b10, Some(hdr)), + IsoSduFragment::Continue { is_last: false } => (0b01, None), + IsoSduFragment::Continue { is_last: true } => (0b11, None), + }; + + let ts_present = hdr.is_some() && hdr.unwrap().timestamp.is_some(); + w.write_u16(pack!((self.connection_handle, 12), (pb_flag, 2), ((ts_present as u16), 1))); + + let packet_len = Self::sdu_header_len(&self.sdu_fragment) + self.payload.len(); + w.write_u16(pack!(u16::try_from(packet_len).unwrap(), 14)); + + if let Some(hdr) = hdr { + w.write(hdr); + } + w.put(self.payload); + } +} + +impl IsoSduHeader { + fn parse(r: &mut Reader, ts_present: bool) -> Option<Self> { + let timestamp = match ts_present { + true => Some(r.read_u32::<4>()?), + false => None, + }; + let sequence_number = r.read_u16()?; + let (sdu_length, _, status) = unpack!(r.read_u16()?, (12, 2, 2)); + Some(Self { timestamp, sequence_number, sdu_length, status }) + } +} + +impl Write for IsoSduHeader { + fn write(&self, w: &mut Writer) { + if let Some(timestamp) = self.timestamp { + w.write_u32::<4>(timestamp); + }; + w.write_u16(self.sequence_number); + w.write_u16(pack!((self.sdu_length, 12), (0, 2), (self.status, 2))); + } +} + +#[test] +fn test_iso_data() { + let dump = [ + 0x60, 0x60, 0x80, 0x00, 0x4d, 0xc8, 0xd0, 0x2f, 0x19, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xe0, 0x93, 0xe5, 0x28, 0x34, 0x00, 0x00, 0x04, + ]; + let Some(pkt) = IsoData::from_bytes(&dump) else { panic!() }; + assert_eq!(pkt.connection_handle, 0x060); + + let IsoSduFragment::First { ref hdr, is_last } = pkt.sdu_fragment else { panic!() }; + assert_eq!(hdr.timestamp, Some(802_211_917)); + assert_eq!(hdr.sequence_number, 793); + assert_eq!(hdr.sdu_length, 120); + assert!(is_last); + + assert_eq!( + pkt.payload, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xe0, 0x93, 0xe5, 0x28, 0x34, 0x00, 0x00, 0x04 + ] + ); + assert_eq!(pkt.to_bytes(), &dump[..]); +} diff --git a/offload/hci/derive/enum_data.rs b/offload/hci/derive/enum_data.rs new file mode 100644 index 0000000000..c758fd756c --- /dev/null +++ b/offload/hci/derive/enum_data.rs @@ -0,0 +1,95 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Derive of `hci::reader::Read` and `hci::writer::Write` traits on an `enum` +//! +//! ``` +//! #[derive(Read, Write)] +//! enum Example { +//! FirstVariant = 0, +//! SecondVariant = 1, +//! } +//! ``` +//! +//! Produces: +//! +//! ``` +//! impl Read for Example { +//! fn read(r: &mut Reader) -> Option<Self> { +//! match r.read_u8()? { +//! 0 => Some(Self::FirstVariant), +//! 1 => Some(Self::SecondVariant), +//! _ => None, +//! } +//! } +//! } +//! +//! impl Write for Example { +//! fn write(&self, w: &mut Writer) { +//! w.write_u8(match self { +//! Self::FirstVariant => 0, +//! Self::SecondVariant => 1, +//! }) +//! } +//! } +//! ``` + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{spanned::Spanned, Error}; + +pub(crate) fn derive_read(name: &syn::Ident, data: &syn::DataEnum) -> Result<TokenStream, Error> { + let mut variants: Vec<TokenStream> = Vec::new(); + for variant in &data.variants { + let ident = &variant.ident; + let Some((_, ref discriminant)) = variant.discriminant else { + return Err(Error::new(variant.span(), "Missing discriminant")); + }; + variants.push(quote! { #discriminant => Some(Self::#ident) }); + } + variants.push(quote! { _ => None }); + + Ok(quote! { + impl Read for #name { + fn read(r: &mut Reader) -> Option<Self> { + match r.read_u8()? { + #( #variants ),* + } + } + } + }) +} + +pub(crate) fn derive_write(name: &syn::Ident, data: &syn::DataEnum) -> Result<TokenStream, Error> { + let mut variants: Vec<TokenStream> = Vec::new(); + for variant in &data.variants { + let ident = &variant.ident; + let Some((_, ref discriminant)) = variant.discriminant else { + return Err(Error::new(variant.span(), "Missing discriminant")); + }; + variants.push(quote! { Self::#ident => #discriminant }); + } + + Ok(quote! { + impl Write for #name { + fn write(&self, w: &mut Writer) { + w.write_u8( + match self { + #( #variants ),* + } + ) + } + } + }) +} diff --git a/offload/hci/derive/lib.rs b/offload/hci/derive/lib.rs new file mode 100644 index 0000000000..97b5d15789 --- /dev/null +++ b/offload/hci/derive/lib.rs @@ -0,0 +1,89 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Derive of traits : +//! - `hci::reader::Read`, `hci::writer::Write` +//! - `hci::command::CommandToBytes` +//! - `hci::event::EventToBytes` + +extern crate proc_macro; +mod enum_data; +mod return_parameters; +mod struct_data; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, Error}; + +/// Derive of `hci::reader::Read` trait +#[proc_macro_derive(Read, attributes(N))] +pub fn derive_read(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as DeriveInput); + let (ident, data) = (&input.ident, &input.data); + let expanded = match (ident.to_string().as_str(), data) { + ("ReturnParameters", syn::Data::Enum(ref data)) => { + return_parameters::derive_read(ident, data) + } + (_, syn::Data::Enum(ref data)) => enum_data::derive_read(ident, data), + (_, syn::Data::Struct(ref data)) => struct_data::derive_read(ident, data), + (_, _) => panic!("Unsupported kind of input"), + } + .unwrap_or_else(Error::into_compile_error); + TokenStream::from(expanded) +} + +/// Derive of `hci::reader::Write` trait +#[proc_macro_derive(Write)] +pub fn derive_write(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as DeriveInput); + let (ident, data) = (&input.ident, &input.data); + let expanded = match (ident.to_string().as_str(), &data) { + ("ReturnParameters", syn::Data::Enum(ref data)) => { + return_parameters::derive_write(ident, data) + } + (_, syn::Data::Enum(ref data)) => enum_data::derive_write(ident, data), + (_, syn::Data::Struct(ref data)) => struct_data::derive_write(ident, data), + (_, _) => panic!("Unsupported kind of input"), + } + .unwrap_or_else(Error::into_compile_error); + TokenStream::from(expanded) +} + +/// Derive of `hci::command::CommandToBytes` +#[proc_macro_derive(CommandToBytes)] +pub fn derive_command_to_bytes(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as DeriveInput); + let name = &input.ident; + TokenStream::from(quote! { + impl CommandToBytes for #name { + fn to_bytes(&self) -> Vec<u8> { + Command::to_bytes(self) + } + } + }) +} + +/// Derive of `hci::command::EventToBytes` +#[proc_macro_derive(EventToBytes)] +pub fn derive_event_to_bytes(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as DeriveInput); + let name = &input.ident; + TokenStream::from(quote! { + impl EventToBytes for #name { + fn to_bytes(&self) -> Vec<u8> { + Event::to_bytes(self) + } + } + }) +} diff --git a/offload/hci/derive/return_parameters.rs b/offload/hci/derive/return_parameters.rs new file mode 100644 index 0000000000..20baf439c5 --- /dev/null +++ b/offload/hci/derive/return_parameters.rs @@ -0,0 +1,116 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Derive of `hci::reader::Read` and `hci::writer::Write` traits on +//! `enum returnReturnParameters`. +//! +//! ``` +//! #[derive(Read, Write)] +//! enum ReturnParameters { +//! CommandOne(CommandOneComplete), +//! CommandTwo(CommandTwoComplete), +//! LastIsDefault(OpCode), +//! } +//! ``` +//! +//! Produces: +//! +//! ``` +//! impl Read for ReturnParameters { +//! fn read(r: &mut Reader) -> Option<Self> { +//! Some(match r.read_u16()?.into() { +//! CommandOne::OPCODE => Self::CommandOne(r.read()?), +//! CommandTwo::OPCODE => Self::CommandTwo(r.read()?), +//! opcode => Self::LastIsDefault(opcode), +//! }) +//! } +//! } +//! +//! impl Write for ReturnParameters { +//! fn write(&self, w: &mut Writer) { +//! match self { +//! Self::CommandOne(p) => { +//! w.write(&CommandOne::OPCODE); +//! w.write(p); +//! } +//! Self::CommandTwo(p) => { +//! w.write(&CommandOne::OPCODE); +//! w.write(p); +//! } +//! Self::LastIsDefault(..) => panic!(), +//! }; +//! } +//! } +//! ``` + +use proc_macro2::TokenStream; +use quote::quote; +use syn::Error; + +pub(crate) fn derive_read(name: &syn::Ident, data: &syn::DataEnum) -> Result<TokenStream, Error> { + let mut variants = Vec::new(); + for (i, variant) in data.variants.iter().enumerate() { + let ident = &variant.ident; + + if i < data.variants.len() - 1 { + variants.push(quote! { + #ident::OPCODE => Self::#ident(r.read()?), + }); + } else { + variants.push(quote! { + opcode => Self::#ident(opcode), + }); + } + } + + Ok(quote! { + impl Read for #name { + fn read(r: &mut Reader) -> Option<Self> { + Some(match r.read_u16()?.into() { + #( #variants )* + }) + } + } + }) +} + +pub(crate) fn derive_write(name: &syn::Ident, data: &syn::DataEnum) -> Result<TokenStream, Error> { + let mut variants = Vec::new(); + for (i, variant) in data.variants.iter().enumerate() { + let ident = &variant.ident; + + if i < data.variants.len() - 1 { + variants.push(quote! { + Self::#ident(p) => { + w.write(&#ident::OPCODE); + w.write(p); + } + }); + } else { + variants.push(quote! { + Self::#ident(..) => panic!(), + }); + } + } + + Ok(quote! { + impl Write for #name { + fn write(&self, w: &mut Writer) { + match self { + #( #variants )* + }; + } + } + }) +} diff --git a/offload/hci/derive/struct_data.rs b/offload/hci/derive/struct_data.rs new file mode 100644 index 0000000000..757a71baec --- /dev/null +++ b/offload/hci/derive/struct_data.rs @@ -0,0 +1,178 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Derive of `hci::reader::Read` and `hci::writer::Write` traits on a `struct` +//! +//! ``` +//! #[derive(Read, Write)] +//! struct Example { +//! one_byte: u8, +//! two_bytes: u16, +//! #[N(3)] three_bytes: u32, +//! #[N(4)] four_bytes: u32, +//! bytes: [u8; 123], +//! other: OtherType, +//! } +//! ``` +//! +//! Produces: +//! +//! ``` +//! impl Read for Example { +//! fn read(r: &mut Reader) -> Option<Self> { +//! Some(Self { +//! one_byte: r.read_u8()?, +//! two_bytes: r.read_u16()?, +//! three_bytes: r.read_u32::<3>()?, +//! four_bytes: r.read_u32::<4>()?, +//! bytes: r.read_bytes()?, +//! other_type: r.read()?, +//! }) +//! } +//! } +//! +//! impl Write for Example { +//! fn write(&self, w: &mut Writer) { +//! w.write_u8(self.one_byte); +//! w.write_u16(self.two_bytes); +//! w.write_u32::<3>(self.three_bytes); +//! w.write_u32::<4>(self.four_bytes); +//! w.write_bytes(&self.bytes); +//! w.write(&self.other_type); +//! } +//! } +//! ``` + +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; +use syn::{spanned::Spanned, Error}; + +struct Attributes { + n: Option<usize>, +} + +impl Attributes { + fn parse(syn_attrs: &[syn::Attribute]) -> Result<Attributes, Error> { + let mut n = None; + for attr in syn_attrs.iter() { + match attr { + attr if attr.path().is_ident("N") => { + let lit: syn::LitInt = attr.parse_args()?; + n = Some(lit.base10_parse()?); + } + attr => return Err(Error::new(attr.span(), "Unrecognized attribute")), + } + } + Ok(Attributes { n }) + } +} + +pub(crate) fn derive_read(name: &syn::Ident, data: &syn::DataStruct) -> Result<TokenStream, Error> { + let mut fields = Vec::new(); + for field in &data.fields { + let ident = &field.ident.as_ref().unwrap(); + let attrs = Attributes::parse(&field.attrs)?; + let fn_token = match &field.ty { + syn::Type::Path(v) if v.path.is_ident("u8") => { + if attrs.n.unwrap_or(1) != 1 { + return Err(Error::new(v.span(), "Expected N(1) for type `u8`")); + } + quote_spanned! { v.span() => read_u8()? } + } + syn::Type::Path(v) if v.path.is_ident("u16") => { + if attrs.n.unwrap_or(2) != 2 { + return Err(Error::new(v.span(), "Expected N(2) for type `u16`")); + } + quote_spanned! { v.span() => read_u16()? } + } + syn::Type::Path(v) if v.path.is_ident("u32") => { + let Some(n) = attrs.n else { + return Err(Error::new(v.span(), "`N()` attribute required")); + }; + if n > 4 { + return Err(Error::new(v.span(), "Expected N(n <= 4)")); + } + quote_spanned! { v.span() => read_u32::<#n>()? } + } + syn::Type::Array(v) => match &*v.elem { + syn::Type::Path(v) if v.path.is_ident("u8") => { + quote_spanned! { v.span() => read_bytes()? } + } + _ => return Err(Error::new(v.elem.span(), "Only Byte array supported")), + }, + ty => quote_spanned! { ty.span() => read()? }, + }; + fields.push(quote! { #ident: r.#fn_token }); + } + + Ok(quote! { + impl Read for #name { + fn read(r: &mut Reader) -> Option<Self> { + Some(Self { + #( #fields ),* + }) + } + } + }) +} + +pub(crate) fn derive_write( + name: &syn::Ident, + data: &syn::DataStruct, +) -> Result<TokenStream, Error> { + let mut fields = Vec::new(); + for field in &data.fields { + let ident = &field.ident.as_ref().unwrap(); + let attrs = Attributes::parse(&field.attrs)?; + let fn_token = match &field.ty { + syn::Type::Path(v) if v.path.is_ident("u8") => { + if attrs.n.unwrap_or(1) != 1 { + return Err(Error::new(v.span(), "Expected N(1) for type `u8`")); + } + quote_spanned! { v.span() => write_u8(self.#ident) } + } + syn::Type::Path(v) if v.path.is_ident("u16") => { + if attrs.n.unwrap_or(2) != 2 { + return Err(Error::new(v.span(), "Expected N(2) for type `u16`")); + } + quote_spanned! { v.span() => write_u16(self.#ident) } + } + syn::Type::Path(v) if v.path.is_ident("u32") => { + let Some(n) = attrs.n else { + return Err(Error::new(v.span(), "`N()` attribute required")); + }; + if n > 4 { + return Err(Error::new(v.span(), "Expected N(n <= 4)")); + } + quote_spanned! { v.span() => write_u32::<#n>(self.#ident) } + } + syn::Type::Array(v) => match &*v.elem { + syn::Type::Path(v) if v.path.is_ident("u8") => { + quote_spanned! { v.span() => write_bytes(&self.#ident) } + } + _ => return Err(Error::new(v.elem.span(), "Only Byte array supported")), + }, + ty => quote_spanned! { ty.span() => write(&self.#ident) }, + }; + fields.push(quote! { w.#fn_token; }); + } + + Ok(quote! { + impl Write for #name { + fn write(&self, w: &mut Writer) { + #( #fields )* + } + } + }) +} diff --git a/offload/hci/event.rs b/offload/hci/event.rs new file mode 100644 index 0000000000..222d0e3546 --- /dev/null +++ b/offload/hci/event.rs @@ -0,0 +1,343 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::reader::{Read, Reader}; +use crate::writer::{Write, Writer}; + +/// HCI Event Packet, as defined in Part E - 5.4.4 +#[derive(Debug)] +pub enum Event { + /// 7.7.5 Disconnection Complete + DisconnectionComplete(DisconnectionComplete), + /// 7.7.14 Command Complete + CommandComplete(CommandComplete), + /// 7.7.15 Command Status + CommandStatus(CommandStatus), + /// 7.7.19 Number Of Completed Packets + NumberOfCompletedPackets(NumberOfCompletedPackets), + /// 7.7.65.25 LE CIS Established + LeCisEstablished(LeCisEstablished), + /// 7.7.65.27 LE Create BIG Complete + LeCreateBigComplete(LeCreateBigComplete), + /// 7.7.65.28 LE Terminate BIG Complete + LeTerminateBigComplete(LeTerminateBigComplete), + /// Unknown Event + Unknown(Code), +} + +impl Event { + /// Read an HCI Event packet + pub fn from_bytes(data: &[u8]) -> Result<Self, Option<Code>> { + fn parse_packet(data: &[u8]) -> Option<(Code, Reader)> { + let mut r = Reader::new(data); + let code = r.read_u8()?; + let len = r.read_u8()? as usize; + + let mut r = Reader::new(r.get(len)?); + let code = match code { + Code::LE_META => Code(Code::LE_META, Some(r.read_u8()?)), + _ => Code(code, None), + }; + + Some((code, r)) + } + + let Some((code, mut r)) = parse_packet(data) else { + return Err(None); + }; + Self::dispatch_read(code, &mut r).ok_or(Some(code)) + } + + fn dispatch_read(code: Code, r: &mut Reader) -> Option<Event> { + Some(match code { + CommandComplete::CODE => Self::CommandComplete(r.read()?), + CommandStatus::CODE => Self::CommandStatus(r.read()?), + DisconnectionComplete::CODE => Self::DisconnectionComplete(r.read()?), + NumberOfCompletedPackets::CODE => Self::NumberOfCompletedPackets(r.read()?), + LeCisEstablished::CODE => Self::LeCisEstablished(r.read()?), + LeCreateBigComplete::CODE => Self::LeCreateBigComplete(r.read()?), + LeTerminateBigComplete::CODE => Self::LeTerminateBigComplete(r.read()?), + code => Self::Unknown(code), + }) + } + + fn to_bytes<T: EventCode + Write>(event: &T) -> Vec<u8> { + let mut w = Writer::new(Vec::with_capacity(2 + 255)); + w.write_u8(T::CODE.0); + w.write_u8(0); + if let Some(sub_code) = T::CODE.1 { + w.write_u8(sub_code) + } + w.write(event); + + let mut vec = w.into_vec(); + vec[1] = (vec.len() - 2).try_into().unwrap(); + vec + } +} + +/// Code of HCI Event, as defined in Part E - 5.4.4 +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Code(u8, Option<u8>); + +impl Code { + const LE_META: u8 = 0x3e; +} + +/// Define event Code +pub trait EventCode { + /// Code of the event + const CODE: Code; +} + +/// Build event from definition +pub trait EventToBytes: EventCode + Write { + /// Output the HCI Event packet + fn to_bytes(&self) -> Vec<u8> + where + Self: Sized + EventCode + Write; +} + +pub use defs::*; + +#[allow(missing_docs)] +#[rustfmt::skip] +mod defs { + +use super::*; +use crate::derive::{Read, Write, EventToBytes}; +use crate::command::{OpCode, ReturnParameters}; +use crate::status::Status; + + +// 7.7.5 Disconnection Complete + +impl EventCode for DisconnectionComplete { + const CODE: Code = Code(0x05, None); +} + +#[derive(Debug, Read, Write, EventToBytes)] +pub struct DisconnectionComplete { + pub status: Status, + pub connection_handle: u16, + pub reason: u8, +} + +#[test] +fn test_disconnection_complete() { + let dump = [0x05, 0x04, 0x00, 0x60, 0x00, 0x16]; + let Ok(Event::DisconnectionComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + assert_eq!(e.status, Status::Success); + assert_eq!(e.connection_handle, 0x60); + assert_eq!(e.reason, 0x16); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.7.14 Command Complete + +impl EventCode for CommandComplete { + const CODE: Code = Code(0x0e, None); +} + +#[derive(Debug, Read, Write, EventToBytes)] +pub struct CommandComplete { + pub num_hci_command_packets: u8, + pub return_parameters: ReturnParameters, +} + +#[test] +fn test_command_complete() { + let dump = [0x0e, 0x04, 0x01, 0x03, 0x0c, 0x00]; + let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + assert_eq!(e.num_hci_command_packets, 1); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.7.15 Command Status + +impl EventCode for CommandStatus { + const CODE: Code = Code(0x0f, None); +} + +#[derive(Debug, Read, Write, EventToBytes)] +pub struct CommandStatus { + pub status: Status, + pub num_hci_command_packets: u8, + pub opcode: OpCode, +} + +#[test] +fn test_command_status() { + let dump = [0x0f, 0x04, 0x00, 0x01, 0x01, 0x04]; + let Ok(Event::CommandStatus(e)) = Event::from_bytes(&dump) else { panic!() }; + assert_eq!(e.status, Status::Success); + assert_eq!(e.num_hci_command_packets, 1); + assert_eq!(e.opcode, OpCode::from(0x01, 0x001)); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.7.19 Number Of Completed Packets + +impl EventCode for NumberOfCompletedPackets { + const CODE: Code = Code(0x13, None); +} + +#[derive(Debug, Read, Write, EventToBytes)] +pub struct NumberOfCompletedPackets { + pub handles: Vec<NumberOfCompletedPacketsHandle>, +} + +#[derive(Debug, Copy, Clone, Read, Write)] +pub struct NumberOfCompletedPacketsHandle { + pub connection_handle: u16, + pub num_completed_packets: u16, +} + +#[test] +fn test_number_of_completed_packets() { + let dump = [0x13, 0x09, 0x02, 0x40, 0x00, 0x01, 0x00, 0x41, 0x00, 0x01, 0x00]; + let Ok(Event::NumberOfCompletedPackets(e)) = Event::from_bytes(&dump) else { panic!() }; + assert_eq!(e.handles.len(), 2); + assert_eq!(e.handles[0].connection_handle, 0x40); + assert_eq!(e.handles[0].num_completed_packets, 1); + assert_eq!(e.handles[1].connection_handle, 0x41); + assert_eq!(e.handles[1].num_completed_packets, 1); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.7.65.25 LE CIS Established + +impl EventCode for LeCisEstablished { + const CODE: Code = Code(Code::LE_META, Some(0x19)); +} + +#[derive(Debug, Read, Write, EventToBytes)] +pub struct LeCisEstablished { + pub status: Status, + pub connection_handle: u16, + #[N(3)] pub cig_sync_delay: u32, + #[N(3)] pub cis_sync_delay: u32, + #[N(3)] pub transport_latency_c_to_p: u32, + #[N(3)] pub transport_latency_p_to_c: u32, + pub phy_c_to_p: u8, + pub phy_p_to_c: u8, + pub nse: u8, + pub bn_c_to_p: u8, + pub bn_p_to_c: u8, + pub ft_c_to_p: u8, + pub ft_p_to_c: u8, + pub max_pdu_c_to_p: u16, + pub max_pdu_p_to_c: u16, + pub iso_interval: u16, +} + +#[test] +fn test_le_cis_established() { + let dump = [ + 0x3e, 0x1d, 0x19, 0x00, 0x60, 0x00, 0x40, 0x2c, 0x00, 0x40, 0x2c, 0x00, 0xd0, 0x8b, 0x01, 0x60, + 0x7a, 0x00, 0x02, 0x02, 0x06, 0x02, 0x00, 0x05, 0x01, 0x78, 0x00, 0x00, 0x00, 0x10, 0x00 ]; + let Ok(Event::LeCisEstablished(e)) = Event::from_bytes(&dump) else { panic!() }; + assert_eq!(e.status, Status::Success); + assert_eq!(e.connection_handle, 0x60); + assert_eq!(e.cig_sync_delay, 11_328); + assert_eq!(e.cis_sync_delay, 11_328); + assert_eq!(e.transport_latency_c_to_p, 101_328); + assert_eq!(e.transport_latency_p_to_c, 31_328); + assert_eq!(e.phy_c_to_p, 0x02); + assert_eq!(e.phy_p_to_c, 0x02); + assert_eq!(e.nse, 6); + assert_eq!(e.bn_c_to_p, 2); + assert_eq!(e.bn_p_to_c, 0); + assert_eq!(e.ft_c_to_p, 5); + assert_eq!(e.ft_p_to_c, 1); + assert_eq!(e.max_pdu_c_to_p, 120); + assert_eq!(e.max_pdu_p_to_c, 0); + assert_eq!(e.iso_interval, 16); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.7.65.27 LE Create BIG Complete + +impl EventCode for LeCreateBigComplete { + const CODE: Code = Code(Code::LE_META, Some(0x1b)); +} + +#[derive(Debug, Read, Write, EventToBytes)] +pub struct LeCreateBigComplete { + pub status: Status, + pub big_handle: u8, + #[N(3)] pub big_sync_delay: u32, + #[N(3)] pub big_transport_latency: u32, + pub phy: u8, + pub nse: u8, + pub bn: u8, + pub pto: u8, + pub irc: u8, + pub max_pdu: u16, + pub iso_interval: u16, + pub bis_handles: Vec<u16>, +} + +#[test] +fn test_le_create_big_complete() { + let dump = [ + 0x3e, 0x17, 0x1b, 0x00, 0x00, 0x46, 0x50, 0x00, 0x66, 0x9e, 0x00, 0x02, 0x0f, 0x03, 0x00, 0x05, + 0x78, 0x00, 0x18, 0x00, 0x02, 0x00, 0x04, 0x01, 0x04 + ]; + let Ok(Event::LeCreateBigComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + assert_eq!(e.status, Status::Success); + assert_eq!(e.big_handle, 0x00); + assert_eq!(e.big_sync_delay, 20_550); + assert_eq!(e.big_transport_latency, 40_550); + assert_eq!(e.phy, 0x02); + assert_eq!(e.nse, 15); + assert_eq!(e.bn, 3); + assert_eq!(e.pto, 0); + assert_eq!(e.irc, 5); + assert_eq!(e.max_pdu, 120); + assert_eq!(e.iso_interval, 24); + assert_eq!(e.bis_handles.len(), 2); + assert_eq!(e.bis_handles[0], 0x400); + assert_eq!(e.bis_handles[1], 0x401); + assert_eq!(e.to_bytes(), &dump[..]); +} + + +// 7.7.65.28 LE Terminate BIG Complete + +impl EventCode for LeTerminateBigComplete { + const CODE: Code = Code(Code::LE_META, Some(0x1c)); +} + +#[derive(Debug, Read, Write, EventToBytes)] +pub struct LeTerminateBigComplete { + pub big_handle: u8, + pub reason: u8, +} + +#[test] +fn test_le_terminate_big_complete() { + let dump = [0x3e, 0x03, 0x1c, 0x00, 0x16]; + let Ok(Event::LeTerminateBigComplete(e)) = Event::from_bytes(&dump) else { panic!() }; + assert_eq!(e.big_handle, 0x00); + assert_eq!(e.reason, 0x16); + assert_eq!(e.to_bytes(), &dump[..]); +} + +} diff --git a/offload/hci/lib.rs b/offload/hci/lib.rs new file mode 100644 index 0000000000..a84e538fec --- /dev/null +++ b/offload/hci/lib.rs @@ -0,0 +1,78 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! HCI Proxy module implementation, along with +//! reading / writing helpers of Bluetooth HCI Commands, Events and Data encapsulations. + +use std::sync::Arc; + +/// Interface for building a module +pub trait ModuleBuilder: Send + Sync { + /// Build the module from the next module in the chain + fn build(&self, next_module: Arc<dyn Module>) -> Arc<dyn Module>; +} + +/// Interface of a an HCI proxy module +pub trait Module: Send + Sync { + /// Returns the next chained proxy module + fn next(&self) -> &dyn Module; + + /// HCI Command from Host to Controller + fn out_cmd(&self, data: &[u8]) { + self.next().out_cmd(data); + } + /// ACL Data from Host to Controller + fn out_acl(&self, data: &[u8]) { + self.next().out_acl(data); + } + /// SCO Data from Host to Controller + fn out_sco(&self, data: &[u8]) { + self.next().out_sco(data); + } + /// ISO Data from Host to Controller + fn out_iso(&self, data: &[u8]) { + self.next().out_iso(data); + } + + /// HCI Command from Controller to Host + fn in_evt(&self, data: &[u8]) { + self.next().in_evt(data); + } + /// ACL Data from Controller to Host + fn in_acl(&self, data: &[u8]) { + self.next().in_acl(data); + } + /// SCO Data from Controller to Host + fn in_sco(&self, data: &[u8]) { + self.next().in_sco(data); + } + /// ISO Data from Controller to Host + fn in_iso(&self, data: &[u8]) { + self.next().in_iso(data); + } +} + +use bluetooth_offload_hci_derive as derive; + +mod command; +mod data; +mod event; +mod reader; +mod status; +mod writer; + +pub use command::*; +pub use data::*; +pub use event::*; +pub use status::*; diff --git a/offload/hci/reader.rs b/offload/hci/reader.rs new file mode 100644 index 0000000000..98aea61735 --- /dev/null +++ b/offload/hci/reader.rs @@ -0,0 +1,99 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(crate) trait Read { + fn read(r: &mut Reader) -> Option<Self> + where + Self: Sized; +} + +pub(crate) struct Reader<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> Reader<'a> { + pub(crate) fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + pub(crate) fn get(&mut self, n: usize) -> Option<&'a [u8]> { + if self.pos + n > self.data.len() { + return None; + } + let old_pos = self.pos; + self.pos += n; + Some(&self.data[old_pos..self.pos]) + } + + pub(crate) fn read<T: Read>(&mut self) -> Option<T> { + T::read(self) + } + + pub(crate) fn read_u8(&mut self) -> Option<u8> { + Some(self.read_u32::<1>()? as u8) + } + + pub(crate) fn read_u16(&mut self) -> Option<u16> { + Some(self.read_u32::<2>()? as u16) + } + + pub(crate) fn read_u32<const N: usize>(&mut self) -> Option<u32> { + let data_it = self.get(N)?.iter().enumerate(); + Some(data_it.fold(0u32, |v, (i, byte)| v | (*byte as u32) << (i * 8))) + } + + pub(crate) fn read_bytes<const N: usize>(&mut self) -> Option<[u8; N]> { + Some(<[u8; N]>::try_from(self.get(N)?).unwrap()) + } +} + +impl Read for Vec<u8> { + fn read(r: &mut Reader) -> Option<Self> { + let len = r.read_u8()? as usize; + Some(Vec::from(r.get(len)?)) + } +} + +impl Read for Vec<u16> { + fn read(r: &mut Reader) -> Option<Self> { + let len = r.read_u8()? as usize; + let vec: Vec<_> = (0..len).map_while(|_| r.read_u16()).collect(); + Some(vec).take_if(|v| v.len() == len) + } +} + +impl<T: Read> Read for Vec<T> { + fn read(r: &mut Reader) -> Option<Self> { + let len = r.read_u8()? as usize; + let vec: Vec<_> = (0..len).map_while(|_| r.read()).collect(); + Some(vec).take_if(|v| v.len() == len) + } +} + +macro_rules! unpack { + ($v:expr, ($( $n:expr ),*)) => { + { + let mut _x = $v; + ($({ + let y = _x & ((1 << $n) - 1); + _x >>= $n; + y + }),*) + } + }; + ($v:expr, $n:expr) => { unpack!($v, ($n)) }; +} + +pub(crate) use unpack; diff --git a/offload/hci/status.rs b/offload/hci/status.rs new file mode 100644 index 0000000000..ee92d06bad --- /dev/null +++ b/offload/hci/status.rs @@ -0,0 +1,95 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::derive::{Read, Write}; +use crate::reader::{Read, Reader}; +use crate::writer::{Write, Writer}; + +/// Status / Error codes, as defined in Part F +#[derive(Debug, PartialEq, Read, Write)] +#[allow(missing_docs)] +pub enum Status { + Success = 0x00, + UnknownHciCommand = 0x01, + UnknownConnectionIdentifier = 0x02, + HardwareFailure = 0x03, + PageTimeout = 0x04, + AuthenticationFailure = 0x05, + PinorKeyMissing = 0x06, + MemoryCapacityExceeded = 0x07, + ConnectionTimeout = 0x08, + ConnectionLimitExceeded = 0x09, + SynchronousConnectionLimitExceeded = 0x0A, + ConnectionAlreadyExists = 0x0B, + CommandDisallowed = 0x0C, + ConnectionRejectedLimitedResources = 0x0D, + ConnectionRejectedSecurityReasons = 0x0E, + ConnectionRejectedUnacceptableBdAddr = 0x0F, + ConnectionAcceptTimeoutExceeded = 0x10, + UnsupportedFeatureOrParameterValue = 0x11, + InvalidHciCommandParameters = 0x12, + RemoteUserTerminatedConnection = 0x13, + RemoteDeviceTerminatedConnectionLowResources = 0x14, + RemoteDeviceTerminatedConnectionPowerOff = 0x15, + ConnectionTerminatedByLocalHost = 0x16, + RepeatedAttempts = 0x17, + PairingNotAllowed = 0x18, + UnknownLmpPdu = 0x19, + UnsupportedRemoteFeature = 0x1A, + ScoOffsetRejected = 0x1B, + ScoIntervalRejected = 0x1C, + ScoAirModeRejected = 0x1D, + InvalidLmpParameters = 0x1E, + UnspecifiedError = 0x1F, + UnsupportedLmpParameterValue = 0x20, + RoleChangeNotAllowed = 0x21, + LmpResponseTimeout = 0x22, + LmpErrorTransactionCollision = 0x23, + LmpPduNotAllowed = 0x24, + EncryptionModeNotAcceptable = 0x25, + LinkKeyCannotBeChanged = 0x26, + RequestedQosNotSupported = 0x27, + InstantPassed = 0x28, + PairingWithUnitKeyNotSupported = 0x29, + DifferentTransactionCollision = 0x2A, + ReservedForUse2B = 0x2B, + QosUnacceptableParameter = 0x2C, + QosRejected = 0x2D, + ChannelClassificationNotSupported = 0x2E, + InsufficientSecurity = 0x2F, + ParameterOutOfMandatoryRange = 0x30, + ReservedForUse31 = 0x31, + RoleSwitchPending = 0x32, + ReservedForUse33 = 0x33, + ReservedSlotViolation = 0x34, + RoleSwitchFailed = 0x35, + ExtendedInquiryResponseTooLarge = 0x36, + SecureSimplePairingNotSupportedByHost = 0x37, + HostBusy = 0x38, + ConnectionRejectedNoSuitableChannelFound = 0x39, + ControllerBusy = 0x3A, + UnacceptableConnectionParameters = 0x3B, + AdvertisingTimeout = 0x3C, + ConnectionTerminatedMicFailure = 0x3D, + ConnectionFailedEstablished = 0x3E, + PreviouslyUsed3F = 0x3F, + CoarseClockAdjustmentRejected = 0x40, + Type0SubmapNotDefined = 0x41, + UnknownAdvertisingIdentifier = 0x42, + LimitReached = 0x43, + OperationCancelledByHost = 0x44, + PacketTooLong = 0x45, + TooLate = 0x46, + TooEarly = 0x47, +} diff --git a/offload/hci/writer.rs b/offload/hci/writer.rs new file mode 100644 index 0000000000..ca0e8e274a --- /dev/null +++ b/offload/hci/writer.rs @@ -0,0 +1,103 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub trait Write { + fn write(&self, w: &mut Writer) + where + Self: Sized; +} + +pub struct Writer { + vec: Vec<u8>, +} + +impl Writer { + pub(crate) fn new(vec: Vec<u8>) -> Self { + Self { vec } + } + + pub(crate) fn into_vec(self) -> Vec<u8> { + self.vec + } + + pub(crate) fn put(&mut self, slice: &[u8]) { + self.vec.extend_from_slice(slice); + } + + pub(crate) fn write<T: Write>(&mut self, v: &T) { + v.write(self) + } + + pub(crate) fn write_u8(&mut self, v: u8) { + self.write_u32::<1>(v.into()); + } + + pub(crate) fn write_u16(&mut self, v: u16) { + self.write_u32::<2>(v.into()); + } + + pub(crate) fn write_u32<const N: usize>(&mut self, mut v: u32) { + for _ in 0..N { + self.vec.push((v & 0xff) as u8); + v >>= 8; + } + } + + pub(crate) fn write_bytes<const N: usize>(&mut self, bytes: &[u8; N]) { + self.put(bytes); + } +} + +impl Write for Vec<u8> { + fn write(&self, w: &mut Writer) { + w.write_u8(self.len().try_into().unwrap()); + w.put(self); + } +} + +impl Write for Vec<u16> { + fn write(&self, w: &mut Writer) { + w.write_u8(self.len().try_into().unwrap()); + for item in self { + w.write_u16(*item); + } + } +} + +impl<T: Write> Write for Vec<T> { + fn write(&self, w: &mut Writer) { + w.write_u8(self.len().try_into().unwrap()); + for item in self { + w.write(item); + } + } +} + +macro_rules! pack { + ( $( ($x:expr, $n:expr) ),* ) => { + { + let mut y = 0; + let mut _shl = 0; + $( + assert!($x & !((1 << $n) - 1) == 0); + y |= ($x << _shl); + _shl += $n; + )* + y + } + }; + ( $x:expr, $n:expr ) => { pack!(($x, $n)) }; +} + +pub(crate) use pack; diff --git a/offload/leaudio/aidl/Android.bp b/offload/leaudio/aidl/Android.bp new file mode 100644 index 0000000000..1e904d2a64 --- /dev/null +++ b/offload/leaudio/aidl/Android.bp @@ -0,0 +1,33 @@ +// Copyright 2025, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +aidl_interface { + name: "android.hardware.bluetooth.offload.leaudio", + vendor_available: true, + unstable: true, + srcs: ["android/hardware/bluetooth/offload/leaudio/*.aidl"], + backend: { + rust: { + enabled: true, + }, + }, + visibility: [ + "//packages/modules/Bluetooth/offload/leaudio:__subpackages__", + "//system/tools/aidl/build", + ], +} diff --git a/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxy.aidl b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxy.aidl new file mode 100644 index 0000000000..a9a1289181 --- /dev/null +++ b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxy.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.bluetooth.offload.leaudio; + +import android.hardware.bluetooth.offload.leaudio.IHciProxyCallbacks; + +/** + * Interface of the HCI-Proxy module + */ +interface IHciProxy { + + /** + * Register callbacks for events in the other direction + */ + void registerCallbacks(in IHciProxyCallbacks callbacks); + + /** + * Send an ISO packet on a `handle` + */ + oneway void sendPacket(in int handle, in int sequence_number, in byte[] data); +} diff --git a/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxyCallbacks.aidl b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxyCallbacks.aidl new file mode 100644 index 0000000000..619b22b9ea --- /dev/null +++ b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxyCallbacks.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.bluetooth.offload.leaudio; + +import android.hardware.bluetooth.offload.leaudio.StreamConfiguration; + +/** + * Interface from HCI-Proxy module to the audio module + */ +interface IHciProxyCallbacks { + + /** + * Start indication of a stream + */ + void startStream(in int handle, in StreamConfiguration configuration); + + /** + * Stop indication of a stream + */ + void stopStream(in int handle); +} diff --git a/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/StreamConfiguration.aidl b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/StreamConfiguration.aidl new file mode 100644 index 0000000000..944facb2ef --- /dev/null +++ b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/StreamConfiguration.aidl @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.bluetooth.offload.leaudio; + +/** + * Configuration relative to a stream + */ +parcelable StreamConfiguration { + + /** + * Constant ISO time transmission interval, in micro-seconds + */ + int isoIntervalUs; + + /** + * Constant SDU transmission interval, in micro-seconds + */ + int sduIntervalUs; + + /** + * Maximum size of a SDU + */ + int maxSduSize; + + /** + * Number of PDU's transmitted by ISO-Intervals + */ + int burstNumber; + + /** + * How many consecutive Isochronous Intervals can be used to transmit a PDU + */ + int flushTimeout; +} diff --git a/offload/leaudio/hci/Android.bp b/offload/leaudio/hci/Android.bp new file mode 100644 index 0000000000..e4f1b51881 --- /dev/null +++ b/offload/leaudio/hci/Android.bp @@ -0,0 +1,46 @@ +// Copyright 2025, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +rust_defaults { + name: "bluetooth_offload_leaudio_hci_defaults", + crate_root: "lib.rs", + crate_name: "bluetooth_offload_leaudio_hci", + edition: "2021", + rustlibs: [ + "android.hardware.bluetooth.offload.leaudio-rust", + "libbinder_rs", + "libbluetooth_offload_hci", + "liblog_rust", + "liblogger", + ], +} + +rust_library { + name: "libbluetooth_offload_leaudio_hci", + defaults: ["bluetooth_offload_leaudio_hci_defaults"], + vendor_available: true, + visibility: [ + "//hardware/interfaces/bluetooth:__subpackages__", + "//packages/modules/Bluetooth/offload:__subpackages__", + ], +} + +rust_test { + name: "libbluetooth_offload_leaudio_hci_test", + defaults: ["bluetooth_offload_leaudio_hci_defaults"], +} diff --git a/offload/leaudio/hci/arbiter.rs b/offload/leaudio/hci/arbiter.rs new file mode 100644 index 0000000000..072295f568 --- /dev/null +++ b/offload/leaudio/hci/arbiter.rs @@ -0,0 +1,154 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bluetooth_offload_hci::{IsoData, Module}; +use std::collections::{HashMap, VecDeque}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard}; +use std::thread::{self, JoinHandle}; + +pub struct Arbiter { + state_cvar: Arc<(Mutex<State>, Condvar)>, + thread: Option<JoinHandle<()>>, + max_buf_len: usize, +} + +#[derive(Default)] +struct State { + /// Halt indication of the sender thread + halt: bool, + + /// Software transmission queues for each `Origin`. + /// A queue is pair of connection handle, and packet raw ISO data. + queues: [VecDeque<(u16, Vec<u8>)>; 2], + + /// Count of packets sent to the controller and not yet acknowledged, + /// by connection handle stored on `u16`. + in_transit: HashMap<u16, usize>, +} + +enum Origin { + Audio, + Incoming, +} + +impl Arbiter { + pub fn new(sink: Arc<dyn Module>, max_buf_len: usize, max_buf_count: usize) -> Self { + let state_cvar = Arc::new((Mutex::<State>::new(Default::default()), Condvar::new())); + let thread = { + let state_cvar = state_cvar.clone(); + thread::spawn(move || Self::thread_loop(state_cvar.clone(), sink, max_buf_count)) + }; + + Self { state_cvar, thread: Some(thread), max_buf_len } + } + + pub fn add_connection(&self, handle: u16) { + let (state, _) = &*self.state_cvar; + if state.lock().unwrap().in_transit.insert(handle, 0).is_some() { + panic!("Connection with handle 0x{:03x} already exists", handle); + } + } + + pub fn remove_connection(&self, handle: u16) { + let (state, cvar) = &*self.state_cvar; + let mut state = state.lock().unwrap(); + for q in state.queues.iter_mut() { + while let Some(idx) = q.iter().position(|&(h, _)| h == handle) { + q.remove(idx); + } + } + if state.in_transit.remove(&handle).is_some() { + cvar.notify_one(); + } + } + + pub fn push_incoming(&self, iso_data: &IsoData) { + self.push(Origin::Incoming, iso_data); + } + + pub fn push_audio(&self, iso_data: &IsoData) { + self.push(Origin::Audio, iso_data); + } + + pub fn set_completed(&self, handle: u16, num: usize) { + let (state, cvar) = &*self.state_cvar; + if let Some(buf_usage) = state.lock().unwrap().in_transit.get_mut(&handle) { + *buf_usage -= num; + cvar.notify_one(); + } + } + + fn push(&self, origin: Origin, iso_data: &IsoData) { + let handle = iso_data.connection_handle; + let data = iso_data.to_bytes(); + assert!(data.len() <= self.max_buf_len + 4); + + let (state, cvar) = &*self.state_cvar; + let mut state = state.lock().unwrap(); + if state.in_transit.contains_key(&handle) { + state.queues[origin as usize].push_back((handle, data)); + cvar.notify_one(); + } + } + + fn thread_loop( + state_cvar: Arc<(Mutex<State>, Condvar)>, + sink: Arc<dyn Module>, + max_buf_count: usize, + ) { + let (state, cvar) = &*state_cvar; + 'main: loop { + let packet = { + let mut state = state.lock().unwrap(); + let mut packet = None; + while !state.halt && { + packet = Self::pull(&mut state, max_buf_count); + packet.is_none() + } { + state = cvar.wait(state).unwrap(); + } + if state.halt { + break 'main; + } + packet.unwrap() + }; + sink.out_iso(&packet); + } + } + + fn pull(state: &mut MutexGuard<'_, State>, max_buf_count: usize) -> Option<Vec<u8>> { + for idx in 0..state.queues.len() { + if state.queues[idx].is_empty() || max_buf_count <= state.in_transit.values().sum() { + continue; + } + let (handle, vec) = state.queues[idx].pop_front().unwrap(); + *state.in_transit.get_mut(&handle).unwrap() += 1; + return Some(vec); + } + None + } +} + +impl Drop for Arbiter { + fn drop(&mut self) { + let (state, cvar) = &*self.state_cvar; + { + let mut state = state.lock().unwrap(); + state.halt = true; + cvar.notify_one(); + } + let thread = self.thread.take().unwrap(); + thread.join().expect("End of thread loop"); + } +} diff --git a/offload/leaudio/hci/lib.rs b/offload/leaudio/hci/lib.rs new file mode 100644 index 0000000000..e06cd79356 --- /dev/null +++ b/offload/leaudio/hci/lib.rs @@ -0,0 +1,42 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! LE Audio HCI-Proxy module +//! +//! The module looks at HCI Commands / Events to control the mux of ISO packet +//! flows coming from the stack and the exposed "LE Audio HCI Proxy" AIDL service: +//! +//! HCI | ^ | HCI AIDL +//! Command | | | ISO Packets Interface +//! ___|_|________|__ ___________ ____________ +//! | : : proxy | | | arbiter | | service | +//! | : : | | | | | | +//! | : : `-|-------|---- ----|--------| | +//! | : : | | \ / | | | +//! | : : | Ctrl | : | | | +//! | : : ---------|------>| : | Ctrl | | +//! | : : ---------|------ | : | ------>| | +//! |___:_:________ __| |_____:_____| |____________| +//! | | | +//! | | HCI | HCI +//! v | Event V ISO Packets + +mod arbiter; +mod proxy; +mod service; + +#[cfg(test)] +mod tests; + +pub use proxy::LeAudioModuleBuilder; diff --git a/offload/leaudio/hci/proxy.rs b/offload/leaudio/hci/proxy.rs new file mode 100644 index 0000000000..413a7f84b5 --- /dev/null +++ b/offload/leaudio/hci/proxy.rs @@ -0,0 +1,381 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bluetooth_offload_hci as hci; + +use crate::arbiter::Arbiter; +use crate::service::{Service, StreamConfiguration}; +use hci::{Command, Event, EventToBytes, IsoData, ReturnParameters, Status}; +use hci::{Module, ModuleBuilder}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +const DATA_PATH_ID_SOFTWARE: u8 = 0x19; // TODO + +/// LE Audio HCI-Proxy module builder +pub struct LeAudioModuleBuilder {} + +pub(crate) struct LeAudioModule { + next_module: Arc<dyn Module>, + state: Mutex<State>, + service: Service, +} + +#[derive(Default)] +struct State { + big: HashMap<u8, BigParameters>, + cig: HashMap<u8, CigParameters>, + stream: HashMap<u16, Stream>, + arbiter: Option<Arc<Arbiter>>, +} + +struct BigParameters { + bis_handles: Vec<u16>, + sdu_interval: u32, +} + +struct CigParameters { + cis_handles: Vec<u16>, + sdu_interval_c_to_p: u32, + sdu_interval_p_to_c: u32, +} + +#[derive(Debug, Clone)] +struct Stream { + state: StreamState, + iso_type: IsoType, + iso_interval_us: u32, +} + +#[derive(Debug, PartialEq, Clone)] +enum StreamState { + Disabled, + Enabling, + Enabled, +} + +#[derive(Debug, Clone)] +enum IsoType { + Cis { c_to_p: IsoInDirection, _p_to_c: IsoInDirection }, + Bis { c_to_p: IsoInDirection }, +} + +#[derive(Debug, Clone)] +struct IsoInDirection { + sdu_interval_us: u32, + max_sdu_size: u16, + burst_number: u8, + flush_timeout: u8, +} + +impl Stream { + fn new_cis(cig: &CigParameters, e: &hci::LeCisEstablished) -> Self { + let iso_interval_us = (e.iso_interval as u32) * 1250; + + assert_eq!(iso_interval_us % cig.sdu_interval_c_to_p, 0, "Framing mode not supported"); + assert_eq!(iso_interval_us % cig.sdu_interval_p_to_c, 0, "Framing mode not supported"); + + assert_eq!( + iso_interval_us / cig.sdu_interval_c_to_p, + e.bn_c_to_p.into(), + "SDU fragmentation not supported" + ); + assert_eq!( + iso_interval_us / cig.sdu_interval_p_to_c, + e.bn_p_to_c.into(), + "SDU fragmentation not supported" + ); + + Self { + state: StreamState::Disabled, + iso_interval_us, + iso_type: IsoType::Cis { + c_to_p: IsoInDirection { + sdu_interval_us: cig.sdu_interval_c_to_p, + max_sdu_size: e.max_pdu_c_to_p, + burst_number: e.bn_c_to_p, + flush_timeout: e.ft_c_to_p, + }, + _p_to_c: IsoInDirection { + sdu_interval_us: cig.sdu_interval_p_to_c, + max_sdu_size: e.max_pdu_p_to_c, + burst_number: e.bn_p_to_c, + flush_timeout: e.ft_p_to_c, + }, + }, + } + } + + fn new_bis(big: &BigParameters, e: &hci::LeCreateBigComplete) -> Self { + let iso_interval_us = (e.iso_interval as u32) * 1250; + + assert_eq!(iso_interval_us % big.sdu_interval, 0, "Framing mode not supported"); + assert_eq!( + iso_interval_us / big.sdu_interval, + e.bn.into(), + "SDU fragmentation not supported" + ); + + Self { + state: StreamState::Disabled, + iso_interval_us, + iso_type: IsoType::Bis { + c_to_p: IsoInDirection { + sdu_interval_us: big.sdu_interval, + max_sdu_size: e.max_pdu, + burst_number: e.bn, + flush_timeout: e.irc, + }, + }, + } + } +} + +impl ModuleBuilder for LeAudioModuleBuilder { + /// Build the HCI-Proxy module from the next module in the chain + fn build(&self, next_module: Arc<dyn Module>) -> Arc<dyn Module> { + Arc::new(LeAudioModule::new(next_module)) + } +} + +impl LeAudioModule { + pub(crate) fn new(next_module: Arc<dyn Module>) -> Self { + Self { next_module, state: Mutex::new(Default::default()), service: Service::new() } + } + + #[cfg(test)] + pub(crate) fn arbiter(&self) -> Option<Arc<Arbiter>> { + let state = self.state.lock().unwrap(); + state.arbiter.clone() + } +} + +impl Module for LeAudioModule { + fn next(&self) -> &dyn Module { + &*self.next_module + } + + fn out_cmd(&self, data: &[u8]) { + match Command::from_bytes(data) { + Ok(Command::LeSetCigParameters(ref c)) => { + let mut state = self.state.lock().unwrap(); + state.cig.insert( + c.cig_id, + CigParameters { + cis_handles: vec![], + sdu_interval_c_to_p: c.sdu_interval_c_to_p, + sdu_interval_p_to_c: c.sdu_interval_p_to_c, + }, + ); + } + + Ok(Command::LeCreateBig(ref c)) => { + let mut state = self.state.lock().unwrap(); + state.big.insert( + c.big_handle, + BigParameters { bis_handles: vec![], sdu_interval: c.sdu_interval }, + ); + } + + Ok(Command::LeSetupIsoDataPath(ref c)) if c.data_path_id == DATA_PATH_ID_SOFTWARE => { + assert_eq!(c.data_path_direction, hci::LeDataPathDirection::Input); + let mut state = self.state.lock().unwrap(); + let stream = state.stream.get_mut(&c.connection_handle).unwrap(); + stream.state = StreamState::Enabling; + } + + _ => (), + } + + self.next().out_cmd(data); + } + + fn in_evt(&self, data: &[u8]) { + match Event::from_bytes(data) { + Ok(Event::CommandComplete(ref e)) => match e.return_parameters { + ReturnParameters::Reset(ref ret) if ret.status == Status::Success => { + let mut state = self.state.lock().unwrap(); + *state = Default::default(); + } + + ReturnParameters::LeReadBufferSizeV2(ref ret) if ret.status == Status::Success => { + let mut state = self.state.lock().unwrap(); + state.arbiter = Some(Arc::new(Arbiter::new( + self.next_module.clone(), + ret.iso_data_packet_length.into(), + ret.total_num_iso_data_packets.into(), + ))); + self.service.reset(Arc::downgrade(state.arbiter.as_ref().unwrap())); + } + + ReturnParameters::LeSetCigParameters(ref ret) if ret.status == Status::Success => { + let mut state = self.state.lock().unwrap(); + let cig = state.cig.get_mut(&ret.cig_id).unwrap(); + cig.cis_handles = ret.connection_handles.clone(); + } + + ReturnParameters::LeRemoveCig(ref ret) if ret.status == Status::Success => { + let mut state = self.state.lock().unwrap(); + state.cig.remove(&ret.cig_id); + } + + ReturnParameters::LeSetupIsoDataPath(ref ret) => 'event: { + let mut state = self.state.lock().unwrap(); + let stream = state.stream.get_mut(&ret.connection_handle).unwrap(); + stream.state = + if stream.state == StreamState::Enabling && ret.status == Status::Success { + StreamState::Enabled + } else { + StreamState::Disabled + }; + + if stream.state != StreamState::Enabled { + break 'event; + } + + let c_to_p = match stream.iso_type { + IsoType::Cis { ref c_to_p, .. } => c_to_p, + IsoType::Bis { ref c_to_p } => c_to_p, + }; + + self.service.start_stream( + ret.connection_handle, + StreamConfiguration { + isoIntervalUs: stream.iso_interval_us as i32, + sduIntervalUs: c_to_p.sdu_interval_us as i32, + maxSduSize: c_to_p.max_sdu_size as i32, + burstNumber: c_to_p.burst_number as i32, + flushTimeout: c_to_p.flush_timeout as i32, + }, + ); + } + + ReturnParameters::LeRemoveIsoDataPath(ref ret) if ret.status == Status::Success => { + let mut state = self.state.lock().unwrap(); + let stream = state.stream.get_mut(&ret.connection_handle).unwrap(); + if stream.state == StreamState::Enabled { + self.service.stop_stream(ret.connection_handle); + } + stream.state = StreamState::Disabled; + } + + _ => (), + }, + + Ok(Event::LeCisEstablished(ref e)) if e.status == Status::Success => { + let mut state = self.state.lock().unwrap(); + let mut cig_values = state.cig.values(); + let Some(cig) = + cig_values.find(|&g| g.cis_handles.iter().any(|&h| h == e.connection_handle)) + else { + panic!("CIG not set-up for CIS 0x{:03x}", e.connection_handle); + }; + + let cis = Stream::new_cis(cig, e); + if state.stream.insert(e.connection_handle, cis).is_some() { + log::error!("CIS already established"); + } else { + let arbiter = state.arbiter.as_ref().unwrap(); + arbiter.add_connection(e.connection_handle); + } + } + + Ok(Event::DisconnectionComplete(ref e)) if e.status == Status::Success => { + let mut state = self.state.lock().unwrap(); + if state.stream.remove(&e.connection_handle).is_some() { + let arbiter = state.arbiter.as_ref().unwrap(); + arbiter.remove_connection(e.connection_handle); + } + } + + Ok(Event::LeCreateBigComplete(ref e)) if e.status == Status::Success => { + let mut state_guard = self.state.lock().unwrap(); + let state = &mut *state_guard; + + let big = state.big.get_mut(&e.big_handle).unwrap(); + big.bis_handles = e.bis_handles.clone(); + + let bis = Stream::new_bis(big, e); + for h in &big.bis_handles { + if state.stream.insert(*h, bis.clone()).is_some() { + log::error!("BIS already established"); + } else { + let arbiter = state.arbiter.as_ref().unwrap(); + arbiter.add_connection(*h); + } + } + } + + Ok(Event::LeTerminateBigComplete(ref e)) => { + let mut state = self.state.lock().unwrap(); + let big = state.big.remove(&e.big_handle).unwrap(); + for h in big.bis_handles { + state.stream.remove(&h); + + let arbiter = state.arbiter.as_ref().unwrap(); + arbiter.remove_connection(h); + } + } + + Ok(Event::NumberOfCompletedPackets(ref e)) => 'event: { + let state = self.state.lock().unwrap(); + let Some(arbiter) = state.arbiter.as_ref() else { + break 'event; + }; + + let (stack_event, _) = { + let mut stack_event = hci::NumberOfCompletedPackets { + handles: Vec::with_capacity(e.handles.len()), + }; + let mut audio_event = hci::NumberOfCompletedPackets { + handles: Vec::with_capacity(e.handles.len()), + }; + for item in &e.handles { + let handle = item.connection_handle; + arbiter.set_completed(handle, item.num_completed_packets.into()); + + if match state.stream.get(&handle) { + Some(stream) => stream.state == StreamState::Enabled, + None => false, + } { + audio_event.handles.push(*item); + } else { + stack_event.handles.push(*item); + } + } + (stack_event, audio_event) + }; + + if !stack_event.handles.is_empty() { + self.next().in_evt(&stack_event.to_bytes()); + } + return; + } + + Ok(..) => (), + + Err(code) => { + log::error!("Malformed event with code: {:?}", code); + } + } + + self.next().in_evt(data); + } + + fn out_iso(&self, data: &[u8]) { + let state = self.state.lock().unwrap(); + let arbiter = state.arbiter.as_ref().unwrap(); + arbiter.push_incoming(&IsoData::from_bytes(data).unwrap()); + } +} diff --git a/offload/leaudio/hci/service.rs b/offload/leaudio/hci/service.rs new file mode 100644 index 0000000000..85fac7168c --- /dev/null +++ b/offload/leaudio/hci/service.rs @@ -0,0 +1,126 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use android_hardware_bluetooth_offload_leaudio::{aidl, binder}; + +use crate::arbiter::Arbiter; +use aidl::android::hardware::bluetooth::offload::leaudio::{ + IHciProxy::{BnHciProxy, BpHciProxy, IHciProxy}, + IHciProxyCallbacks::IHciProxyCallbacks, +}; +use binder::{ + BinderFeatures, DeathRecipient, ExceptionCode, Interface, Result as BinderResult, Strong, +}; +use bluetooth_offload_hci::IsoData; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, Weak}; + +pub(crate) use aidl::android::hardware::bluetooth::offload::leaudio::StreamConfiguration::StreamConfiguration; + +pub(crate) struct Service { + state: Arc<Mutex<State>>, +} + +#[derive(Default)] +struct State { + arbiter: Weak<Arbiter>, + callbacks: Option<Strong<dyn IHciProxyCallbacks>>, + streams: HashMap<u16, StreamConfiguration>, +} + +impl Service { + pub(crate) fn new() -> Self { + let state = Arc::new(Mutex::new(State::default())); + HciProxy::register(state.clone()); + Self { state } + } + + pub(crate) fn reset(&self, arbiter: Weak<Arbiter>) { + let mut state = self.state.lock().unwrap(); + *state = State { arbiter, ..Default::default() } + } + + pub(crate) fn start_stream(&self, handle: u16, config: StreamConfiguration) { + let mut state = self.state.lock().unwrap(); + if let Some(callbacks) = &state.callbacks { + let _ = callbacks.startStream(handle.into(), &config); + } else { + log::warn!("Stream started without registered client"); + }; + state.streams.insert(handle, config); + } + + pub(crate) fn stop_stream(&self, handle: u16) { + let mut state = self.state.lock().unwrap(); + state.streams.remove(&handle); + if let Some(callbacks) = &state.callbacks { + let _ = callbacks.stopStream(handle.into()); + }; + } +} + +struct HciProxy { + state: Arc<Mutex<State>>, + _death_recipient: DeathRecipient, +} + +impl Interface for HciProxy {} + +impl HciProxy { + fn register(state: Arc<Mutex<State>>) { + let death_recipient = { + let state = state.clone(); + DeathRecipient::new(move || { + log::info!("Client has died"); + state.lock().unwrap().callbacks = None; + }) + }; + + binder::add_service( + &format!("{}/default", BpHciProxy::get_descriptor()), + BnHciProxy::new_binder( + Self { state, _death_recipient: death_recipient }, + BinderFeatures::default(), + ) + .as_binder(), + ) + .expect("Failed to register service"); + } +} + +impl IHciProxy for HciProxy { + fn registerCallbacks(&self, callbacks: &Strong<dyn IHciProxyCallbacks>) -> BinderResult<()> { + let mut state = self.state.lock().unwrap(); + state.callbacks = Some(callbacks.clone()); + for (handle, config) in &state.streams { + let _ = callbacks.startStream((*handle).into(), config); + } + + Ok(()) + } + + fn sendPacket(&self, handle: i32, seqnum: i32, data: &[u8]) -> BinderResult<()> { + let handle: u16 = handle.try_into().map_err(|_| ExceptionCode::ILLEGAL_ARGUMENT)?; + let seqnum: u16 = seqnum.try_into().map_err(|_| ExceptionCode::ILLEGAL_ARGUMENT)?; + + let state = self.state.lock().unwrap(); + if let Some(arbiter) = state.arbiter.upgrade() { + arbiter.push_audio(&IsoData::new(handle, seqnum, data)); + } else { + log::warn!("Trashing packet received in bad state"); + } + + Ok(()) + } +} diff --git a/offload/leaudio/hci/tests.rs b/offload/leaudio/hci/tests.rs new file mode 100644 index 0000000000..dd55a93c2c --- /dev/null +++ b/offload/leaudio/hci/tests.rs @@ -0,0 +1,914 @@ +// Copyright 2024, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bluetooth_offload_hci as hci; + +use crate::proxy::LeAudioModule; +use hci::{CommandToBytes, EventToBytes, IsoData, Module, ReturnParameters, Status}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::Duration; + +struct ModuleSinkState { + out_cmd: Vec<Vec<u8>>, + in_evt: Vec<Vec<u8>>, + out_iso: mpsc::Receiver<Vec<u8>>, +} + +struct ModuleSink { + state: Mutex<ModuleSinkState>, + out_iso: mpsc::Sender<Vec<u8>>, +} + +impl ModuleSink { + fn new() -> Self { + let (out_iso_tx, out_iso_rx) = mpsc::channel(); + ModuleSink { + state: Mutex::new(ModuleSinkState { + out_cmd: Default::default(), + in_evt: Default::default(), + out_iso: out_iso_rx, + }), + out_iso: out_iso_tx, + } + } +} + +impl Module for ModuleSink { + fn out_cmd(&self, data: &[u8]) { + self.state.lock().unwrap().out_cmd.push(data.to_vec()); + } + fn in_evt(&self, data: &[u8]) { + self.state.lock().unwrap().in_evt.push(data.to_vec()); + } + fn out_iso(&self, data: &[u8]) { + self.out_iso.send(data.to_vec()).expect("Sending ISO packet"); + } + + fn next(&self) -> &dyn Module { + panic!(); + } +} + +#[test] +fn cig() { + let sink: Arc<ModuleSink> = Arc::new(ModuleSink::new()); + let m = LeAudioModule::new(sink.clone()); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::Reset(hci::ResetComplete { + status: Status::Success, + }), + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeReadBufferSizeV2( + hci::LeReadBufferSizeV2Complete { + status: Status::Success, + le_acl_data_packet_length: 0, + total_num_le_acl_data_packets: 0, + iso_data_packet_length: 16, + total_num_iso_data_packets: 2, + }, + ), + } + .to_bytes(), + ); + + m.out_cmd( + &hci::LeSetCigParameters { + cig_id: 0x01, + sdu_interval_c_to_p: 10_000, + sdu_interval_p_to_c: 10_000, + worst_case_sca: 0, + packing: 0, + framing: 0, + max_transport_latency_c_to_p: 0, + max_transport_latency_p_to_c: 0, + cis: vec![], + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeSetCigParameters( + hci::LeSetCigParametersComplete { + status: Status::Success, + cig_id: 0x01, + connection_handles: vec![0x123, 0x456], + }, + ), + } + .to_bytes(), + ); + + m.in_evt( + &hci::LeCisEstablished { + status: Status::Success, + connection_handle: 0x456, + cig_sync_delay: 0, + cis_sync_delay: 0, + transport_latency_c_to_p: 0, + transport_latency_p_to_c: 0, + phy_c_to_p: 0x02, + phy_p_to_c: 0x02, + nse: 0, + bn_c_to_p: 2, + bn_p_to_c: 2, + ft_c_to_p: 1, + ft_p_to_c: 1, + max_pdu_c_to_p: 10, + max_pdu_p_to_c: 0, + iso_interval: 20_000 / 1250, + } + .to_bytes(), + ); + + m.out_cmd( + &hci::LeSetupIsoDataPath { + connection_handle: 0x456, + data_path_direction: hci::LeDataPathDirection::Input, + data_path_id: 0, + codec_id: hci::LeCodecId { + coding_format: hci::CodingFormat::Transparent, + company_id: 0, + vendor_id: 0, + }, + controller_delay: 0, + codec_configuration: vec![], + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete { + status: Status::Success, + connection_handle: 0x456, + }), + } + .to_bytes(), + ); + + m.out_iso(&IsoData::new(0x456, 0, &[0x00, 0x11]).to_bytes()); + m.out_iso(&IsoData::new(0x456, 1, &[]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + } + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 1, + }], + } + .to_bytes(), + ); + + m.out_iso(&IsoData::new(0x456, 2, &[0x22]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + } + + m.in_evt( + &hci::LeCisEstablished { + status: Status::Success, + connection_handle: 0x123, + cig_sync_delay: 0, + cis_sync_delay: 0, + transport_latency_c_to_p: 0, + transport_latency_p_to_c: 0, + phy_c_to_p: 0x02, + phy_p_to_c: 0x02, + nse: 0, + bn_c_to_p: 2, + bn_p_to_c: 2, + ft_c_to_p: 1, + ft_p_to_c: 1, + max_pdu_c_to_p: 10, + max_pdu_p_to_c: 0, + iso_interval: 20_000 / 1250, + } + .to_bytes(), + ); + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 1, + }], + } + .to_bytes(), + ); + + m.out_iso(&IsoData::new(0x123, 0, &[]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + } + + m.in_evt( + &hci::DisconnectionComplete { + status: Status::Success, + connection_handle: 0x456, + reason: 0, + } + .to_bytes(), + ); + + m.out_iso(&IsoData::new(0x456, 3, &[0x33]).to_bytes()); + m.out_iso(&IsoData::new(0x123, 1, &[0x11, 0x22]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + } + + { + let state = sink.state.lock().unwrap(); + assert_eq!(state.out_cmd.len(), 2); + assert_eq!(state.in_evt.len(), 9); + } +} + +#[test] +fn big() { + let sink: Arc<ModuleSink> = Arc::new(ModuleSink::new()); + let m = LeAudioModule::new(sink.clone()); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::Reset(hci::ResetComplete { + status: Status::Success, + }), + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeReadBufferSizeV2( + hci::LeReadBufferSizeV2Complete { + status: Status::Success, + le_acl_data_packet_length: 0, + total_num_le_acl_data_packets: 0, + iso_data_packet_length: 16, + total_num_iso_data_packets: 2, + }, + ), + } + .to_bytes(), + ); + + m.out_cmd( + &hci::LeCreateBig { + big_handle: 0x10, + advertising_handle: 0, + num_bis: 2, + sdu_interval: 10_000, + max_sdu: 120, + max_transport_latency: 0, + rtn: 5, + phy: 0x02, + packing: 0, + framing: 0, + encryption: 0, + broadcast_code: [0u8; 16], + } + .to_bytes(), + ); + + m.in_evt( + &hci::LeCreateBigComplete { + status: Status::Success, + big_handle: 0x10, + big_sync_delay: 0, + big_transport_latency: 0, + phy: 0x02, + nse: 0, + bn: 2, + pto: 0, + irc: 0, + max_pdu: 10, + iso_interval: 20_000 / 1250, + bis_handles: vec![0x123, 0x456], + } + .to_bytes(), + ); + + m.out_cmd( + &hci::LeSetupIsoDataPath { + connection_handle: 0x123, + data_path_direction: hci::LeDataPathDirection::Input, + data_path_id: 0, + codec_id: hci::LeCodecId { + coding_format: hci::CodingFormat::Transparent, + company_id: 0, + vendor_id: 0, + }, + controller_delay: 0, + codec_configuration: vec![], + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete { + status: Status::Success, + connection_handle: 0x123, + }), + } + .to_bytes(), + ); + + m.out_cmd( + &hci::LeSetupIsoDataPath { + connection_handle: 0x456, + data_path_direction: hci::LeDataPathDirection::Input, + data_path_id: 0, + codec_id: hci::LeCodecId { + coding_format: hci::CodingFormat::Transparent, + company_id: 0, + vendor_id: 0, + }, + controller_delay: 0, + codec_configuration: vec![], + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete { + status: Status::Success, + connection_handle: 0x456, + }), + } + .to_bytes(), + ); + + m.out_iso(&IsoData::new(0x456, 0, &[0x00, 0x11]).to_bytes()); + m.out_iso(&IsoData::new(0x456, 1, &[]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + } + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 2, + }], + } + .to_bytes(), + ); + + m.out_iso(&IsoData::new(0x123, 0, &[0x22, 0x33]).to_bytes()); + m.out_iso(&IsoData::new(0x456, 2, &[]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + } + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![ + hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 1, + }, + hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x123, + num_completed_packets: 1, + }, + ], + } + .to_bytes(), + ); + + m.out_iso(&IsoData::new(0x123, 1, &[0x44]).to_bytes()); + m.out_iso(&IsoData::new(0x123, 2, &[0x55, 0x66]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + } + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x123, + num_completed_packets: 2, + }], + } + .to_bytes(), + ); + + m.out_iso(&IsoData::new(0x123, 3, &[]).to_bytes()); + m.out_iso(&IsoData::new(0x123, 4, &[]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + } + + { + let state = sink.state.lock().unwrap(); + assert_eq!(state.out_cmd.len(), 3); + assert_eq!(state.in_evt.len(), 8); + } +} + +#[test] +fn merge() { + let sink: Arc<ModuleSink> = Arc::new(ModuleSink::new()); + let m = LeAudioModule::new(sink.clone()); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::Reset(hci::ResetComplete { + status: Status::Success, + }), + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeReadBufferSizeV2( + hci::LeReadBufferSizeV2Complete { + status: Status::Success, + le_acl_data_packet_length: 0, + total_num_le_acl_data_packets: 0, + iso_data_packet_length: 16, + total_num_iso_data_packets: 2, + }, + ), + } + .to_bytes(), + ); + + m.out_cmd( + &hci::LeSetCigParameters { + cig_id: 0x01, + sdu_interval_c_to_p: 10_000, + sdu_interval_p_to_c: 10_000, + worst_case_sca: 0, + packing: 0, + framing: 0, + max_transport_latency_c_to_p: 0, + max_transport_latency_p_to_c: 0, + cis: vec![], + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeSetCigParameters( + hci::LeSetCigParametersComplete { + status: Status::Success, + cig_id: 0x01, + connection_handles: vec![0x123, 0x456], + }, + ), + } + .to_bytes(), + ); + + // Establish CIS 0x123, using Software Offload path + // Establish CIS 0x456, using HCI (stack) path + + m.in_evt( + &hci::LeCisEstablished { + status: Status::Success, + connection_handle: 0x123, + cig_sync_delay: 0, + cis_sync_delay: 0, + transport_latency_c_to_p: 0, + transport_latency_p_to_c: 0, + phy_c_to_p: 0x02, + phy_p_to_c: 0x02, + nse: 0, + bn_c_to_p: 2, + bn_p_to_c: 2, + ft_c_to_p: 1, + ft_p_to_c: 1, + max_pdu_c_to_p: 10, + max_pdu_p_to_c: 0, + iso_interval: 20_000 / 1250, + } + .to_bytes(), + ); + + m.in_evt( + &hci::LeCisEstablished { + status: Status::Success, + connection_handle: 0x456, + cig_sync_delay: 0, + cis_sync_delay: 0, + transport_latency_c_to_p: 0, + transport_latency_p_to_c: 0, + phy_c_to_p: 0x02, + phy_p_to_c: 0x02, + nse: 0, + bn_c_to_p: 2, + bn_p_to_c: 2, + ft_c_to_p: 1, + ft_p_to_c: 1, + max_pdu_c_to_p: 10, + max_pdu_p_to_c: 0, + iso_interval: 20_000 / 1250, + } + .to_bytes(), + ); + + m.out_cmd( + &hci::LeSetupIsoDataPath { + connection_handle: 0x123, + data_path_direction: hci::LeDataPathDirection::Input, + data_path_id: 0x19, + codec_id: hci::LeCodecId { + coding_format: hci::CodingFormat::Transparent, + company_id: 0, + vendor_id: 0, + }, + controller_delay: 0, + codec_configuration: vec![], + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete { + status: Status::Success, + connection_handle: 0x123, + }), + } + .to_bytes(), + ); + + m.out_cmd( + &hci::LeSetupIsoDataPath { + connection_handle: 0x456, + data_path_direction: hci::LeDataPathDirection::Input, + data_path_id: 0, + codec_id: hci::LeCodecId { + coding_format: hci::CodingFormat::Transparent, + company_id: 0, + vendor_id: 0, + }, + controller_delay: 0, + codec_configuration: vec![], + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete { + status: Status::Success, + connection_handle: 0x456, + }), + } + .to_bytes(), + ); + + { + let mut state = sink.state.lock().unwrap(); + assert_eq!(state.out_cmd.len(), 3); + assert_eq!(state.in_evt.len(), 7); + state.out_cmd.clear(); + state.in_evt.clear(); + } + + // Send 2 Packets on 0x123 + // -> The packets are sent to the controller, and fulfill the FIFO + + m.arbiter().unwrap().push_audio(&IsoData::new(0x123, 1, &[0x44])); + m.arbiter().unwrap().push_audio(&IsoData::new(0x123, 2, &[0x55, 0x66])); + { + let state = sink.state.lock().unwrap(); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet"); + } + + // Send 2 packets on 0x456 + // -> The packets are buffered, the controller FIFO is full + + m.out_iso(&IsoData::new(0x456, 1, &[0x11]).to_bytes()); + m.out_iso(&IsoData::new(0x456, 2, &[0x22, 0x33]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + assert_eq!( + state.out_iso.recv_timeout(Duration::from_millis(100)), + Err(mpsc::RecvTimeoutError::Timeout), + ); + } + + // Acknowledge packet 1 on 0x123: + // -> The acknowledgment is filtered + // -> Packet 1 on 0x456 is tranmitted + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x123, + num_completed_packets: 1, + }], + } + .to_bytes(), + ); + + { + let mut state = sink.state.lock().unwrap(); + assert_eq!(state.in_evt.pop(), None); + assert_eq!( + state.out_iso.recv_timeout(Duration::from_millis(100)), + Ok(IsoData::new(0x456, 1, &[0x11]).to_bytes()) + ); + assert_eq!( + state.out_iso.recv_timeout(Duration::from_millis(100)), + Err(mpsc::RecvTimeoutError::Timeout), + ); + } + + // Link 0x123 disconnect (implicitly ack packet 2 on 0x123) + // -> Packet 2 on 0x456 is tranmitted + + m.in_evt( + &hci::DisconnectionComplete { + status: Status::Success, + connection_handle: 0x123, + reason: 0, + } + .to_bytes(), + ); + + { + let state = sink.state.lock().unwrap(); + assert_eq!( + state.out_iso.recv_timeout(Duration::from_millis(100)), + Ok(IsoData::new(0x456, 2, &[0x22, 0x33]).to_bytes()) + ); + } + + // Send packets and ack packets 1 and 2 on 0x456. + // -> Connection 0x123 is disconnected, ignored + // -> Packets 3, and 4 on 0x456 are sent + // -> Packet 5 on 0x456 is buffered + + m.out_iso(&IsoData::new(0x456, 3, &[0x33]).to_bytes()); + m.arbiter().unwrap().push_audio(&IsoData::new(0x123, 3, &[0x33])); + m.arbiter().unwrap().push_audio(&IsoData::new(0x123, 4, &[0x44, 0x55])); + m.out_iso(&IsoData::new(0x456, 4, &[0x44, 0x55]).to_bytes()); + m.out_iso(&IsoData::new(0x456, 5, &[0x55, 0x66]).to_bytes()); + { + let state = sink.state.lock().unwrap(); + assert_eq!( + state.out_iso.recv_timeout(Duration::from_millis(100)), + Err(mpsc::RecvTimeoutError::Timeout), + ); + } + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 2, + }], + } + .to_bytes(), + ); + + { + let mut state = sink.state.lock().unwrap(); + assert_eq!( + state.in_evt.pop(), + Some( + hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 2, + }], + } + .to_bytes(), + ) + ); + assert_eq!(state.in_evt.len(), 1); + assert_eq!( + state.out_iso.recv_timeout(Duration::from_millis(100)), + Ok(IsoData::new(0x456, 3, &[0x33]).to_bytes()) + ); + assert_eq!( + state.out_iso.recv_timeout(Duration::from_millis(100)), + Ok(IsoData::new(0x456, 4, &[0x44, 0x55]).to_bytes()) + ); + state.in_evt.clear(); + } + + // Re-establish CIS 0x123, using Software Offload path + + m.in_evt( + &hci::LeCisEstablished { + status: Status::Success, + connection_handle: 0x123, + cig_sync_delay: 0, + cis_sync_delay: 0, + transport_latency_c_to_p: 0, + transport_latency_p_to_c: 0, + phy_c_to_p: 0x02, + phy_p_to_c: 0x02, + nse: 0, + bn_c_to_p: 2, + bn_p_to_c: 2, + ft_c_to_p: 1, + ft_p_to_c: 1, + max_pdu_c_to_p: 10, + max_pdu_p_to_c: 0, + iso_interval: 20_000 / 1250, + } + .to_bytes(), + ); + + m.out_cmd( + &hci::LeSetupIsoDataPath { + connection_handle: 0x123, + data_path_direction: hci::LeDataPathDirection::Input, + data_path_id: 0x19, + codec_id: hci::LeCodecId { + coding_format: hci::CodingFormat::Transparent, + company_id: 0, + vendor_id: 0, + }, + controller_delay: 0, + codec_configuration: vec![], + } + .to_bytes(), + ); + + m.in_evt( + &hci::CommandComplete { + num_hci_command_packets: 0, + return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete { + status: Status::Success, + connection_handle: 0x123, + }), + } + .to_bytes(), + ); + + // Acknowledge packets 3 and 4 on 0x456 + // -> Packet 5 on 0x456 is sent + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 2, + }], + } + .to_bytes(), + ); + + { + let mut state = sink.state.lock().unwrap(); + assert_eq!( + state.in_evt.pop(), + Some( + hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 2, + }], + } + .to_bytes(), + ) + ); + assert_eq!( + state.out_iso.recv_timeout(Duration::from_millis(100)), + Ok(IsoData::new(0x456, 5, &[0x55, 0x66]).to_bytes()) + ); + assert_eq!( + state.out_iso.recv_timeout(Duration::from_millis(100)), + Err(mpsc::RecvTimeoutError::Timeout), + ); + state.out_cmd.clear(); + state.in_evt.clear(); + } + + // Acknowledge packet 5 on 0x456 + // -> Controller FIFO is now empty + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 1, + }], + } + .to_bytes(), + ); + + { + let mut state = sink.state.lock().unwrap(); + assert_eq!( + state.in_evt.pop(), + Some( + hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 1, + }], + } + .to_bytes(), + ) + ); + } + + // Send 1 packet on each CIS, and acknowledge them + // -> The CIS 0x123 is removed from "NumberOfCompletedPackets" event + + m.out_iso(&IsoData::new(0x123, 0, &[]).to_bytes()); + m.arbiter().unwrap().push_audio(&IsoData::new(0x456, 6, &[0x66, 0x77])); + + { + let state = sink.state.lock().unwrap(); + for _ in 0..2 { + let pkt = state.out_iso.recv_timeout(Duration::from_millis(100)); + assert!( + pkt == Ok(IsoData::new(0x123, 0, &[]).to_bytes()) + || pkt == Ok(IsoData::new(0x456, 6, &[0x66, 0x77]).to_bytes()) + ); + } + } + + m.in_evt( + &hci::NumberOfCompletedPackets { + handles: vec![ + hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 1, + }, + hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x123, + num_completed_packets: 1, + }, + ], + } + .to_bytes(), + ); + + { + let mut state = sink.state.lock().unwrap(); + assert_eq!( + state.in_evt.pop(), + Some( + hci::NumberOfCompletedPackets { + handles: vec![hci::NumberOfCompletedPacketsHandle { + connection_handle: 0x456, + num_completed_packets: 1, + }], + } + .to_bytes(), + ) + ); + } +} diff --git a/pandora/interfaces/pandora_experimental/mediaplayer.proto b/pandora/interfaces/pandora_experimental/mediaplayer.proto index 98ac7b674d..69ead553d8 100644 --- a/pandora/interfaces/pandora_experimental/mediaplayer.proto +++ b/pandora/interfaces/pandora_experimental/mediaplayer.proto @@ -9,6 +9,7 @@ import "google/protobuf/empty.proto"; service MediaPlayer { rpc Play(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc PlayUpdated(google.protobuf.Empty) returns (google.protobuf.Empty); rpc Stop(google.protobuf.Empty) returns (google.protobuf.Empty); rpc Pause(google.protobuf.Empty) returns (google.protobuf.Empty); rpc Rewind(google.protobuf.Empty) returns (google.protobuf.Empty); @@ -17,6 +18,7 @@ service MediaPlayer { rpc Backward(google.protobuf.Empty) returns (google.protobuf.Empty); rpc SetLargeMetadata(google.protobuf.Empty) returns (google.protobuf.Empty); rpc UpdateQueue(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc ResetQueue(google.protobuf.Empty) returns (google.protobuf.Empty); rpc GetShuffleMode(google.protobuf.Empty) returns (GetShuffleModeResponse); rpc SetShuffleMode(SetShuffleModeRequest) returns (google.protobuf.Empty); rpc StartTestPlayback(google.protobuf.Empty) returns (google.protobuf.Empty); diff --git a/sysprop/a2dp.sysprop b/sysprop/a2dp.sysprop index 80b359c390..ae02fbbb99 100644 --- a/sysprop/a2dp.sysprop +++ b/sysprop/a2dp.sysprop @@ -9,3 +9,10 @@ prop { prop_name: "bluetooth.a2dp.src_sink_coexist.enabled"
}
+prop {
+ api_name: "avdt_accept_open_timeout_ms"
+ type: Integer
+ scope: Internal
+ access: Readonly
+ prop_name: "bluetooth.a2dp.avdt_accept_open_timeout_ms"
+}
\ No newline at end of file diff --git a/system/bta/Android.bp b/system/bta/Android.bp index 63bd5f4e53..57fd56b039 100644 --- a/system/bta/Android.bp +++ b/system/bta/Android.bp @@ -1063,6 +1063,95 @@ cc_test { } cc_test { + name: "bluetooth_ras_test", + test_suites: ["general-tests"], + defaults: [ + "fluoride_bta_defaults", + "mts_defaults", + ], + host_supported: true, + isolated: false, + include_dirs: [ + "packages/modules/Bluetooth/system", + "packages/modules/Bluetooth/system/bta/include", + "packages/modules/Bluetooth/system/bta/test/common", + "packages/modules/Bluetooth/system/stack/include", + ], + srcs: [ + ":TestCommonMockFunctions", + ":TestMockBtaGatt", + ":TestMockMainShim", + ":TestMockMainShimEntry", + ":TestMockStackBtm", + ":TestMockStackBtmInterface", + ":TestMockStackBtmIso", + ":TestMockStackGatt", + ":TestMockStackL2cap", + ":TestStubOsi", + "gatt/database.cc", + "gatt/database_builder.cc", + "ras/ras_utils.cc", + "ras/ras_utils_test.cc", + "test/common/bta_gatt_queue_mock.cc", + "test/common/btif_storage_mock.cc", + "test/common/mock_device_groups.cc", + ], + shared_libs: [ + "libaconfig_storage_read_api_cc", + "libbase", + "libcrypto", + "libcutils", + "libhidlbase", + "liblog", + ], + static_libs: [ + "bluetooth_flags_c_lib_for_test", + "libbluetooth-types", + "libbluetooth_crypto_toolbox", + "libbluetooth_gd", + "libbluetooth_log", + "libbt-audio-hal-interface", + "libbt-common", + "libbt-platform-protos-lite", + "libchrome", + "libcom.android.sysprop.bluetooth.wrapped", + "libevent", + "libflagtest", + "libflatbuffers-cpp", + "libgmock", + "libgtest", + "liblc3", + "libosi", + "server_configurable_flags", + ], + target: { + android: { + shared_libs: [ + "libbinder_ndk", + ], + static_libs: [ + "libPlatformProperties", + ], + }, + host: { + static_libs: [ + "libbinder_ndk", + ], + }, + }, + sanitize: { + cfi: true, + scs: true, + address: true, + all_undefined: true, + integer_overflow: true, + diag: { + undefined: true, + }, + }, +} + +cc_test { name: "bluetooth_test_broadcaster_state_machine", test_suites: ["general-tests"], defaults: [ diff --git a/system/bta/av/bta_av_aact.cc b/system/bta/av/bta_av_aact.cc index 8cb73a4402..769cefc4d9 100644 --- a/system/bta/av/bta_av_aact.cc +++ b/system/bta/av/bta_av_aact.cc @@ -26,6 +26,7 @@ #define LOG_TAG "bluetooth-a2dp" +#include <android_bluetooth_sysprop.h> #include <bluetooth/log.h> #include <com_android_bluetooth_flags.h> @@ -877,17 +878,6 @@ void bta_av_cleanup(tBTA_AV_SCB* p_scb, tBTA_AV_DATA* /* p_data */) { alarm_cancel(p_scb->accept_open_timer); } - /* TODO(eisenbach): RE-IMPLEMENT USING VSC OR HAL EXTENSION - vendor_get_interface()->send_command( - (vendor_opcode_t)BT_VND_OP_A2DP_OFFLOAD_STOP, (void*)&p_scb->l2c_cid); - if (p_scb->offload_start_pending) { - tBTA_AV_STATUS status = BTA_AV_FAIL_STREAM; - tBTA_AV bta_av_data; - bta_av_data.status = status; - (*bta_av_cb.p_cback)(BTA_AV_OFFLOAD_START_RSP_EVT, &bta_av_data); - } - */ - if (p_scb->deregistering) { /* remove stream */ for (int i = 0; i < BTAV_A2DP_CODEC_INDEX_MAX; i++) { @@ -1134,7 +1124,11 @@ void bta_av_setconfig_rsp(tBTA_AV_SCB* p_scb, tBTA_AV_DATA* p_data) { if (!p_scb->accept_open_timer) { p_scb->accept_open_timer = alarm_new("accept_open_timer"); } - alarm_set_on_mloop(p_scb->accept_open_timer, BTA_AV_ACCEPT_OPEN_TIMEOUT_MS, + const uint64_t accept_open_timeout = + android::sysprop::bluetooth::A2dp::avdt_accept_open_timeout_ms().value_or( + BTA_AV_ACCEPT_OPEN_TIMEOUT_MS); + log::debug("accept_open_timeout = {} ms", accept_open_timeout); + alarm_set_on_mloop(p_scb->accept_open_timer, accept_open_timeout, bta_av_accept_open_timer_cback, UINT_TO_PTR(p_scb->hdi)); } } @@ -3180,67 +3174,32 @@ void bta_av_vendor_offload_stop() { * ******************************************************************************/ void bta_av_offload_req(tBTA_AV_SCB* p_scb, tBTA_AV_DATA* /*p_data*/) { - tBTA_AV_STATUS status = BTA_AV_FAIL_RESOURCES; - + tBTA_AV bta_av_data = {}; tBT_A2DP_OFFLOAD offload_start; log::verbose("stream {}, audio channels open {}", p_scb->started ? "STARTED" : "STOPPED", bta_av_cb.audio_open_cnt); - A2dpCodecConfig* codec_config = bta_av_get_a2dp_current_codec(); - log::assert_that(codec_config != nullptr, "assert failed: codec_config != nullptr"); + if (!p_scb->started) { + log::warn("stream not started, start offload failed."); + bta_av_data.status = BTA_AV_FAIL_STREAM; + (*bta_av_cb.p_cback)(BTA_AV_OFFLOAD_START_RSP_EVT, &bta_av_data); + return; + } - /* Check if stream has already been started. */ - /* Support offload if only one audio source stream is open. */ - if (p_scb->started != true) { - status = BTA_AV_FAIL_STREAM; - } else if (bta_av_cb.offload_start_pending_hndl || bta_av_cb.offload_started_hndl) { + if (bta_av_cb.offload_start_pending_hndl || bta_av_cb.offload_started_hndl) { log::warn("offload already started, ignore request"); return; - } else if (::bluetooth::audio::a2dp::provider::supports_codec(codec_config->codecIndex())) { + } + + A2dpCodecConfig* codec_config = bta_av_get_a2dp_current_codec(); + log::assert_that(codec_config != nullptr, "assert failed: codec_config != nullptr"); + + if (::bluetooth::audio::a2dp::provider::supports_codec(codec_config->codecIndex())) { bta_av_vendor_offload_start_v2(p_scb, static_cast<A2dpCodecConfigExt*>(codec_config)); } else { bta_av_offload_codec_builder(p_scb, &offload_start); bta_av_vendor_offload_start(p_scb, &offload_start); - return; - } - if (status != BTA_AV_SUCCESS) { - tBTA_AV bta_av_data; - bta_av_data.status = status; - (*bta_av_cb.p_cback)(BTA_AV_OFFLOAD_START_RSP_EVT, &bta_av_data); - } - /* TODO(eisenbach): RE-IMPLEMENT USING VSC OR HAL EXTENSION - else if (bta_av_cb.audio_open_cnt == 1 && - p_scb->seps[p_scb->sep_idx].tsep == AVDT_TSEP_SRC && - p_scb->chnl == BTA_AV_CHNL_AUDIO) { - bt_vendor_op_a2dp_offload_t a2dp_offload_start; - - if (L2CA_GetConnectionConfig( - p_scb->l2c_cid, &a2dp_offload_start.acl_data_size, - &a2dp_offload_start.remote_cid, &a2dp_offload_start.lm_handle)) { - log::verbose("l2cmtu {} lcid 0x{:02X} rcid 0x{:02X} lm_handle 0x{:02X}", - a2dp_offload_start.acl_data_size, p_scb->l2c_cid, - a2dp_offload_start.remote_cid, a2dp_offload_start.lm_handle); - - a2dp_offload_start.bta_av_handle = p_scb->hndl; - a2dp_offload_start.xmit_quota = BTA_AV_A2DP_OFFLOAD_XMIT_QUOTA; - a2dp_offload_start.stream_mtu = p_scb->stream_mtu; - a2dp_offload_start.local_cid = p_scb->l2c_cid; - a2dp_offload_start.is_flushable = true; - a2dp_offload_start.stream_source = - ((uint32_t)(p_scb->cfg.codec_info[1] | p_scb->cfg.codec_info[2])); - - memcpy(a2dp_offload_start.codec_info, p_scb->cfg.codec_info, - sizeof(a2dp_offload_start.codec_info)); - - if (!vendor_get_interface()->send_command( - (vendor_opcode_t)BT_VND_OP_A2DP_OFFLOAD_START, - &a2dp_offload_start)) { - status = BTA_AV_SUCCESS; - p_scb->offload_start_pending = true; - } - } } - */ } /******************************************************************************* @@ -3402,4 +3361,4 @@ static void bta_av_accept_open_timer_cback(void* data) { tBTA_AV_API_OPEN* p_buf = (tBTA_AV_API_OPEN*)osi_malloc(sizeof(tBTA_AV_API_OPEN)); memcpy(p_buf, &(p_scb->open_api), sizeof(tBTA_AV_API_OPEN)); bta_sys_sendmsg(p_buf); -}
\ No newline at end of file +} diff --git a/system/bta/dm/bta_dm_disc.cc b/system/bta/dm/bta_dm_disc.cc index e9c6ba5e77..c3977454f5 100644 --- a/system/bta/dm/bta_dm_disc.cc +++ b/system/bta/dm/bta_dm_disc.cc @@ -511,7 +511,7 @@ static void bta_dm_gatt_disc_complete(tCONN_ID conn_id, tGATT_STATUS status) { log::verbose("conn_id = {}, status = {}, sdp_pending = {}, le_pending = {}", conn_id, status, sdp_pending, le_pending); - if (com::android::bluetooth::flags::bta_dm_discover_both() && sdp_pending && !le_pending) { + if (sdp_pending && !le_pending) { /* LE Service discovery finished, and services were reported, but SDP is not * finished yet. gatt_close_timer closed the connection, and we received * this callback because of disconnection */ @@ -784,8 +784,7 @@ static void bta_dm_disc_sm_execute(tBTA_DM_DISC_EVT event, std::unique_ptr<tBTA_ "bad message type: {}", msg->index()); auto req = std::get<tBTA_DM_API_DISCOVER>(*msg); - if (com::android::bluetooth::flags::bta_dm_discover_both() && - is_same_device(req.bd_addr, bta_dm_discovery_cb.peer_bdaddr)) { + if (is_same_device(req.bd_addr, bta_dm_discovery_cb.peer_bdaddr)) { bta_dm_discover_services(std::get<tBTA_DM_API_DISCOVER>(*msg)); } else { bta_dm_queue_disc(std::get<tBTA_DM_API_DISCOVER>(*msg)); diff --git a/system/bta/hh/bta_hh_headtracker.cc b/system/bta/hh/bta_hh_headtracker.cc index c8b3e20f05..0282cb59d0 100644 --- a/system/bta/hh/bta_hh_headtracker.cc +++ b/system/bta/hh/bta_hh_headtracker.cc @@ -140,7 +140,10 @@ void bta_hh_headtracker_parse_service(tBTA_HH_DEV_CB* p_dev_cb, const gatt::Serv bool bta_hh_headtracker_supported(tBTA_HH_DEV_CB* p_dev_cb) { if (p_dev_cb->hid_srvc.headtracker_support == BTA_HH_UNKNOWN) { bluetooth::Uuid remote_uuids[BT_MAX_NUM_UUIDS] = {}; - bt_property_t remote_properties = {BT_PROPERTY_UUIDS, sizeof(remote_uuids), &remote_uuids}; + bt_property_t remote_properties = {com::android::bluetooth::flags::separate_service_storage() + ? BT_PROPERTY_UUIDS_LE + : BT_PROPERTY_UUIDS, + sizeof(remote_uuids), &remote_uuids}; const RawAddress& bd_addr = p_dev_cb->link_spec.addrt.bda; p_dev_cb->hid_srvc.headtracker_support = BTA_HH_UNAVAILABLE; diff --git a/system/bta/include/bta_ras_api.h b/system/bta/include/bta_ras_api.h index ee6eccec4f..48113e5edf 100644 --- a/system/bta/include/bta_ras_api.h +++ b/system/bta/include/bta_ras_api.h @@ -25,6 +25,12 @@ namespace bluetooth { namespace ras { +enum class RasDisconnectReason { + GATT_DISCONNECT, + SERVER_NOT_AVAILABLE, + FATAL_ERROR, +}; + struct VendorSpecificCharacteristic { bluetooth::Uuid characteristicUuid_; std::vector<uint8_t> value_; @@ -64,7 +70,8 @@ public: const std::vector<VendorSpecificCharacteristic>& vendor_specific_characteristics, uint16_t conn_interval) = 0; virtual void OnConnIntervalUpdated(const RawAddress& address, uint16_t conn_interval) = 0; - virtual void OnDisconnected(const RawAddress& address) = 0; + virtual void OnDisconnected(const RawAddress& address, + const RasDisconnectReason& ras_disconnect_reason) = 0; virtual void OnWriteVendorSpecificReplyComplete(const RawAddress& address, bool success) = 0; virtual void OnRemoteData(const RawAddress& address, const std::vector<uint8_t>& data) = 0; virtual void OnRemoteDataTimeout(const RawAddress& address) = 0; diff --git a/system/bta/jv/bta_jv_act.cc b/system/bta/jv/bta_jv_act.cc index 7c3cb13d47..ffe1756943 100644 --- a/system/bta/jv/bta_jv_act.cc +++ b/system/bta/jv/bta_jv_act.cc @@ -233,7 +233,7 @@ tBTA_JV_RFC_CB* bta_jv_alloc_rfc_cb(uint16_t port_handle, tBTA_JV_PCB** pp_pcb) p_cb->rfc_hdl[j] = 0; } p_cb->rfc_hdl[0] = port_handle; - log::verbose("port_handle={}, handle=0x{:x}", port_handle, p_cb->handle); + log::verbose("port_handle={}, jv_handle=0x{:x}", port_handle, p_cb->handle); p_pcb = &bta_jv_cb.port_cb[port_handle - 1]; p_pcb->handle = p_cb->handle; @@ -307,7 +307,7 @@ static tBTA_JV_STATUS bta_jv_free_rfc_cb(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pc log::error("p_cb or p_pcb cannot be null"); return tBTA_JV_STATUS::FAILURE; } - log::verbose("max_sess={}, curr_sess={}, p_pcb={}, user={}, state={}, jv handle=0x{:x}", + log::verbose("max_sess={}, curr_sess={}, p_pcb={}, user={}, state={}, jv_handle=0x{:x}", p_cb->max_sess, p_cb->curr_sess, std::format_ptr(p_pcb), p_pcb->rfcomm_slot_id, p_pcb->state, p_pcb->handle); @@ -341,7 +341,7 @@ static tBTA_JV_STATUS bta_jv_free_rfc_cb(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pc break; default: log::warn( - "failed, ignore port state= {}, scn={}, p_pcb= {}, jv handle=0x{:x}, " + "failed, ignore port state= {}, scn={}, p_pcb= {}, jv_handle=0x{:x}, " "port_handle={}, user_data={}", p_pcb->state, p_cb->scn, std::format_ptr(p_pcb), p_pcb->handle, p_pcb->port_handle, p_pcb->rfcomm_slot_id); @@ -359,7 +359,7 @@ static tBTA_JV_STATUS bta_jv_free_rfc_cb(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pc if (port_status != PORT_SUCCESS) { status = tBTA_JV_STATUS::FAILURE; log::warn( - "Remove jv handle=0x{:x}, state={}, port_status={}, port_handle={}, close_pending={}", + "Remove jv_handle=0x{:x}, state={}, port_status={}, port_handle={}, close_pending={}", p_pcb->handle, p_pcb->state, port_status, p_pcb->port_handle, close_pending); } } @@ -470,8 +470,8 @@ static tBTA_JV_STATUS bta_jv_free_set_pm_profile_cb(uint32_t jv_handle) { } } - log::verbose("jv_handle=0x{:x}, idx={}app_id={}, bd_counter={}, appid_counter={}", jv_handle, - i, bta_jv_cb.pm_cb[i].app_id, bd_counter, appid_counter); + log::verbose("jv_handle=0x{:x}, idx={}, app_id={}, bd_counter={}, appid_counter={}", + jv_handle, i, bta_jv_cb.pm_cb[i].app_id, bd_counter, appid_counter); if (bd_counter > 1) { bta_jv_pm_conn_idle(&bta_jv_cb.pm_cb[i]); } @@ -559,7 +559,7 @@ static tBTA_JV_PM_CB* bta_jv_alloc_set_pm_profile_cb(uint32_t jv_handle, tBTA_JV } } } - log::verbose("handle=0x{:x}, app_id={}, idx={}, BTA_JV_PM_MAX_NUM={}, pp_cb={}", jv_handle, + log::verbose("jv_handle=0x{:x}, app_id={}, idx={}, BTA_JV_PM_MAX_NUM={}, pp_cb={}", jv_handle, app_id, i, BTA_JV_PM_MAX_NUM, std::format_ptr(pp_cb)); break; } @@ -573,7 +573,7 @@ static tBTA_JV_PM_CB* bta_jv_alloc_set_pm_profile_cb(uint32_t jv_handle, tBTA_JV bta_jv_cb.pm_cb[i].state = BTA_JV_PM_IDLE_ST; return &bta_jv_cb.pm_cb[i]; } - log::warn("handle=0x{:x}, app_id={}, return NULL", jv_handle, app_id); + log::warn("jv_handle=0x{:x}, app_id={}, return NULL", jv_handle, app_id); return NULL; } @@ -954,7 +954,7 @@ void bta_jv_delete_record(uint32_t handle) { if (handle) { /* this is a record created by btif layer*/ if (!get_legacy_stack_sdp_api()->handle.SDP_DeleteRecord(handle)) { - log::warn("Unable to delete SDP record handle:{}", handle); + log::warn("Unable to delete SDP record handle:{}", handle); } } } @@ -997,7 +997,7 @@ static void bta_jv_l2cap_client_cback(uint16_t gap_handle, uint16_t event, tGAP_ if (GAP_GetLeChannelInfo(gap_handle, &remote_mtu, &local_mps, &remote_mps, &local_credit, &remote_credit, &local_cid, &remote_cid, &acl_handle) != PORT_SUCCESS) { - log::warn("Unable to get GAP channel info handle:{}", gap_handle); + log::warn("Unable to get GAP channel info gap_handle:{}", gap_handle); } evt_data.l2c_open.tx_mtu = remote_mtu; evt_data.l2c_open.local_coc_mps = local_mps; @@ -1426,10 +1426,10 @@ static void bta_jv_port_mgmt_cl_cback(const tPORT_RESULT code, uint16_t port_han return; } - log::verbose("code={}, port_handle={}, handle={}", code, port_handle, p_cb->handle); + log::verbose("code={}, port_handle={}, rfc_handle={}", code, port_handle, p_cb->handle); if (PORT_CheckConnection(port_handle, &rem_bda, &lcid) != PORT_SUCCESS) { - log::warn("Unable to check RFCOMM connection peer:{} handle:{}", rem_bda, port_handle); + log::warn("Unable to check RFCOMM connection peer:{} port_handle:{}", rem_bda, port_handle); } if (code == PORT_SUCCESS) { @@ -1448,7 +1448,7 @@ static void bta_jv_port_mgmt_cl_cback(const tPORT_RESULT code, uint16_t port_han &evt_data.rfc_open.dlci, &evt_data.rfc_open.max_frame_size, &evt_data.rfc_open.acl_handle, &evt_data.rfc_open.mux_initiator) != PORT_SUCCESS) { - log::warn("Unable to get RFCOMM channel info peer:{} handle:{}", rem_bda, port_handle); + log::warn("Unable to get RFCOMM channel info peer:{} port_handle:{}", rem_bda, port_handle); } } p_pcb->state = BTA_JV_ST_CL_OPEN; @@ -1490,7 +1490,7 @@ static void bta_jv_port_event_cl_cback(uint32_t code, uint16_t port_handle) { return; } - log::verbose("code=0x{:x}, port_handle={}, handle={}", code, port_handle, p_cb->handle); + log::verbose("code=0x{:x}, port_handle={}, rfc_handle={}", code, port_handle, p_cb->handle); if (code & PORT_EV_RXCHAR) { evt_data.data_ind.handle = p_cb->handle; p_cb->p_cback(BTA_JV_RFCOMM_DATA_IND_EVT, &evt_data, p_pcb->rfcomm_slot_id); @@ -1550,23 +1550,23 @@ void bta_jv_rfcomm_connect(tBTA_SEC sec_mask, uint8_t remote_scn, const RawAddre bta_jv.rfc_cl_init.use_co = true; if (PORT_SetAppUid(handle, app_uid) != PORT_SUCCESS) { - log::warn("Unable to set app_uid for port handle:{}", handle); + log::warn("Unable to set app_uid for port_handle:{}", handle); } if (PORT_SetEventMaskAndCallback(handle, event_mask, bta_jv_port_event_cl_cback) != PORT_SUCCESS) { - log::warn("Unable to set RFCOMM client event mask and callback handle:{}", handle); + log::warn("Unable to set RFCOMM client event mask and callback port_handle:{}", handle); } if (PORT_SetDataCOCallback(handle, bta_jv_port_data_co_cback) != PORT_SUCCESS) { - log::warn("Unable to set RFCOMM client data callback handle:{}", handle); + log::warn("Unable to set RFCOMM client data callback port_handle:{}", handle); } if (PORT_GetSettings(handle, &port_settings) != PORT_SUCCESS) { - log::warn("Unable to get RFCOMM client state handle:{}", handle); + log::warn("Unable to get RFCOMM client state port_handle:{}", handle); } port_settings.fc_type = (PORT_FC_CTS_ON_INPUT | PORT_FC_CTS_ON_OUTPUT); if (PORT_SetSettings(handle, &port_settings) != PORT_SUCCESS) { - log::warn("Unable to set RFCOMM client state handle:{}", handle); + log::warn("Unable to set RFCOMM client state port_handle:{}", handle); } bta_jv.rfc_cl_init.handle = p_cb->handle; @@ -1580,7 +1580,7 @@ void bta_jv_rfcomm_connect(tBTA_SEC sec_mask, uint8_t remote_scn, const RawAddre if (bta_jv.rfc_cl_init.status == tBTA_JV_STATUS::FAILURE) { if (handle) { if (RFCOMM_RemoveConnection(handle) != PORT_SUCCESS) { - log::warn("Unable to remove RFCOMM connection handle:{}", handle); + log::warn("Unable to remove RFCOMM connection port_handle:{}", handle); } } } @@ -1597,7 +1597,7 @@ static int find_rfc_pcb(uint32_t rfcomm_slot_id, tBTA_JV_RFC_CB** cb, tBTA_JV_PC *pcb = &bta_jv_cb.port_cb[i]; *cb = &bta_jv_cb.rfc_cb[rfc_handle - 1]; log::verbose( - "FOUND rfc_cb_handle=0x{:x}, port.jv_handle=0x{:x}, state={}, rfc_cb->handle=0x{:x}", + "FOUND rfc_handle=0x{:x}, port.jv_handle=0x{:x}, state={}, rfc_cb->handle=0x{:x}", rfc_handle, (*pcb)->handle, (*pcb)->state, (*cb)->handle); return 1; } @@ -1609,11 +1609,11 @@ static int find_rfc_pcb(uint32_t rfcomm_slot_id, tBTA_JV_RFC_CB** cb, tBTA_JV_PC /* Close an RFCOMM connection */ void bta_jv_rfcomm_close(uint32_t handle, uint32_t rfcomm_slot_id) { if (!handle) { - log::error("rfc handle is null"); + log::error("rfc_handle is null"); return; } - log::verbose("rfc handle={}", handle); + log::verbose("rfc_handle={}", handle); tBTA_JV_RFC_CB* p_cb = NULL; tBTA_JV_PCB* p_pcb = NULL; @@ -1647,7 +1647,7 @@ static void bta_jv_port_mgmt_sr_cback(const tPORT_RESULT code, uint16_t port_han return; } uint32_t rfcomm_slot_id = p_pcb->rfcomm_slot_id; - log::verbose("code={}, port_handle=0x{:x}, handle=0x{:x}, p_pcb{}, user={}", code, port_handle, + log::verbose("code={}, port_handle=0x{:x}, jv_handle=0x{:x}, p_pcb{}, user={}", code, port_handle, p_cb->handle, std::format_ptr(p_pcb), p_pcb->rfcomm_slot_id); int status = PORT_CheckConnection(port_handle, &rem_bda, &lcid); @@ -1668,7 +1668,7 @@ static void bta_jv_port_mgmt_sr_cback(const tPORT_RESULT code, uint16_t port_han &evt_data.rfc_srv_open.dlci, &evt_data.rfc_srv_open.max_frame_size, &evt_data.rfc_srv_open.acl_handle, &evt_data.rfc_srv_open.mux_initiator) != PORT_SUCCESS) { - log::warn("Unable to get RFCOMM channel info peer:{} handle:{}", rem_bda, port_handle); + log::warn("Unable to get RFCOMM channel info peer:{} port_handle:{}", rem_bda, port_handle); } } tBTA_JV_PCB* p_pcb_new_listen = bta_jv_add_rfc_port(p_cb, p_pcb); @@ -1729,7 +1729,7 @@ static void bta_jv_port_event_sr_cback(uint32_t code, uint16_t port_handle) { return; } - log::verbose("code=0x{:x}, port_handle={}, handle={}", code, port_handle, p_cb->handle); + log::verbose("code=0x{:x}, port_handle={}, rfc_handle={}", code, port_handle, p_cb->handle); uint32_t user_data = p_pcb->rfcomm_slot_id; if (code & PORT_EV_RXCHAR) { @@ -1779,7 +1779,8 @@ static tBTA_JV_PCB* bta_jv_add_rfc_port(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pcb } else { log::error( - "open pcb not matching listen one, count={}, listen pcb handle={}, open pcb={}", + "open pcb not matching listen one, count={}, listen port_handle={}, open " + "pcb={}", listen, p_pcb->port_handle, p_pcb_open->handle); return NULL; } @@ -1809,24 +1810,25 @@ static tBTA_JV_PCB* bta_jv_add_rfc_port(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pcb p_pcb->rfcomm_slot_id = p_pcb_open->rfcomm_slot_id; if (PORT_ClearKeepHandleFlag(p_pcb->port_handle) != PORT_SUCCESS) { - log::warn("Unable to clear RFCOMM server keep handle flag handle:{}", p_pcb->port_handle); + log::warn("Unable to clear RFCOMM server keep handle flag port_handle:{}", + p_pcb->port_handle); } if (PORT_SetEventMaskAndCallback(p_pcb->port_handle, event_mask, bta_jv_port_event_sr_cback) != PORT_SUCCESS) { - log::warn("Unable to set RFCOMM server event mask and callback handle:{}", + log::warn("Unable to set RFCOMM server event mask and callback port_handle:{}", p_pcb->port_handle); } if (PORT_SetDataCOCallback(p_pcb->port_handle, bta_jv_port_data_co_cback) != PORT_SUCCESS) { - log::warn("Unable to set RFCOMM server data callback handle:{}", p_pcb->port_handle); + log::warn("Unable to set RFCOMM server data callback port_handle:{}", p_pcb->port_handle); } if (PORT_GetSettings(p_pcb->port_handle, &port_settings) != PORT_SUCCESS) { - log::warn("Unable to get RFCOMM server state handle:{}", p_pcb->port_handle); + log::warn("Unable to get RFCOMM server state port_handle:{}", p_pcb->port_handle); } port_settings.fc_type = (PORT_FC_CTS_ON_INPUT | PORT_FC_CTS_ON_OUTPUT); if (PORT_SetSettings(p_pcb->port_handle, &port_settings) != PORT_SUCCESS) { - log::warn("Unable to set RFCOMM server state handle:{}", p_pcb->port_handle); + log::warn("Unable to set RFCOMM server state port_handle:{}", p_pcb->port_handle); } p_pcb->handle = BTA_JV_RFC_H_S_TO_HDL(p_cb->handle, si); log::verbose("p_pcb->handle=0x{:x}, curr_sess={}", p_pcb->handle, p_cb->curr_sess); @@ -1881,23 +1883,23 @@ void bta_jv_rfcomm_start_server(tBTA_SEC sec_mask, uint8_t local_scn, uint8_t ma evt_data.use_co = true; if (PORT_SetAppUid(handle, app_uid) != PORT_SUCCESS) { - log::warn("Unable to set app_uid for port handle:{}", handle); + log::warn("Unable to set app_uid for port_handle:{}", handle); } if (PORT_ClearKeepHandleFlag(handle) != PORT_SUCCESS) { - log::warn("Unable to clear RFCOMM server keep handle flag handle:{}", handle); + log::warn("Unable to clear RFCOMM server keep handle flag port_handle:{}", handle); } if (PORT_SetEventMaskAndCallback(handle, event_mask, bta_jv_port_event_sr_cback) != PORT_SUCCESS) { - log::warn("Unable to set RFCOMM server event mask and callback handle:{}", handle); + log::warn("Unable to set RFCOMM server event mask and callback port_handle:{}", handle); } if (PORT_GetSettings(handle, &port_settings) != PORT_SUCCESS) { - log::warn("Unable to get RFCOMM server state handle:{}", handle); + log::warn("Unable to get RFCOMM server state port_handle:{}", handle); } port_settings.fc_type = (PORT_FC_CTS_ON_INPUT | PORT_FC_CTS_ON_OUTPUT); if (PORT_SetSettings(handle, &port_settings) != PORT_SUCCESS) { - log::warn("Unable to set RFCOMM port state handle:{}", handle); + log::warn("Unable to set RFCOMM port state port_handle:{}", handle); } } while (0); @@ -1906,12 +1908,12 @@ void bta_jv_rfcomm_start_server(tBTA_SEC sec_mask, uint8_t local_scn, uint8_t ma p_cback(BTA_JV_RFCOMM_START_EVT, &bta_jv, rfcomm_slot_id); if (bta_jv.rfc_start.status == tBTA_JV_STATUS::SUCCESS) { if (PORT_SetDataCOCallback(handle, bta_jv_port_data_co_cback) != PORT_SUCCESS) { - log::error("Unable to set RFCOMM server data callback handle:{}", handle); + log::error("Unable to set RFCOMM server data callback port_handle:{}", handle); } } else { if (handle) { if (RFCOMM_RemoveConnection(handle) != PORT_SUCCESS) { - log::warn("Unable to remote RFCOMM server connection handle:{}", handle); + log::warn("Unable to remote RFCOMM server connection port_handle:{}", handle); } } } @@ -1920,7 +1922,7 @@ void bta_jv_rfcomm_start_server(tBTA_SEC sec_mask, uint8_t local_scn, uint8_t ma /* stops an RFCOMM server */ void bta_jv_rfcomm_stop_server(uint32_t handle, uint32_t rfcomm_slot_id) { if (!handle) { - log::error("jv handle is null"); + log::error("jv_handle is null"); return; } @@ -1971,23 +1973,24 @@ void bta_jv_rfcomm_write(uint32_t handle, uint32_t req_id, tBTA_JV_RFC_CB* p_cb, /* Set or free power mode profile for a JV application */ void bta_jv_set_pm_profile(uint32_t handle, tBTA_JV_PM_ID app_id, tBTA_JV_CONN_STATE init_st) { - log::verbose("handle=0x{:x}, app_id={}, init_st={}", handle, app_id, + log::verbose("jv_handle=0x{:x}, app_id={}, init_st={}", handle, app_id, bta_jv_conn_state_text(init_st)); /* clear PM control block */ if (app_id == BTA_JV_PM_ID_CLEAR) { tBTA_JV_STATUS status = bta_jv_free_set_pm_profile_cb(handle); if (status != tBTA_JV_STATUS::SUCCESS) { - log::warn("Unable to free a power mode profile handle:0x:{:x} app_id:{} state:{} status:{}", - handle, app_id, init_st, bta_jv_status_text(status)); + log::warn( + "Unable to free a power mode profile jv_handle:0x:{:x} app_id:{} state:{} status:{}", + handle, app_id, init_st, bta_jv_status_text(status)); } } else { /* set PM control block */ tBTA_JV_PM_CB* p_cb = bta_jv_alloc_set_pm_profile_cb(handle, app_id); if (p_cb) { bta_jv_pm_state_change(p_cb, init_st); } else { - log::warn("Unable to allocate a power mode profile handle:0x:{:x} app_id:{} state:{}", handle, - app_id, init_st); + log::warn("Unable to allocate a power mode profile jv_handle:0x:{:x} app_id:{} state:{}", + handle, app_id, init_st); } } } @@ -2038,7 +2041,7 @@ static void bta_jv_pm_conn_idle(tBTA_JV_PM_CB* p_cb) { * ******************************************************************************/ static void bta_jv_pm_state_change(tBTA_JV_PM_CB* p_cb, const tBTA_JV_CONN_STATE state) { - log::verbose("p_cb={}, handle=0x{:x}, busy/idle_state={}, app_id={}, conn_state={}", + log::verbose("p_cb={}, jv_handle=0x{:x}, busy/idle_state={}, app_id={}, conn_state={}", std::format_ptr(p_cb), p_cb->handle, p_cb->state, p_cb->app_id, bta_jv_conn_state_text(state)); diff --git a/system/bta/le_audio/client.cc b/system/bta/le_audio/client.cc index c5901065d5..6dbb4ace85 100644 --- a/system/bta/le_audio/client.cc +++ b/system/bta/le_audio/client.cc @@ -6160,6 +6160,20 @@ public: } break; } + case GroupStreamStatus::RELEASING_AUTONOMOUS: + /* Remote device releases all the ASEs autonomusly. This should not happen and not sure what + * is the remote device intention. If remote wants stop the stream then MCS shall be used to + * stop the stream in a proper way. For a phone call, GTBS shall be used. For now we assume + * this device has does not want to be used for streaming and mark it as Inactive. + */ + log::warn("Group {} is doing autonomous release, make it inactive", group_id); + if (group) { + group->PrintDebugState(); + groupSetAndNotifyInactive(); + } + audio_sender_state_ = AudioState::IDLE; + audio_receiver_state_ = AudioState::IDLE; + break; case GroupStreamStatus::RELEASING: case GroupStreamStatus::SUSPENDING: if (active_group_id_ != bluetooth::groups::kGroupUnknown && @@ -6172,9 +6186,12 @@ public: * it means that it is some internal state machine error. This is very unlikely and * for now just Inactivate the group. */ - log::error("Internal state machine error"); + log::error("Internal state machine error for group {}", group_id); group->PrintDebugState(); groupSetAndNotifyInactive(); + audio_sender_state_ = AudioState::IDLE; + audio_receiver_state_ = AudioState::IDLE; + return; } if (is_active_group_operation) { diff --git a/system/bta/le_audio/le_audio_client_test.cc b/system/bta/le_audio/le_audio_client_test.cc index 3e4c50aff3..693494f73c 100644 --- a/system/bta/le_audio/le_audio_client_test.cc +++ b/system/bta/le_audio/le_audio_client_test.cc @@ -6817,11 +6817,18 @@ TEST_F(UnicastTest, SpeakerStreamingAutonomousRelease) { Mock::VerifyAndClearExpectations(&mock_audio_hal_client_callbacks_); Mock::VerifyAndClearExpectations(mock_le_audio_source_hal_client_); + Mock::VerifyAndClearExpectations(mock_le_audio_sink_hal_client_); SyncOnMainLoop(); // Verify Data transfer on one audio source cis TestAudioDataTransfer(group_id, 1 /* cis_count_out */, 0 /* cis_count_in */, 1920); + EXPECT_CALL(mock_audio_hal_client_callbacks_, OnGroupStatus(group_id, GroupStatus::INACTIVE)) + .Times(1); + EXPECT_CALL(mock_audio_hal_client_callbacks_, + OnGroupStreamStatus(group_id, GroupStreamStatus::IDLE)) + .Times(1); + // Inject the IDLE state as if an autonomous release happened ASSERT_NE(0lu, streaming_groups.count(group_id)); auto group = streaming_groups.at(group_id); @@ -6835,9 +6842,14 @@ TEST_F(UnicastTest, SpeakerStreamingAutonomousRelease) { InjectCisDisconnected(group_id, ase.cis_conn_hdl); } } - // Verify no Data transfer after the autonomous release TestAudioDataTransfer(group_id, 0 /* cis_count_out */, 0 /* cis_count_in */, 1920); + + // Inject Releasing + state_machine_callbacks_->StatusReportCb(group->group_id_, + GroupStreamStatus::RELEASING_AUTONOMOUS); + SyncOnMainLoop(); + Mock::VerifyAndClearExpectations(&mock_audio_hal_client_callbacks_); } TEST_F(UnicastTest, TwoEarbudsStreaming) { @@ -12121,6 +12133,16 @@ TEST_F(UnicastTest, GroupStreamStatus) { EXPECT_CALL(mock_audio_hal_client_callbacks_, OnGroupStreamStatus(group_id, GroupStreamStatus::IDLE)) .Times(1); + state_machine_callbacks_->StatusReportCb(group_id, GroupStreamStatus::RELEASING_AUTONOMOUS); + + EXPECT_CALL(mock_audio_hal_client_callbacks_, + OnGroupStreamStatus(group_id, GroupStreamStatus::STREAMING)) + .Times(1); + state_machine_callbacks_->StatusReportCb(group_id, GroupStreamStatus::STREAMING); + + EXPECT_CALL(mock_audio_hal_client_callbacks_, + OnGroupStreamStatus(group_id, GroupStreamStatus::IDLE)) + .Times(1); state_machine_callbacks_->StatusReportCb(group_id, GroupStreamStatus::SUSPENDING); EXPECT_CALL(mock_audio_hal_client_callbacks_, diff --git a/system/bta/le_audio/state_machine.cc b/system/bta/le_audio/state_machine.cc index dd1c535be1..cc0a79690c 100644 --- a/system/bta/le_audio/state_machine.cc +++ b/system/bta/le_audio/state_machine.cc @@ -1084,7 +1084,7 @@ public: /* Note, that this type is actually LONG WRITE. * Meaning all the Prepare Writes plus Execute is handled in the stack */ - write_type = GATT_WRITE_PREPARE; + write_type = GATT_WRITE; } BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_, leAudioDevice->ctp_hdls_.val_hdl, @@ -3045,7 +3045,7 @@ private: log::info("Group {} is doing autonomous release", group->group_id_); SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_IDLE); state_machine_callbacks_->StatusReportCb(group->group_id_, - GroupStreamStatus::RELEASING); + GroupStreamStatus::RELEASING_AUTONOMOUS); } } diff --git a/system/bta/le_audio/state_machine_test.cc b/system/bta/le_audio/state_machine_test.cc index 7fc2985b28..682bb9c722 100644 --- a/system/bta/le_audio/state_machine_test.cc +++ b/system/bta/le_audio/state_machine_test.cc @@ -4147,9 +4147,13 @@ TEST_F(StateMachineTest, testAutonomousReleaseMultiple) { // Validate GroupStreamStatus EXPECT_CALL(mock_callbacks_, - StatusReportCb(leaudio_group_id, bluetooth::le_audio::GroupStreamStatus::RELEASING)) + StatusReportCb(leaudio_group_id, + bluetooth::le_audio::GroupStreamStatus::RELEASING_AUTONOMOUS)) .Times(1); EXPECT_CALL(mock_callbacks_, + StatusReportCb(leaudio_group_id, bluetooth::le_audio::GroupStreamStatus::RELEASING)) + .Times(0); + EXPECT_CALL(mock_callbacks_, StatusReportCb(leaudio_group_id, bluetooth::le_audio::GroupStreamStatus::IDLE)) .Times(1); EXPECT_CALL(mock_callbacks_, diff --git a/system/bta/ras/ras_client.cc b/system/bta/ras/ras_client.cc index 196ec06a5e..b052717d14 100644 --- a/system/bta/ras/ras_client.cc +++ b/system/bta/ras/ras_client.cc @@ -51,6 +51,7 @@ using namespace bluetooth; using namespace ::ras; using namespace ::ras::feature; using namespace ::ras::uuid; +using bluetooth::ras::RasDisconnectReason; using bluetooth::ras::VendorSpecificCharacteristic; namespace { @@ -287,6 +288,8 @@ public: if (evt.status != GATT_SUCCESS) { log::error("Failed to connect to server device {}", evt.remote_bda); + callbacks_->OnDisconnected(tracker->address_for_cs_, + RasDisconnectReason::SERVER_NOT_AVAILABLE); return; } tracker->conn_id_ = evt.conn_id; @@ -308,7 +311,7 @@ public: BTA_GATTC_Close(evt.conn_id); return; } - callbacks_->OnDisconnected(tracker->address_for_cs_); + callbacks_->OnDisconnected(tracker->address_for_cs_, RasDisconnectReason::GATT_DISCONNECT); trackers_.remove(tracker); } @@ -338,6 +341,8 @@ public: return; } else if (!service_found) { log::error("Can't find Ranging Service in the services list"); + callbacks_->OnDisconnected(tracker->address_for_cs_, + RasDisconnectReason::SERVER_NOT_AVAILABLE); return; } else { log::info("Found Ranging Service"); @@ -347,7 +352,10 @@ public: if (UseCachedData(tracker)) { log::info("Use cached data for Ras features and vendor specific characteristic"); - SubscribeCharacteristic(tracker, kRasControlPointCharacteristic); + if (!SubscribeCharacteristic(tracker, kRasControlPointCharacteristic)) { + callbacks_->OnDisconnected(tracker->address_for_cs_, + RasDisconnectReason::SERVER_NOT_AVAILABLE); + } AllCharacteristicsReadComplete(tracker); } else { // Read Vendor Specific Uuid @@ -370,6 +378,8 @@ public: auto characteristic = tracker->FindCharacteristicByUuid(kRasFeaturesCharacteristic); if (characteristic == nullptr) { log::error("Can not find Characteristic for Ras Features"); + callbacks_->OnDisconnected(tracker->address_for_cs_, + RasDisconnectReason::SERVER_NOT_AVAILABLE); return; } BTA_GATTC_ReadCharacteristic( @@ -380,7 +390,9 @@ public: }, &gatt_read_callback_data_); - SubscribeCharacteristic(tracker, kRasControlPointCharacteristic); + if (!SubscribeCharacteristic(tracker, kRasControlPointCharacteristic)) { + callbacks_->OnDisconnected(tracker->address_for_cs_, RasDisconnectReason::FATAL_ERROR); + } } } @@ -628,23 +640,23 @@ public: } } - void SubscribeCharacteristic(std::shared_ptr<RasTracker> tracker, const Uuid uuid) { + bool SubscribeCharacteristic(std::shared_ptr<RasTracker> tracker, const Uuid uuid) { auto characteristic = tracker->FindCharacteristicByUuid(uuid); if (characteristic == nullptr) { log::warn("Can't find characteristic 0x{:04x}", uuid.As16Bit()); - return; + return false; } uint16_t ccc_handle = FindCccHandle(characteristic); if (ccc_handle == GAP_INVALID_HANDLE) { log::warn("Can't find Client Characteristic Configuration descriptor"); - return; + return false; } tGATT_STATUS register_status = BTA_GATTC_RegisterForNotifications(gatt_if_, tracker->address_, characteristic->value_handle); if (register_status != GATT_SUCCESS) { log::error("Fail to register, {}", gatt_status_text(register_status)); - return; + return false; } std::vector<uint8_t> value(2); @@ -664,6 +676,7 @@ public: } }, nullptr); + return true; } void UnsubscribeCharacteristic(std::shared_ptr<RasTracker> tracker, const Uuid uuid) { @@ -790,14 +803,22 @@ public: if (tracker->remote_supported_features_ & feature::kRealTimeRangingData) { log::info("Subscribe Real-time Ranging Data"); tracker->ranging_type_ = REAL_TIME; - SubscribeCharacteristic(tracker, kRasRealTimeRangingDataCharacteristic); + if (!SubscribeCharacteristic(tracker, kRasRealTimeRangingDataCharacteristic)) { + callbacks_->OnDisconnected(tracker->address_for_cs_, + RasDisconnectReason::SERVER_NOT_AVAILABLE); + return; + } SetTimeOutAlarm(tracker, kFirstSegmentRangingDataTimeoutMs, TimeoutType::FIRST_SEGMENT); } else { log::info("Subscribe On-demand Ranging Data"); tracker->ranging_type_ = ON_DEMAND; - SubscribeCharacteristic(tracker, kRasOnDemandDataCharacteristic); - SubscribeCharacteristic(tracker, kRasRangingDataReadyCharacteristic); - SubscribeCharacteristic(tracker, kRasRangingDataOverWrittenCharacteristic); + if (!SubscribeCharacteristic(tracker, kRasOnDemandDataCharacteristic) || + !SubscribeCharacteristic(tracker, kRasRangingDataReadyCharacteristic) || + !SubscribeCharacteristic(tracker, kRasRangingDataOverWrittenCharacteristic)) { + callbacks_->OnDisconnected(tracker->address_for_cs_, + RasDisconnectReason::SERVER_NOT_AVAILABLE); + return; + } SetTimeOutAlarm(tracker, kRangingDataReadyTimeoutMs, TimeoutType::RANGING_DATA_READY); } auto characteristic = tracker->FindCharacteristicByUuid(kRasRealTimeRangingDataCharacteristic); diff --git a/system/bta/ras/ras_utils_test.cc b/system/bta/ras/ras_utils_test.cc new file mode 100644 index 0000000000..7acc658265 --- /dev/null +++ b/system/bta/ras/ras_utils_test.cc @@ -0,0 +1,179 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <gtest/gtest.h> + +#include "bta/include/bta_ras_api.h" +#include "bta/ras/ras_types.h" + +class RasUtilsTest : public ::testing::Test {}; + +TEST(RasUtilsTest, GetUuidName) { + // Test known UUIDs + EXPECT_EQ(ras::uuid::getUuidName(bluetooth::Uuid::From16Bit(ras::uuid::kRangingService16Bit)), + "Ranging Service"); + EXPECT_EQ(ras::uuid::getUuidName( + bluetooth::Uuid::From16Bit(ras::uuid::kRasFeaturesCharacteristic16bit)), + "RAS Features"); + EXPECT_EQ(ras::uuid::getUuidName(bluetooth::Uuid::From16Bit( + ras::uuid::kRasRealTimeRangingDataCharacteristic16bit)), + "Real-time Ranging Data"); + EXPECT_EQ(ras::uuid::getUuidName( + bluetooth::Uuid::From16Bit(ras::uuid::kRasOnDemandDataCharacteristic16bit)), + "On-demand Ranging Data"); + EXPECT_EQ(ras::uuid::getUuidName( + bluetooth::Uuid::From16Bit(ras::uuid::kRasControlPointCharacteristic16bit)), + "RAS Control Point (RAS-CP)"); + EXPECT_EQ(ras::uuid::getUuidName( + bluetooth::Uuid::From16Bit(ras::uuid::kRasRangingDataReadyCharacteristic16bit)), + "Ranging Data Ready"); + EXPECT_EQ(ras::uuid::getUuidName(bluetooth::Uuid::From16Bit( + ras::uuid::kRasRangingDataOverWrittenCharacteristic16bit)), + "Ranging Data Overwritten"); + EXPECT_EQ(ras::uuid::getUuidName( + bluetooth::Uuid::From16Bit(ras::uuid::kClientCharacteristicConfiguration16bit)), + "Client Characteristic Configuration"); + + // Test unknown UUID + EXPECT_EQ(ras::uuid::getUuidName( + bluetooth::Uuid::FromString("00001101-0000-1000-8000-00805F9B34FB")), + "Unknown UUID"); +} + +TEST(RasUtilsTest, ParseControlPointCommand) { + // Test successful parsing of valid commands + uint8_t valid_data_get_ranging_data[] = {0x00, 0x01, 0x02}; + ras::ControlPointCommand command_get_ranging_data; + ASSERT_TRUE(ras::ParseControlPointCommand(&command_get_ranging_data, valid_data_get_ranging_data, + sizeof(valid_data_get_ranging_data))); + ASSERT_EQ(command_get_ranging_data.opcode_, ras::Opcode::GET_RANGING_DATA); + ASSERT_EQ(command_get_ranging_data.parameter_[0], 0x01); + ASSERT_EQ(command_get_ranging_data.parameter_[1], 0x02); + + uint8_t valid_data_ack_ranging_data[] = {0x01, 0x03, 0x04}; + ras::ControlPointCommand command_ack_ranging_data; + ASSERT_TRUE(ras::ParseControlPointCommand(&command_ack_ranging_data, valid_data_ack_ranging_data, + sizeof(valid_data_ack_ranging_data))); + ASSERT_EQ(command_ack_ranging_data.opcode_, ras::Opcode::ACK_RANGING_DATA); + ASSERT_EQ(command_ack_ranging_data.parameter_[0], 0x03); + ASSERT_EQ(command_ack_ranging_data.parameter_[1], 0x04); + + uint8_t valid_data_retrieve_lost_ranging_data_segments[] = {0x02, 0x05, 0x06, 0x07, 0x08}; + ras::ControlPointCommand command_retrieve_lost_ranging_data_segments; + ASSERT_TRUE( + ras::ParseControlPointCommand(&command_retrieve_lost_ranging_data_segments, + valid_data_retrieve_lost_ranging_data_segments, + sizeof(valid_data_retrieve_lost_ranging_data_segments))); + ASSERT_EQ(command_retrieve_lost_ranging_data_segments.opcode_, + ras::Opcode::RETRIEVE_LOST_RANGING_DATA_SEGMENTS); + ASSERT_EQ(command_retrieve_lost_ranging_data_segments.parameter_[0], 0x05); + ASSERT_EQ(command_retrieve_lost_ranging_data_segments.parameter_[1], 0x06); + ASSERT_EQ(command_retrieve_lost_ranging_data_segments.parameter_[2], 0x07); + ASSERT_EQ(command_retrieve_lost_ranging_data_segments.parameter_[3], 0x08); + + uint8_t valid_data_abort_operation[] = {0x03}; + ras::ControlPointCommand command_abort_operation; + ASSERT_TRUE(ras::ParseControlPointCommand(&command_abort_operation, valid_data_abort_operation, + sizeof(valid_data_abort_operation))); + ASSERT_EQ(command_abort_operation.opcode_, ras::Opcode::ABORT_OPERATION); + + uint8_t valid_data_filter[] = {0x04, 0x09, 0x0A}; + ras::ControlPointCommand command_filter; + ASSERT_TRUE(ras::ParseControlPointCommand(&command_filter, valid_data_filter, + sizeof(valid_data_filter))); + ASSERT_EQ(command_filter.opcode_, ras::Opcode::FILTER); + ASSERT_EQ(command_filter.parameter_[0], 0x09); + ASSERT_EQ(command_filter.parameter_[1], 0x0A); + + // Test failed parsing of invalid commands + uint8_t invalid_data_short_get_ranging_data[] = {0x00, 0x01}; + ras::ControlPointCommand command_invalid_short_get_ranging_data; + ASSERT_FALSE(ras::ParseControlPointCommand(&command_invalid_short_get_ranging_data, + invalid_data_short_get_ranging_data, + sizeof(invalid_data_short_get_ranging_data))); + + uint8_t invalid_data_long_get_ranging_data[] = {0x00, 0x01, 0x02, 0x03}; + ras::ControlPointCommand command_invalid_long_get_ranging_data; + ASSERT_FALSE(ras::ParseControlPointCommand(&command_invalid_long_get_ranging_data, + invalid_data_long_get_ranging_data, + sizeof(invalid_data_long_get_ranging_data))); + + uint8_t invalid_data_unknown_opcode[] = {0x05, 0x01, 0x02}; + ras::ControlPointCommand command_invalid_unknown_opcode; + ASSERT_FALSE(ras::ParseControlPointCommand(&command_invalid_unknown_opcode, + invalid_data_unknown_opcode, + sizeof(invalid_data_unknown_opcode))); +} + +TEST(RasUtilsTest, GetOpcodeText) { + // Test known opcodes + EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::GET_RANGING_DATA), "GET_RANGING_DATA"); + EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::ACK_RANGING_DATA), "ACK_RANGING_DATA"); + EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::RETRIEVE_LOST_RANGING_DATA_SEGMENTS), + "RETRIEVE_LOST_RANGING_DATA_SEGMENTS"); + EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::ABORT_OPERATION), "ABORT_OPERATION"); + EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::FILTER), "FILTER"); + + // Test unknown opcode (casting an invalid value to Opcode) + EXPECT_EQ(ras::GetOpcodeText(static_cast<ras::Opcode>(0x05)), "Unknown Opcode"); +} + +TEST(RasUtilsTest, GetResponseOpcodeValueText) { + // Test known response code values + EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::RESERVED_FOR_FUTURE_USE), + "RESERVED_FOR_FUTURE_USE"); + EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::SUCCESS), "SUCCESS"); + EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::OP_CODE_NOT_SUPPORTED), + "OP_CODE_NOT_SUPPORTED"); + EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::INVALID_PARAMETER), + "INVALID_PARAMETER"); + EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::PERSISTED), "PERSISTED"); + EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::ABORT_UNSUCCESSFUL), + "ABORT_UNSUCCESSFUL"); + EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::PROCEDURE_NOT_COMPLETED), + "PROCEDURE_NOT_COMPLETED"); + EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::SERVER_BUSY), "SERVER_BUSY"); + EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::NO_RECORDS_FOUND), + "NO_RECORDS_FOUND"); + + // Test unknown response code value (casting an invalid value to ResponseCodeValue) + EXPECT_EQ(ras::GetResponseOpcodeValueText(static_cast<ras::ResponseCodeValue>(0x09)), + "Reserved for Future Use"); +} + +TEST(RasUtilsTest, IsRangingServiceCharacteristic) { + // Test true cases for Ranging Service characteristics + EXPECT_TRUE(ras::IsRangingServiceCharacteristic( + bluetooth::Uuid::From16Bit(ras::uuid::kRangingService16Bit))); + EXPECT_TRUE(ras::IsRangingServiceCharacteristic( + bluetooth::Uuid::From16Bit(ras::uuid::kRasFeaturesCharacteristic16bit))); + EXPECT_TRUE(ras::IsRangingServiceCharacteristic( + bluetooth::Uuid::From16Bit(ras::uuid::kRasRealTimeRangingDataCharacteristic16bit))); + EXPECT_TRUE(ras::IsRangingServiceCharacteristic( + bluetooth::Uuid::From16Bit(ras::uuid::kRasOnDemandDataCharacteristic16bit))); + EXPECT_TRUE(ras::IsRangingServiceCharacteristic( + bluetooth::Uuid::From16Bit(ras::uuid::kRasControlPointCharacteristic16bit))); + EXPECT_TRUE(ras::IsRangingServiceCharacteristic( + bluetooth::Uuid::From16Bit(ras::uuid::kRasRangingDataReadyCharacteristic16bit))); + EXPECT_TRUE(ras::IsRangingServiceCharacteristic( + bluetooth::Uuid::From16Bit(ras::uuid::kRasRangingDataOverWrittenCharacteristic16bit))); + + // Test false cases for non-Ranging Service characteristics + EXPECT_FALSE(ras::IsRangingServiceCharacteristic( + bluetooth::Uuid::From16Bit(ras::uuid::kClientCharacteristicConfiguration16bit))); + EXPECT_FALSE(ras::IsRangingServiceCharacteristic( + bluetooth::Uuid::FromString("00001101-0000-1000-8000-00805F9B34FB"))); // Random UUID +} diff --git a/system/bta/test/bta_disc_test.cc b/system/bta/test/bta_disc_test.cc index 58421f2962..3a1dbc9882 100644 --- a/system/bta/test/bta_disc_test.cc +++ b/system/bta/test/bta_disc_test.cc @@ -219,61 +219,7 @@ int gatt_service_cb_both_call_cnt = 0; /* This test exercises the usual service discovery flow when bonding to * dual-mode, CTKD capable device on LE transport. */ -TEST_F_WITH_FLAGS(BtaInitializedTest, bta_dm_disc_both_transports_flag_disabled, - REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(TEST_BT, bta_dm_discover_both))) { - bta_dm_disc_start(true); - - std::promise<void> gatt_triggered; - int gatt_call_cnt = 0; - base::RepeatingCallback<void(const RawAddress&)> gatt_performer = - base::BindLambdaForTesting([&](const RawAddress& /*bd_addr*/) { - gatt_call_cnt++; - gatt_triggered.set_value(); - }); - bta_dm_disc_override_gatt_performer_for_testing(gatt_performer); - - int sdp_call_cnt = 0; - base::RepeatingCallback<void(tBTA_DM_SDP_STATE*)> sdp_performer = - base::BindLambdaForTesting([&](tBTA_DM_SDP_STATE* /*sdp_state*/) { sdp_call_cnt++; }); - bta_dm_disc_override_sdp_performer_for_testing(sdp_performer); - - gatt_service_cb_both_call_cnt = 0; - service_cb_both_call_cnt = 0; - - bta_dm_disc_start_service_discovery( - {[](RawAddress, std::vector<bluetooth::Uuid>&, bool) {}, nullptr, - [](RawAddress /*addr*/, const std::vector<bluetooth::Uuid>&, tBTA_STATUS) { - service_cb_both_call_cnt++; - }}, - kRawAddress, BT_TRANSPORT_BR_EDR); - EXPECT_EQ(sdp_call_cnt, 1); - - bta_dm_disc_start_service_discovery( - {[](RawAddress, std::vector<bluetooth::Uuid>&, bool) { gatt_service_cb_both_call_cnt++; }, - nullptr, [](RawAddress /*addr*/, const std::vector<bluetooth::Uuid>&, tBTA_STATUS) {}}, - kRawAddress, BT_TRANSPORT_LE); - - // GATT discovery is queued, until SDP finishes - EXPECT_EQ(gatt_call_cnt, 0); - - bta_dm_sdp_finished(kRawAddress, BTA_SUCCESS, {}, {}); - EXPECT_EQ(service_cb_both_call_cnt, 1); - - // SDP finished, wait until GATT is triggered. - EXPECT_EQ(std::future_status::ready, - gatt_triggered.get_future().wait_for(std::chrono::seconds(1))); - bta_dm_gatt_finished(kRawAddress, BTA_SUCCESS); - EXPECT_EQ(gatt_service_cb_both_call_cnt, 1); - - bta_dm_disc_override_sdp_performer_for_testing({}); - bta_dm_disc_override_gatt_performer_for_testing({}); -} - -/* This test exercises the usual service discovery flow when bonding to - * dual-mode, CTKD capable device on LE transport. - */ -TEST_F_WITH_FLAGS(BtaInitializedTest, bta_dm_disc_both_transports_flag_enabled, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(TEST_BT, bta_dm_discover_both))) { +TEST_F(BtaInitializedTest, bta_dm_disc_both_transports) { bta_dm_disc_start(true); int gatt_call_cnt = 0; diff --git a/system/btif/include/btif_dm.h b/system/btif/include/btif_dm.h index 15a5bfa1ae..9069c97b76 100644 --- a/system/btif/include/btif_dm.h +++ b/system/btif/include/btif_dm.h @@ -104,6 +104,7 @@ void btif_dm_clear_event_filter(); void btif_dm_clear_event_mask(); void btif_dm_clear_filter_accept_list(); void btif_dm_disconnect_all_acls(); +void btif_dm_disconnect_acl(const RawAddress& bd_addr, tBT_TRANSPORT transport); void btif_dm_le_rand(bluetooth::hci::LeRandCallback callback); void btif_dm_set_event_filter_connection_setup_all_devices(); @@ -150,6 +151,7 @@ void btif_dm_get_ble_local_keys(tBTA_DM_BLE_LOCAL_KEY_MASK* p_key_mask, Octet16* tBTA_BLE_LOCAL_ID_KEYS* p_id_keys); void btif_update_remote_properties(const RawAddress& bd_addr, BD_NAME bd_name, DEV_CLASS dev_class, tBT_DEVICE_TYPE dev_type); +bool btif_is_interesting_le_service(const bluetooth::Uuid& uuid); bool check_cod_hid(const RawAddress& bd_addr); bool check_cod_hid_major(const RawAddress& bd_addr, uint32_t cod); diff --git a/system/btif/include/btif_storage.h b/system/btif/include/btif_storage.h index 0c1043faae..01432bade6 100644 --- a/system/btif/include/btif_storage.h +++ b/system/btif/include/btif_storage.h @@ -446,6 +446,7 @@ bt_status_t btif_storage_set_hid_connection_policy(const tAclLinkSpec& link_spec bt_status_t btif_storage_get_hid_connection_policy(const tAclLinkSpec& link_spec, bool* reconnect_allowed); +void btif_storage_migrate_services(); /****************************************************************************** * Exported for unit tests *****************************************************************************/ diff --git a/system/btif/src/bluetooth.cc b/system/btif/src/bluetooth.cc index 25a74ea47a..7bdf50781b 100644 --- a/system/btif/src/bluetooth.cc +++ b/system/btif/src/bluetooth.cc @@ -814,6 +814,16 @@ static int disconnect_all_acls() { return BT_STATUS_SUCCESS; } +static int disconnect_acl(const RawAddress& bd_addr, int transport) { + log::verbose("{}", bd_addr); + if (!interface_ready()) { + return BT_STATUS_NOT_READY; + } + + do_in_main_thread(base::BindOnce(btif_dm_disconnect_acl, bd_addr, to_bt_transport(transport))); + return BT_STATUS_SUCCESS; +} + static void le_rand_btif_cb(uint64_t random_number) { log::verbose(""); do_in_jni_thread(base::BindOnce( @@ -1286,6 +1296,7 @@ EXPORT_SYMBOL bt_interface_t bluetoothInterface = { .clear_event_mask = clear_event_mask, .clear_filter_accept_list = clear_filter_accept_list, .disconnect_all_acls = disconnect_all_acls, + .disconnect_acl = disconnect_acl, .le_rand = le_rand, .set_event_filter_inquiry_result_all_devices = set_event_filter_inquiry_result_all_devices, .set_default_event_mask_except = set_default_event_mask_except, diff --git a/system/btif/src/btif_a2dp_source.cc b/system/btif/src/btif_a2dp_source.cc index d58e3de696..70634b9984 100644 --- a/system/btif/src/btif_a2dp_source.cc +++ b/system/btif/src/btif_a2dp_source.cc @@ -531,9 +531,15 @@ bool btif_a2dp_source_restart_session(const RawAddress& old_peer_address, bool btif_a2dp_source_end_session(const RawAddress& peer_address) { log::info("peer_address={} state={}", peer_address, btif_a2dp_source_cb.StateStr()); - local_thread()->DoInThread(FROM_HERE, - base::BindOnce(&btif_a2dp_source_end_session_delayed, peer_address)); - btif_a2dp_source_cleanup_codec(); + if (com::android::bluetooth::flags::a2dp_source_threading_fix()) { + btif_a2dp_source_cleanup_codec(); + btif_a2dp_source_end_session_delayed(peer_address); + } else { + + local_thread()->DoInThread(FROM_HERE, + base::BindOnce(&btif_a2dp_source_end_session_delayed, peer_address)); + btif_a2dp_source_cleanup_codec(); + } return true; } diff --git a/system/btif/src/btif_core.cc b/system/btif/src/btif_core.cc index f0d06373b6..8e481daed0 100644 --- a/system/btif/src/btif_core.cc +++ b/system/btif/src/btif_core.cc @@ -134,7 +134,12 @@ int btif_is_enabled(void) { return (!btif_is_dut_mode()) && (stack_manager_get_interface()->get_stack_is_running()); } -void btif_init_ok() { btif_dm_load_ble_local_keys(); } +void btif_init_ok() { + btif_dm_load_ble_local_keys(); + if (com::android::bluetooth::flags::separate_service_storage()) { + btif_storage_migrate_services(); + } +} /******************************************************************************* * @@ -290,7 +295,7 @@ void btif_dut_mode_send(uint16_t opcode, uint8_t* buf, uint8_t len) { ****************************************************************************/ static bt_status_t btif_in_get_adapter_properties(void) { - const static uint32_t NUM_ADAPTER_PROPERTIES = 5; + static const uint32_t NUM_ADAPTER_PROPERTIES = 5; bt_property_t properties[NUM_ADAPTER_PROPERTIES]; uint32_t num_props = 0; @@ -340,12 +345,13 @@ static bt_status_t btif_in_get_adapter_properties(void) { } static bt_status_t btif_in_get_remote_device_properties(RawAddress* bd_addr) { - bt_property_t remote_properties[8]; + bt_property_t remote_properties[9]; uint32_t num_props = 0; bt_bdname_t name, alias; uint32_t cod, devtype; Uuid remote_uuids[BT_MAX_NUM_UUIDS]; + Uuid remote_uuids_le[BT_MAX_NUM_UUIDS]; memset(remote_properties, 0, sizeof(remote_properties)); BTIF_STORAGE_FILL_PROPERTY(&remote_properties[num_props], BT_PROPERTY_BDNAME, sizeof(name), @@ -369,10 +375,17 @@ static bt_status_t btif_in_get_remote_device_properties(RawAddress* bd_addr) { num_props++; BTIF_STORAGE_FILL_PROPERTY(&remote_properties[num_props], BT_PROPERTY_UUIDS, sizeof(remote_uuids), - remote_uuids); + &remote_uuids); btif_storage_get_remote_device_property(bd_addr, &remote_properties[num_props]); num_props++; + if (com::android::bluetooth::flags::separate_service_storage()) { + BTIF_STORAGE_FILL_PROPERTY(&remote_properties[num_props], BT_PROPERTY_UUIDS_LE, + sizeof(remote_uuids_le), &remote_uuids_le); + btif_storage_get_remote_device_property(bd_addr, &remote_properties[num_props]); + num_props++; + } + GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb( BT_STATUS_SUCCESS, *bd_addr, num_props, remote_properties); diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc index 5848ebba1c..6f83aee781 100644 --- a/system/btif/src/btif_dm.cc +++ b/system/btif/src/btif_dm.cc @@ -1517,15 +1517,16 @@ static void btif_dm_search_devices_evt(tBTA_DM_SEARCH_EVT event, tBTA_DM_SEARCH* } /* Returns true if |uuid| should be passed as device property */ -static bool btif_is_interesting_le_service(bluetooth::Uuid uuid) { +bool btif_is_interesting_le_service(const bluetooth::Uuid& uuid) { return uuid.As16Bit() == UUID_SERVCLASS_LE_HID || uuid == UUID_HEARING_AID || uuid == UUID_VC || uuid == UUID_CSIS || uuid == UUID_LE_AUDIO || uuid == UUID_LE_MIDI || uuid == UUID_HAS || uuid == UUID_BASS || uuid == UUID_BATTERY || uuid == ANDROID_HEADTRACKER_SERVICE_UUID; } -static bt_status_t btif_get_existing_uuids(RawAddress* bd_addr, Uuid* existing_uuids) { +static bt_status_t btif_get_existing_uuids(RawAddress* bd_addr, Uuid* existing_uuids, + bt_property_type_t property_type = BT_PROPERTY_UUIDS) { bt_property_t tmp_prop; - BTIF_STORAGE_FILL_PROPERTY(&tmp_prop, BT_PROPERTY_UUIDS, sizeof(*existing_uuids), existing_uuids); + BTIF_STORAGE_FILL_PROPERTY(&tmp_prop, property_type, sizeof(*existing_uuids), existing_uuids); return btif_storage_get_remote_device_property(bd_addr, &tmp_prop); } @@ -1537,9 +1538,10 @@ static bool btif_is_gatt_service_discovery_post_pairing(const RawAddress bd_addr (pairing_cb.gatt_over_le == btif_dm_pairing_cb_t::ServiceDiscoveryState::SCHEDULED); } -static void btif_merge_existing_uuids(RawAddress& addr, std::set<Uuid>* uuids) { +static void btif_merge_existing_uuids(RawAddress& addr, std::set<Uuid>* uuids, + bt_property_type_t property_type = BT_PROPERTY_UUIDS) { Uuid existing_uuids[BT_MAX_NUM_UUIDS] = {}; - bt_status_t lookup_result = btif_get_existing_uuids(&addr, existing_uuids); + bt_status_t lookup_result = btif_get_existing_uuids(&addr, existing_uuids, property_type); if (lookup_result == BT_STATUS_FAIL) { return; @@ -1550,18 +1552,14 @@ static void btif_merge_existing_uuids(RawAddress& addr, std::set<Uuid>* uuids) { if (btif_should_ignore_uuid(uuid)) { continue; } - if (btif_is_interesting_le_service(uuid)) { - log::info("interesting le service {} insert", uuid.ToString()); - uuids->insert(uuid); - } + + uuids->insert(uuid); } } static void btif_on_service_discovery_results(RawAddress bd_addr, const std::vector<bluetooth::Uuid>& uuids_param, tBTA_STATUS result) { - bt_property_t prop; - std::vector<uint8_t> property_value; std::set<Uuid> uuids; bool a2dp_sink_capable = false; @@ -1589,8 +1587,12 @@ static void btif_on_service_discovery_results(RawAddress bd_addr, pairing_cb.sdp_over_classic = btif_dm_pairing_cb_t::ServiceDiscoveryState::FINISHED; } - prop.type = BT_PROPERTY_UUIDS; - prop.len = 0; + std::vector<uint8_t> bredr_property_value; + std::vector<uint8_t> le_property_value; + bt_property_t uuid_props[2] = {}; + bt_property_t& bredr_prop = uuid_props[0]; + bt_property_t& le_prop = uuid_props[1]; + if ((result == BTA_SUCCESS) && !uuids_param.empty()) { log::info("New UUIDs for {}:", bd_addr); for (const auto& uuid : uuids_param) { @@ -1610,13 +1612,35 @@ static void btif_on_service_discovery_results(RawAddress bd_addr, for (auto& uuid : uuids) { auto uuid_128bit = uuid.To128BitBE(); - property_value.insert(property_value.end(), uuid_128bit.begin(), uuid_128bit.end()); + bredr_property_value.insert(bredr_property_value.end(), uuid_128bit.begin(), + uuid_128bit.end()); if (uuid == UUID_A2DP_SINK) { a2dp_sink_capable = true; } } - prop.val = (void*)property_value.data(); - prop.len = Uuid::kNumBytes128 * uuids.size(); + + bredr_prop = {BT_PROPERTY_UUIDS, static_cast<int>(Uuid::kNumBytes128 * uuids.size()), + (void*)bredr_property_value.data()}; + + if (com::android::bluetooth::flags::separate_service_storage()) { + bt_status_t ret = btif_storage_set_remote_device_property(&bd_addr, &bredr_prop); + ASSERTC(ret == BT_STATUS_SUCCESS, "storing remote classic services failed", ret); + + std::set<Uuid> le_uuids; + if (results_for_bonding_device) { + btif_merge_existing_uuids(pairing_cb.static_bdaddr, &le_uuids, BT_PROPERTY_UUIDS_LE); + btif_merge_existing_uuids(pairing_cb.bd_addr, &le_uuids, BT_PROPERTY_UUIDS_LE); + } else { + btif_merge_existing_uuids(bd_addr, &le_uuids, BT_PROPERTY_UUIDS_LE); + } + + for (auto& uuid : le_uuids) { + auto uuid_128bit = uuid.To128BitBE(); + le_property_value.insert(le_property_value.end(), uuid_128bit.begin(), uuid_128bit.end()); + } + le_prop = {BT_PROPERTY_UUIDS_LE, static_cast<int>(Uuid::kNumBytes128 * le_uuids.size()), + (void*)le_property_value.data()}; + } } bool skip_reporting_wait_for_le = false; @@ -1649,17 +1673,18 @@ static void btif_on_service_discovery_results(RawAddress bd_addr, log::info("SDP failed, send {} EIR UUIDs to unblock bonding {}", num_eir_uuids, bd_addr); for (auto eir_uuid : uuids_iter->second) { auto uuid_128bit = eir_uuid.To128BitBE(); - property_value.insert(property_value.end(), uuid_128bit.begin(), uuid_128bit.end()); + bredr_property_value.insert(bredr_property_value.end(), uuid_128bit.begin(), + uuid_128bit.end()); } eir_uuids_cache.erase(uuids_iter); } if (num_eir_uuids > 0) { - prop.val = (void*)property_value.data(); - prop.len = num_eir_uuids * Uuid::kNumBytes128; + bredr_prop.val = (void*)bredr_property_value.data(); + bredr_prop.len = num_eir_uuids * Uuid::kNumBytes128; } else { log::warn("SDP failed and we have no EIR UUIDs to report either"); - prop.val = &uuid; - prop.len = Uuid::kNumBytes128; + bredr_prop.val = &uuid; + bredr_prop.len = Uuid::kNumBytes128; } } @@ -1677,9 +1702,10 @@ static void btif_on_service_discovery_results(RawAddress bd_addr, uuids_param.size(), num_eir_uuids)); if (!uuids_param.empty() || num_eir_uuids != 0) { - /* Also write this to the NVRAM */ - const bt_status_t ret = btif_storage_set_remote_device_property(&bd_addr, &prop); - ASSERTC(ret == BT_STATUS_SUCCESS, "storing remote services failed", ret); + if (!com::android::bluetooth::flags::separate_service_storage()) { + const bt_status_t ret = btif_storage_set_remote_device_property(&bd_addr, &bredr_prop); + ASSERTC(ret == BT_STATUS_SUCCESS, "storing remote services failed", ret); + } if (skip_reporting_wait_for_le) { log::info( @@ -1694,16 +1720,13 @@ static void btif_on_service_discovery_results(RawAddress bd_addr, } /* Send the event to the BTIF */ - GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb(BT_STATUS_SUCCESS, bd_addr, - 1, &prop); + GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb( + BT_STATUS_SUCCESS, bd_addr, ARRAY_SIZE(uuid_props), uuid_props); } } static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid>& services, bool is_transport_le) { - std::vector<bt_property_t> prop; - std::vector<uint8_t> property_value; - std::set<Uuid> uuids; RawAddress static_addr_copy = pairing_cb.static_bdaddr; bool lea_supported = is_le_audio_capable_during_service_discovery(bd_addr); @@ -1739,6 +1762,7 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid BTM_LogHistory(kBtmLogTag, bd_addr, "Discovered GATT services using SDP transport"); } + std::set<Uuid> uuids; for (Uuid uuid : services) { if (btif_is_interesting_le_service(uuid)) { if (btif_should_ignore_uuid(uuid)) { @@ -1767,46 +1791,28 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid log::info("Will return Classic SDP results, if done, to unblock bonding"); } - Uuid existing_uuids[BT_MAX_NUM_UUIDS] = {}; - - // Look up UUIDs using pseudo address (either RPA or static address) - bt_status_t existing_lookup_result = btif_get_existing_uuids(&bd_addr, existing_uuids); - - if (existing_lookup_result != BT_STATUS_FAIL) { - log::info("Got some existing UUIDs by address {}", bd_addr); - - for (int i = 0; i < BT_MAX_NUM_UUIDS; i++) { - Uuid uuid = existing_uuids[i]; - if (uuid.IsEmpty()) { - continue; - } - uuids.insert(uuid); + if (!com::android::bluetooth::flags::separate_service_storage()) { + // Look up UUIDs using pseudo address (either RPA or static address) + btif_merge_existing_uuids(bd_addr, &uuids); + if (bd_addr != static_addr_copy) { + // Look up UUID using static address, if different than sudo address + btif_merge_existing_uuids(static_addr_copy, &uuids); } } - if (bd_addr != static_addr_copy) { - // Look up UUID using static address, if different than sudo address - existing_lookup_result = btif_get_existing_uuids(&static_addr_copy, existing_uuids); - if (existing_lookup_result != BT_STATUS_FAIL) { - log::info("Got some existing UUIDs by static address {}", static_addr_copy); - for (int i = 0; i < BT_MAX_NUM_UUIDS; i++) { - Uuid uuid = existing_uuids[i]; - if (uuid.IsEmpty()) { - continue; - } - uuids.insert(uuid); - } - } - } + std::vector<bt_property_t> prop; + std::vector<uint8_t> property_value; for (auto& uuid : uuids) { auto uuid_128bit = uuid.To128BitBE(); property_value.insert(property_value.end(), uuid_128bit.begin(), uuid_128bit.end()); } - prop.push_back(bt_property_t{BT_PROPERTY_UUIDS, - static_cast<int>(Uuid::kNumBytes128 * uuids.size()), - (void*)property_value.data()}); + prop.push_back(bt_property_t{ + (com::android::bluetooth::flags::separate_service_storage() && is_transport_le) + ? BT_PROPERTY_UUIDS_LE + : BT_PROPERTY_UUIDS, + static_cast<int>(Uuid::kNumBytes128 * uuids.size()), (void*)property_value.data()}); /* Also write this to the NVRAM */ bt_status_t ret = btif_storage_set_remote_device_property(&bd_addr, &prop[0]); @@ -1817,8 +1823,7 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid * send them with rest of SDP results in on_service_discovery_results */ return; } else { - if (pairing_cb.sdp_over_classic == btif_dm_pairing_cb_t::ServiceDiscoveryState::SCHEDULED && - com::android::bluetooth::flags::bta_dm_discover_both()) { + if (pairing_cb.sdp_over_classic == btif_dm_pairing_cb_t::ServiceDiscoveryState::SCHEDULED) { /* Don't report services yet, they will be reported together once SDP * finishes. */ log::info("will report services later, with SDP results {}", bd_addr); @@ -1826,6 +1831,32 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid } } + if (!com::android::bluetooth::flags::separate_service_storage()) { + /* Send the event to the BTIF */ + GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb(BT_STATUS_SUCCESS, bd_addr, + prop.size(), prop.data()); + return; + } + + std::set<Uuid> bredr_uuids; + // Look up UUIDs using pseudo address (either RPA or static address) + btif_merge_existing_uuids(bd_addr, &bredr_uuids); + if (bd_addr != static_addr_copy) { + // Look up UUID using static address, if different than sudo address + btif_merge_existing_uuids(static_addr_copy, &bredr_uuids); + } + + std::vector<uint8_t> bredr_property_value; + + for (auto& uuid : bredr_uuids) { + auto uuid_128bit = uuid.To128BitBE(); + bredr_property_value.insert(bredr_property_value.end(), uuid_128bit.begin(), uuid_128bit.end()); + } + + prop.push_back(bt_property_t{BT_PROPERTY_UUIDS, + static_cast<int>(Uuid::kNumBytes128 * bredr_uuids.size()), + (void*)bredr_property_value.data()}); + /* Send the event to the BTIF */ GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb(BT_STATUS_SUCCESS, bd_addr, prop.size(), prop.data()); @@ -2517,6 +2548,9 @@ void btif_dm_cancel_bond(const RawAddress bd_addr) { ** 2. special handling for HID devices */ if (is_bonding_or_sdp()) { + // clear sdp_attempts + pairing_cb.sdp_attempts = 0; + if (com::android::bluetooth::flags::ignore_unrelated_cancel_bond() && (pairing_cb.bd_addr != bd_addr)) { log::warn("Ignoring bond cancel for unrelated device: {} pairing: {}", bd_addr, @@ -3896,6 +3930,30 @@ void btif_dm_clear_filter_accept_list() { BTA_DmClearFilterAcceptList(); } void btif_dm_disconnect_all_acls() { BTA_DmDisconnectAllAcls(); } +void btif_dm_disconnect_acl(const RawAddress& bd_addr, tBT_TRANSPORT transport) { + log::debug(" {}, transport {}", bd_addr, transport); + + if (transport == BT_TRANSPORT_LE || transport == BT_TRANSPORT_AUTO) { + uint16_t acl_handle = + get_btm_client_interface().peer.BTM_GetHCIConnHandle(bd_addr, BT_TRANSPORT_LE); + + log::debug("{}, le_acl_handle: {:#x}", bd_addr, acl_handle); + if (acl_handle != HCI_INVALID_HANDLE) { + acl_disconnect_from_handle(acl_handle, HCI_ERR_PEER_USER, "bt_btif_dm disconnect"); + } + } + + if (transport == BT_TRANSPORT_BR_EDR || transport == BT_TRANSPORT_AUTO) { + uint16_t acl_handle = + get_btm_client_interface().peer.BTM_GetHCIConnHandle(bd_addr, BT_TRANSPORT_BR_EDR); + + log::debug("{}, bredr_acl_handle: {:#x}", bd_addr, acl_handle); + if (acl_handle != HCI_INVALID_HANDLE) { + acl_disconnect_from_handle(acl_handle, HCI_ERR_PEER_USER, "bt_btif_dm disconnect"); + } + } +} + void btif_dm_le_rand(bluetooth::hci::LeRandCallback callback) { BTA_DmLeRand(std::move(callback)); } void btif_dm_set_event_filter_connection_setup_all_devices() { diff --git a/system/btif/src/btif_sock_rfc.cc b/system/btif/src/btif_sock_rfc.cc index de091e8b05..d23f012249 100644 --- a/system/btif/src/btif_sock_rfc.cc +++ b/system/btif/src/btif_sock_rfc.cc @@ -196,7 +196,7 @@ static rfc_slot_t* find_rfc_slot_by_pending_sdp(void) { static bool is_requesting_sdp(void) { for (size_t i = 0; i < ARRAY_SIZE(rfc_slots); ++i) { if (rfc_slots[i].id && rfc_slots[i].f.doing_sdp_request) { - log::info("slot id {} is doing sdp request", rfc_slots[i].id); + log::info("slot_id {} is doing sdp request", rfc_slots[i].id); return true; } } @@ -467,7 +467,7 @@ bt_status_t btsock_rfc_connect(const RawAddress* bd_addr, const Uuid* service_uu } if (!send_app_scn(slot)) { - log::error("send_app_scn() failed, closing slot->id:{}", slot->id); + log::error("send_app_scn() failed, closing slot_id:{}", slot->id); cleanup_rfc_slot(slot); return BT_STATUS_SOCKET_ERROR; } @@ -536,7 +536,7 @@ static void cleanup_rfc_slot(rfc_slot_t* slot) { close(slot->fd); log::info( "disconnected from RFCOMM socket connections for device: {}, scn: {}, " - "app_uid: {}, id: {}, socket_id: {}", + "app_uid: {}, slot_id: {}, socket_id: {}", slot->addr, slot->scn, slot->app_uid, slot->id, slot->socket_id); btif_sock_connection_logger( slot->addr, slot->id, BTSOCK_RFCOMM, SOCKET_CONNECTION_STATE_DISCONNECTED, @@ -586,7 +586,7 @@ static bool send_app_scn(rfc_slot_t* slot) { // already sent, just return success. return true; } - log::debug("Sending scn for slot {}. bd_addr:{}", slot->id, slot->addr); + log::debug("Sending scn for slot_id {}. bd_addr:{}", slot->id, slot->addr); slot->scn_notified = true; return sock_send_all(slot->fd, (const uint8_t*)&slot->scn, sizeof(slot->scn)) == sizeof(slot->scn); @@ -615,10 +615,10 @@ static void on_cl_rfc_init(tBTA_JV_RFCOMM_CL_INIT* p_init, uint32_t id) { std::unique_lock<std::recursive_mutex> lock(slot_lock); rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found. p_init->status={}", id, + log::error("RFCOMM slot_id {} not found. p_init->status={}", id, bta_jv_status_text(p_init->status)); } else if (p_init->status != tBTA_JV_STATUS::SUCCESS) { - log::warn("INIT unsuccessful, status {}. Cleaning up slot with id {}", + log::warn("INIT unsuccessful, status {}. Cleaning up slot_id {}", bta_jv_status_text(p_init->status), slot->id); cleanup_rfc_slot(slot); } else { @@ -630,10 +630,10 @@ static void on_srv_rfc_listen_started(tBTA_JV_RFCOMM_START* p_start, uint32_t id std::unique_lock<std::recursive_mutex> lock(slot_lock); rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found", id); + log::error("RFCOMM slot_id {} not found", id); return; } else if (p_start->status != tBTA_JV_STATUS::SUCCESS) { - log::warn("START unsuccessful, status {}. Cleaning up slot with id {}", + log::warn("START unsuccessful, status {}. Cleaning up slot_id {}", bta_jv_status_text(p_start->status), slot->id); cleanup_rfc_slot(slot); return; @@ -702,7 +702,7 @@ static uint32_t on_srv_rfc_connect(tBTA_JV_RFCOMM_SRV_OPEN* p_open, uint32_t id) rfc_slot_t* accept_rs; rfc_slot_t* srv_rs = find_rfc_slot_by_id(id); if (!srv_rs) { - log::error("RFCOMM slot with id {} not found.", id); + log::error("RFCOMM slot_id {} not found.", id); return 0; } @@ -719,7 +719,7 @@ static uint32_t on_srv_rfc_connect(tBTA_JV_RFCOMM_SRV_OPEN* p_open, uint32_t id) log::info( "connected to RFCOMM socket connections for device: {}, scn: {}, " - "app_uid: {}, id: {}, socket_id: {}", + "app_uid: {}, slot_id: {}, socket_id: {}", accept_rs->addr, accept_rs->scn, accept_rs->app_uid, id, accept_rs->socket_id); btif_sock_connection_logger(accept_rs->addr, accept_rs->id, BTSOCK_RFCOMM, SOCKET_CONNECTION_STATE_CONNECTED, @@ -779,12 +779,12 @@ static void on_cli_rfc_connect(tBTA_JV_RFCOMM_OPEN* p_open, uint32_t id) { std::unique_lock<std::recursive_mutex> lock(slot_lock); rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found.", id); + log::error("RFCOMM slot_id {} not found.", id); return; } if (p_open->status != tBTA_JV_STATUS::SUCCESS) { - log::warn("CONNECT unsuccessful, status {}. Cleaning up slot with id {}", + log::warn("CONNECT unsuccessful, status {}. Cleaning up slot_id {}", bta_jv_status_text(p_open->status), slot->id); cleanup_rfc_slot(slot); return; @@ -906,7 +906,7 @@ static void on_rfc_close(tBTA_JV_RFCOMM_CLOSE* /* p_close */, uint32_t id) { static void on_rfc_write_done(tBTA_JV_RFCOMM_WRITE* p, uint32_t id) { if (p->status != tBTA_JV_STATUS::SUCCESS) { - log::error("error writing to RFCOMM socket with slot {}.", p->req_id); + log::error("error writing to RFCOMM socket, req_id:{}.", p->req_id); return; } @@ -915,7 +915,7 @@ static void on_rfc_write_done(tBTA_JV_RFCOMM_WRITE* p, uint32_t id) { rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found.", id); + log::error("RFCOMM slot_id {} not found.", id); return; } app_uid = slot->app_uid; @@ -931,7 +931,7 @@ static void on_rfc_outgoing_congest(tBTA_JV_RFCOMM_CONG* p, uint32_t id) { rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found.", id); + log::error("RFCOMM slot_id {} not found.", id); return; } @@ -946,39 +946,39 @@ static uint32_t rfcomm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t rfcomm switch (event) { case BTA_JV_RFCOMM_START_EVT: - log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); + log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); on_srv_rfc_listen_started(&p_data->rfc_start, rfcomm_slot_id); break; case BTA_JV_RFCOMM_CL_INIT_EVT: - log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); + log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); on_cl_rfc_init(&p_data->rfc_cl_init, rfcomm_slot_id); break; case BTA_JV_RFCOMM_OPEN_EVT: - log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); + log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); BTA_JvSetPmProfile(p_data->rfc_open.handle, BTA_JV_PM_ID_1, BTA_JV_CONN_OPEN); on_cli_rfc_connect(&p_data->rfc_open, rfcomm_slot_id); break; case BTA_JV_RFCOMM_SRV_OPEN_EVT: - log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); + log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); BTA_JvSetPmProfile(p_data->rfc_srv_open.handle, BTA_JV_PM_ALL, BTA_JV_CONN_OPEN); id = on_srv_rfc_connect(&p_data->rfc_srv_open, rfcomm_slot_id); break; case BTA_JV_RFCOMM_CLOSE_EVT: - log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); + log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); on_rfc_close(&p_data->rfc_close, rfcomm_slot_id); break; case BTA_JV_RFCOMM_WRITE_EVT: - log::verbose("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); + log::verbose("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); on_rfc_write_done(&p_data->rfc_write, rfcomm_slot_id); break; case BTA_JV_RFCOMM_CONG_EVT: - log::verbose("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); + log::verbose("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id); on_rfc_outgoing_congest(&p_data->rfc_cong, rfcomm_slot_id); break; @@ -987,20 +987,20 @@ static uint32_t rfcomm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t rfcomm break; default: - log::warn("unhandled event {}, slot id: {}", bta_jv_event_text(event), rfcomm_slot_id); + log::warn("unhandled event {}, slot_id: {}", bta_jv_event_text(event), rfcomm_slot_id); break; } return id; } static void jv_dm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t id) { - log::info("handling event:{}, id:{}", bta_jv_event_text(event), id); + log::info("handling event:{}, slot_id:{}", bta_jv_event_text(event), id); switch (event) { case BTA_JV_GET_SCN_EVT: { std::unique_lock<std::recursive_mutex> lock(slot_lock); rfc_slot_t* rs = find_rfc_slot_by_id(id); if (!rs) { - log::error("RFCOMM slot with id {} not found. event:{}", id, bta_jv_event_text(event)); + log::error("RFCOMM slot with slot_id {} not found. event:{}", id, bta_jv_event_text(event)); break; } if (p_data->scn == 0) { @@ -1061,7 +1061,7 @@ static void jv_dm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t id) { rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found. event:{}", id, bta_jv_event_text(event)); + log::error("RFCOMM slot_id {} not found. event:{}", id, bta_jv_event_text(event)); break; } @@ -1104,7 +1104,7 @@ static void jv_dm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t id) { } default: - log::debug("unhandled event:{}, slot id:{}", bta_jv_event_text(event), id); + log::debug("unhandled event:{}, slot_id:{}", bta_jv_event_text(event), id); break; } } @@ -1112,18 +1112,18 @@ static void jv_dm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t id) { static void handle_discovery_comp(tBTA_JV_STATUS status, int scn, uint32_t id) { rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found. event: BTA_JV_DISCOVERY_COMP_EVT", id); + log::error("RFCOMM slot_id {} not found. event: BTA_JV_DISCOVERY_COMP_EVT", id); return; } if (!slot->f.doing_sdp_request) { - log::error("SDP response returned but RFCOMM slot {} did not request SDP record.", id); + log::error("SDP response returned but RFCOMM slot_id {} did not request SDP record.", id); return; } if (status != tBTA_JV_STATUS::SUCCESS || !scn) { log::error( - "SDP service discovery completed for slot id: {} with the result " + "SDP service discovery completed for slot_id: {} with the result " "status: {}, scn: {}", id, bta_jv_status_text(status), scn); cleanup_rfc_slot(slot); @@ -1146,10 +1146,7 @@ static void handle_discovery_comp(tBTA_JV_STATUS status, int scn, uint32_t id) { if (BTA_JvRfcommConnect(slot->security, scn, slot->addr, rfcomm_cback, slot->id, cfg, slot->app_uid) != tBTA_JV_STATUS::SUCCESS) { - log::warn( - "BTA_JvRfcommConnect() returned BTA_JV_FAILURE for RFCOMM slot with " - "id: {}", - id); + log::warn("BTA_JvRfcommConnect() returned BTA_JV_FAILURE for RFCOMM slot_id:{}", id); cleanup_rfc_slot(slot); return; } @@ -1158,7 +1155,7 @@ static void handle_discovery_comp(tBTA_JV_STATUS status, int scn, uint32_t id) { slot->f.doing_sdp_request = false; if (!send_app_scn(slot)) { - log::warn("send_app_scn() failed, closing slot->id {}", slot->id); + log::warn("send_app_scn() failed, closing slot_id {}", slot->id); cleanup_rfc_slot(slot); return; } @@ -1233,7 +1230,7 @@ static bool flush_incoming_que_on_wr_signal(rfc_slot_t* slot) { static bool btsock_rfc_read_signaled_on_connected_socket(int /* fd */, int flags, uint32_t /* id */, rfc_slot_t* slot) { if (!slot->f.connected) { - log::error("socket signaled for read while disconnected, slot: {}, channel: {}", slot->id, + log::error("socket signaled for read while disconnected, slot_id: {}, channel: {}", slot->id, slot->scn); return false; } @@ -1260,7 +1257,7 @@ static bool btsock_rfc_read_signaled_on_listen_socket(int fd, int /* flags */, u return false; } slot->is_accepting = accept_signal.is_accepting; - log::info("Server socket: {}, is_accepting: {}", slot->id, slot->is_accepting); + log::info("Server socket slot_id: {}, is_accepting: {}", slot->id, slot->is_accepting); } return true; } @@ -1270,7 +1267,7 @@ static void btsock_rfc_signaled_flagged(int fd, int flags, uint32_t id) { std::unique_lock<std::recursive_mutex> lock(slot_lock); rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::warn("RFCOMM slot with id {} not found.", id); + log::warn("RFCOMM slot_id {} not found.", id); return; } @@ -1294,7 +1291,7 @@ static void btsock_rfc_signaled_flagged(int fd, int flags, uint32_t id) { if (!slot->f.connected || !flush_incoming_que_on_wr_signal(slot)) { log::error( "socket signaled for write while disconnected (or write failure), " - "slot: {}, channel: {}", + "slot_id: {}, channel: {}", slot->id, slot->scn); need_close = true; } @@ -1335,7 +1332,7 @@ void btsock_rfc_signaled(int fd, int flags, uint32_t id) { BTA_JvRfcommWrite(slot->rfc_handle, slot->id); } } else { - log::error("socket signaled for read while disconnected, slot: {}, channel: {}", slot->id, + log::error("socket signaled for read while disconnected, slot_id: {}, channel: {}", slot->id, slot->scn); need_close = true; } @@ -1346,7 +1343,7 @@ void btsock_rfc_signaled(int fd, int flags, uint32_t id) { if (!slot->f.connected || !flush_incoming_que_on_wr_signal(slot)) { log::error( "socket signaled for write while disconnected (or write failure), " - "slot: {}, channel: {}", + "slot_id: {}, channel: {}", slot->id, slot->scn); need_close = true; } @@ -1372,7 +1369,7 @@ int bta_co_rfc_data_incoming(uint32_t id, BT_HDR* p_buf) { std::unique_lock<std::recursive_mutex> lock(slot_lock); rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found.", id); + log::error("RFCOMM slot_id {} not found.", id); return 0; } @@ -1412,7 +1409,7 @@ int bta_co_rfc_data_outgoing_size(uint32_t id, int* size) { std::unique_lock<std::recursive_mutex> lock(slot_lock); rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found.", id); + log::error("RFCOMM slot_id {} not found.", id); return false; } @@ -1430,7 +1427,7 @@ int bta_co_rfc_data_outgoing(uint32_t id, uint8_t* buf, uint16_t size) { std::unique_lock<std::recursive_mutex> lock(slot_lock); rfc_slot_t* slot = find_rfc_slot_by_id(id); if (!slot) { - log::error("RFCOMM slot with id {} not found.", id); + log::error("RFCOMM slot_id {} not found.", id); return false; } diff --git a/system/btif/src/btif_storage.cc b/system/btif/src/btif_storage.cc index e51756483c..4704362f2c 100644 --- a/system/btif/src/btif_storage.cc +++ b/system/btif/src/btif_storage.cc @@ -47,6 +47,7 @@ #include "btif/include/btif_api.h" #include "btif/include/btif_config.h" +#include "btif/include/btif_dm.h" #include "btif/include/btif_util.h" #include "btif/include/core_callbacks.h" #include "btif/include/stack_manager_t.h" @@ -179,15 +180,18 @@ static bool prop2cfg(const RawAddress* remote_bd_addr, bt_property_t* prop) { case BT_PROPERTY_TYPE_OF_DEVICE: btif_config_set_int(bdstr, BTIF_STORAGE_KEY_DEV_TYPE, *reinterpret_cast<int*>(prop->val)); break; - case BT_PROPERTY_UUIDS: { + case BT_PROPERTY_UUIDS: + case BT_PROPERTY_UUIDS_LE: { std::string val; size_t cnt = (prop->len) / sizeof(Uuid); for (size_t i = 0; i < cnt; i++) { val += (reinterpret_cast<Uuid*>(prop->val) + i)->ToString() + " "; } - btif_config_set_str(bdstr, BTIF_STORAGE_KEY_REMOTE_SERVICE, val); - break; - } + std::string key = (prop->type == BT_PROPERTY_UUIDS_LE) ? BTIF_STORAGE_KEY_REMOTE_SERVICE_LE + : BTIF_STORAGE_KEY_REMOTE_SERVICE; + btif_config_set_str(bdstr, key, val); + } break; + case BT_PROPERTY_REMOTE_VERSION_INFO: { bt_remote_version_t* info = reinterpret_cast<bt_remote_version_t*>(prop->val); @@ -300,10 +304,15 @@ static bool cfg2prop(const RawAddress* remote_bd_addr, bt_property_t* prop) { reinterpret_cast<int*>(prop->val)); } break; - case BT_PROPERTY_UUIDS: { + case BT_PROPERTY_UUIDS: + case BT_PROPERTY_UUIDS_LE: { char value[1280]; int size = sizeof(value); - if (btif_config_get_str(bdstr, BTIF_STORAGE_KEY_REMOTE_SERVICE, value, &size)) { + + std::string key = (prop->type == BT_PROPERTY_UUIDS_LE) ? BTIF_STORAGE_KEY_REMOTE_SERVICE_LE + : BTIF_STORAGE_KEY_REMOTE_SERVICE; + + if (btif_config_get_str(bdstr, key, value, &size)) { Uuid* p_uuid = reinterpret_cast<Uuid*>(prop->val); size_t num_uuids = btif_split_uuids_string(value, p_uuid, BT_MAX_NUM_UUIDS); prop->len = num_uuids * sizeof(Uuid); @@ -938,13 +947,14 @@ bt_status_t btif_storage_load_bonded_devices(void) { uint32_t i = 0; bt_property_t adapter_props[6]; uint32_t num_props = 0; - bt_property_t remote_properties[10]; + bt_property_t remote_properties[11]; RawAddress addr; bt_bdname_t name, alias, model_name; bt_scan_mode_t mode; uint32_t disc_timeout; Uuid local_uuids[BT_MAX_NUM_UUIDS]; Uuid remote_uuids[BT_MAX_NUM_UUIDS]; + Uuid remote_uuids_le[BT_MAX_NUM_UUIDS]; bt_status_t status; remove_devices_with_sample_ltk(); @@ -1026,10 +1036,16 @@ bt_status_t btif_storage_load_bonded_devices(void) { sizeof(devtype), &remote_properties[num_props]); num_props++; - btif_storage_get_remote_prop(p_remote_addr, BT_PROPERTY_UUIDS, remote_uuids, + btif_storage_get_remote_prop(p_remote_addr, BT_PROPERTY_UUIDS, &remote_uuids, sizeof(remote_uuids), &remote_properties[num_props]); num_props++; + if (com::android::bluetooth::flags::separate_service_storage()) { + btif_storage_get_remote_prop(p_remote_addr, BT_PROPERTY_UUIDS_LE, &remote_uuids_le, + sizeof(remote_uuids_le), &remote_properties[num_props]); + num_props++; + } + // Floss needs appearance for metrics purposes uint16_t appearance = 0; if (btif_storage_get_remote_prop(p_remote_addr, BT_PROPERTY_APPEARANCE, &appearance, @@ -1438,6 +1454,48 @@ void btif_storage_remove_gatt_cl_db_hash(const RawAddress& bd_addr) { bd_addr)); } +// TODO(b/369381361) Remove this function after all devices are migrated +void btif_storage_migrate_services() { + for (const auto& mac_address : btif_config_get_paired_devices()) { + auto addr_str = mac_address.ToString(); + + int device_type = BT_DEVICE_TYPE_UNKNOWN; + btif_config_get_int(addr_str, BTIF_STORAGE_KEY_DEV_TYPE, &device_type); + + if ((device_type == BT_DEVICE_TYPE_BREDR) || + btif_config_exist(addr_str, BTIF_STORAGE_KEY_REMOTE_SERVICE_LE)) { + /* Classic only, or already migrated entries don't need migration */ + continue; + } + + bt_property_t remote_uuids_prop; + Uuid remote_uuids[BT_MAX_NUM_UUIDS]; + BTIF_STORAGE_FILL_PROPERTY(&remote_uuids_prop, BT_PROPERTY_UUIDS, sizeof(remote_uuids), + remote_uuids); + btif_storage_get_remote_device_property(&mac_address, &remote_uuids_prop); + + log::info("Will migrate Services => ServicesLe for {}", mac_address.ToStringForLogging()); + + std::vector<uint8_t> property_value; + for (auto& uuid : remote_uuids) { + if (!btif_is_interesting_le_service(uuid)) { + continue; + } + + log::info("interesting LE service: {}", uuid); + auto uuid_128bit = uuid.To128BitBE(); + property_value.insert(property_value.end(), uuid_128bit.begin(), uuid_128bit.end()); + } + + bt_property_t le_uuids_prop{BT_PROPERTY_UUIDS_LE, static_cast<int>(property_value.size()), + (void*)property_value.data()}; + + /* Write LE services to storage */ + btif_storage_set_remote_device_property(&mac_address, &le_uuids_prop); + log::info("Migration finished for {}", mac_address.ToStringForLogging()); + } +} + void btif_debug_linkkey_type_dump(int fd) { dprintf(fd, "\nLink Key Types:\n"); for (const auto& bd_addr : btif_config_get_paired_devices()) { diff --git a/system/btif/src/btif_util.cc b/system/btif/src/btif_util.cc index 85c0069eaa..c6cf544d4a 100644 --- a/system/btif/src/btif_util.cc +++ b/system/btif/src/btif_util.cc @@ -129,6 +129,7 @@ std::string dump_property_type(bt_property_type_t type) { CASE_RETURN_STRING(BT_PROPERTY_ADAPTER_DISCOVERABLE_TIMEOUT); CASE_RETURN_STRING(BT_PROPERTY_ADAPTER_BONDED_DEVICES); CASE_RETURN_STRING(BT_PROPERTY_REMOTE_FRIENDLY_NAME); + CASE_RETURN_STRING(BT_PROPERTY_UUIDS_LE); default: RETURN_UNKNOWN_TYPE_STRING(bt_property_type_t, type); } diff --git a/system/btif/test/btif_core_test.cc b/system/btif/test/btif_core_test.cc index 66881a92d8..1bffc9ce3a 100644 --- a/system/btif/test/btif_core_test.cc +++ b/system/btif/test/btif_core_test.cc @@ -320,6 +320,7 @@ TEST_F(BtifUtilsTest, dump_property_type) { "BT_PROPERTY_ADAPTER_DISCOVERABLE_TIMEOUT"), std::make_pair(BT_PROPERTY_ADAPTER_BONDED_DEVICES, "BT_PROPERTY_ADAPTER_BONDED_DEVICES"), std::make_pair(BT_PROPERTY_REMOTE_FRIENDLY_NAME, "BT_PROPERTY_REMOTE_FRIENDLY_NAME"), + std::make_pair(BT_PROPERTY_UUIDS_LE, "BT_PROPERTY_UUIDS_LE"), }; for (const auto& type : types) { EXPECT_TRUE(dump_property_type(type.first).starts_with(type.second)); diff --git a/system/gd/hci/distance_measurement_manager.cc b/system/gd/hci/distance_measurement_manager.cc index c0e996b12e..696f33e621 100644 --- a/system/gd/hci/distance_measurement_manager.cc +++ b/system/gd/hci/distance_measurement_manager.cc @@ -542,7 +542,8 @@ struct DistanceMeasurementManager::impl : bluetooth::hal::RangingHalCallback { } } - void handle_ras_client_disconnected_event(const Address address) { + void handle_ras_client_disconnected_event(const Address address, + const ras::RasDisconnectReason& ras_disconnect_reason) { log::info("address:{}", address); for (auto it = cs_requester_trackers_.begin(); it != cs_requester_trackers_.end();) { if (it->second.address == address) { @@ -550,8 +551,13 @@ struct DistanceMeasurementManager::impl : bluetooth::hal::RangingHalCallback { it->second.repeating_alarm->Cancel(); it->second.repeating_alarm.reset(); } - distance_measurement_callbacks_->OnDistanceMeasurementStopped( - address, REASON_NO_LE_CONNECTION, METHOD_CS); + DistanceMeasurementErrorCode reason = REASON_NO_LE_CONNECTION; + if (ras_disconnect_reason == ras::RasDisconnectReason::SERVER_NOT_AVAILABLE) { + reason = REASON_FEATURE_NOT_SUPPORTED_REMOTE; + } else if (ras_disconnect_reason == ras::RasDisconnectReason::FATAL_ERROR) { + reason = REASON_INTERNAL_ERROR; + } + distance_measurement_callbacks_->OnDistanceMeasurementStopped(address, reason, METHOD_CS); gatt_mtus_.erase(it->first); it = cs_requester_trackers_.erase(it); // erase and get the next iterator } else { @@ -930,9 +936,6 @@ struct DistanceMeasurementManager::impl : bluetooth::hal::RangingHalCallback { req_it->second.remote_support_phase_based_ranging = event_view.GetOptionalSubfeaturesSupported().phase_based_ranging_ == 0x01; req_it->second.remote_num_antennas_supported_ = event_view.GetNumAntennasSupported(); - req_it->second.setup_complete = true; - log::info("Setup phase complete, connection_handle: {}, address: {}", connection_handle, - req_it->second.address); req_it->second.retry_counter_for_create_config = 0; req_it->second.remote_supported_sw_time_ = event_view.GetTSwTimeSupported(); send_le_cs_create_config(connection_handle, req_it->second.requesting_config_id); @@ -1031,6 +1034,10 @@ struct DistanceMeasurementManager::impl : bluetooth::hal::RangingHalCallback { if (!live_tracker->local_start) { // reset the responder state, as no other event to set the state. live_tracker->state = CsTrackerState::WAIT_FOR_CONFIG_COMPLETE; + } else { + live_tracker->setup_complete = true; + log::info("connection_handle: {}, address: {}, config_id: {}", connection_handle, + live_tracker->address, config_id); } live_tracker->used_config_id = config_id; @@ -2596,8 +2603,9 @@ void DistanceMeasurementManager::HandleConnIntervalUpdated(const Address& addres conn_interval); } -void DistanceMeasurementManager::HandleRasClientDisconnectedEvent(const Address& address) { - CallOn(pimpl_.get(), &impl::handle_ras_client_disconnected_event, address); +void DistanceMeasurementManager::HandleRasClientDisconnectedEvent( + const Address& address, const ras::RasDisconnectReason& ras_disconnect_reason) { + CallOn(pimpl_.get(), &impl::handle_ras_client_disconnected_event, address, ras_disconnect_reason); } void DistanceMeasurementManager::HandleVendorSpecificReply( diff --git a/system/gd/hci/distance_measurement_manager.h b/system/gd/hci/distance_measurement_manager.h index 5d3484ff74..d8c3d49aea 100644 --- a/system/gd/hci/distance_measurement_manager.h +++ b/system/gd/hci/distance_measurement_manager.h @@ -18,6 +18,7 @@ #include <bluetooth/log.h> #include "address.h" +#include "bta/include/bta_ras_api.h" #include "hal/ranging_hal.h" #include "hci/hci_packets.h" #include "module.h" @@ -92,7 +93,8 @@ public: const Address& address, uint16_t connection_handle, uint16_t att_handle, const std::vector<hal::VendorSpecificCharacteristic>& vendor_specific_data, uint16_t conn_interval); - void HandleRasClientDisconnectedEvent(const Address& address); + void HandleRasClientDisconnectedEvent(const Address& address, + const ras::RasDisconnectReason& ras_disconnect_reason); void HandleVendorSpecificReply( const Address& address, uint16_t connection_handle, const std::vector<hal::VendorSpecificCharacteristic>& vendor_specific_reply); diff --git a/system/gd/hci/le_advertising_manager.h b/system/gd/hci/le_advertising_manager.h index 55245a5cfa..fe614861e2 100644 --- a/system/gd/hci/le_advertising_manager.h +++ b/system/gd/hci/le_advertising_manager.h @@ -47,8 +47,8 @@ class AdvertisingConfig { public: std::vector<GapData> advertisement; std::vector<GapData> scan_response; - uint16_t interval_min; - uint16_t interval_max; + uint32_t interval_min; + uint32_t interval_max; AdvertisingType advertising_type; AdvertiserAddressType requested_advertiser_address_type; PeerAddressType peer_address_type; diff --git a/system/gd/rust/topshim/le_audio/le_audio_shim.cc b/system/gd/rust/topshim/le_audio/le_audio_shim.cc index 77d7a5c60d..53a0f7d98f 100644 --- a/system/gd/rust/topshim/le_audio/le_audio_shim.cc +++ b/system/gd/rust/topshim/le_audio/le_audio_shim.cc @@ -162,6 +162,8 @@ static BtLeAudioGroupStreamStatus to_rust_btle_audio_group_stream_status( return BtLeAudioGroupStreamStatus::Streaming; case le_audio::GroupStreamStatus::RELEASING: return BtLeAudioGroupStreamStatus::Releasing; + case le_audio::GroupStreamStatus::RELEASING_AUTONOMOUS: + return BtLeAudioGroupStreamStatus::ReleasingAutonomous; case le_audio::GroupStreamStatus::SUSPENDING: return BtLeAudioGroupStreamStatus::Suspending; case le_audio::GroupStreamStatus::SUSPENDED: diff --git a/system/gd/rust/topshim/src/profiles/le_audio.rs b/system/gd/rust/topshim/src/profiles/le_audio.rs index 266f24f7fb..651ad82ecd 100644 --- a/system/gd/rust/topshim/src/profiles/le_audio.rs +++ b/system/gd/rust/topshim/src/profiles/le_audio.rs @@ -109,6 +109,7 @@ pub mod ffi { Idle = 0, Streaming, Releasing, + ReleasingAutonomous, Suspending, Suspended, ConfiguredAutonomous, @@ -413,11 +414,12 @@ impl From<BtLeAudioGroupStreamStatus> for i32 { BtLeAudioGroupStreamStatus::Idle => 0, BtLeAudioGroupStreamStatus::Streaming => 1, BtLeAudioGroupStreamStatus::Releasing => 2, - BtLeAudioGroupStreamStatus::Suspending => 3, - BtLeAudioGroupStreamStatus::Suspended => 4, - BtLeAudioGroupStreamStatus::ConfiguredAutonomous => 5, - BtLeAudioGroupStreamStatus::ConfiguredByUser => 6, - BtLeAudioGroupStreamStatus::Destroyed => 7, + BtLeAudioGroupStreamStatus::ReleasingAutonomous => 3, + BtLeAudioGroupStreamStatus::Suspending => 4, + BtLeAudioGroupStreamStatus::Suspended => 5, + BtLeAudioGroupStreamStatus::ConfiguredAutonomous => 6, + BtLeAudioGroupStreamStatus::ConfiguredByUser => 7, + BtLeAudioGroupStreamStatus::Destroyed => 8, _ => panic!("Invalid value {:?} to BtLeAudioGroupStreamStatus", value), } } @@ -429,11 +431,12 @@ impl From<i32> for BtLeAudioGroupStreamStatus { 0 => BtLeAudioGroupStreamStatus::Idle, 1 => BtLeAudioGroupStreamStatus::Streaming, 2 => BtLeAudioGroupStreamStatus::Releasing, - 3 => BtLeAudioGroupStreamStatus::Suspending, - 4 => BtLeAudioGroupStreamStatus::Suspended, - 5 => BtLeAudioGroupStreamStatus::ConfiguredAutonomous, - 6 => BtLeAudioGroupStreamStatus::ConfiguredByUser, - 7 => BtLeAudioGroupStreamStatus::Destroyed, + 3 => BtLeAudioGroupStreamStatus::ReleasingAutonomous, + 4 => BtLeAudioGroupStreamStatus::Suspending, + 5 => BtLeAudioGroupStreamStatus::Suspended, + 6 => BtLeAudioGroupStreamStatus::ConfiguredAutonomous, + 7 => BtLeAudioGroupStreamStatus::ConfiguredByUser, + 8 => BtLeAudioGroupStreamStatus::Destroyed, _ => panic!("Invalid value {} to BtLeAudioGroupStreamStatus", value), } } diff --git a/system/gd/storage/config_keys.h b/system/gd/storage/config_keys.h index 4629a494ba..d3659b6a96 100644 --- a/system/gd/storage/config_keys.h +++ b/system/gd/storage/config_keys.h @@ -111,6 +111,7 @@ #define BTIF_STORAGE_KEY_PIN_LENGTH "PinLength" #define BTIF_STORAGE_KEY_PRODUCT_ID "ProductId" #define BTIF_STORAGE_KEY_REMOTE_SERVICE "Service" +#define BTIF_STORAGE_KEY_REMOTE_SERVICE_LE "ServiceLe" #define BTIF_STORAGE_KEY_REMOTE_VER_MFCT "Manufacturer" #define BTIF_STORAGE_KEY_REMOTE_VER_SUBVER "LmpSubVer" #define BTIF_STORAGE_KEY_REMOTE_VER_VER "LmpVer" diff --git a/system/include/hardware/bluetooth.h b/system/include/hardware/bluetooth.h index a8a169a58d..fa15353d9f 100644 --- a/system/include/hardware/bluetooth.h +++ b/system/include/hardware/bluetooth.h @@ -299,7 +299,7 @@ typedef enum { */ BT_PROPERTY_TYPE_OF_DEVICE, /** - * Description - Bluetooth Service Record + * Description - Bluetooth Service Record, UUIDs on BREDR transport * Access mode - Only GET. * Data type - bt_service_record_t */ @@ -427,6 +427,14 @@ typedef enum { */ BT_PROPERTY_LPP_OFFLOAD_FEATURES, + /** + * Description - Bluetooth Service 128-bit UUIDs on LE transport + * Access mode - Only GET. + * Data type - Array of bluetooth::Uuid (Array size inferred from property + * length). + */ + BT_PROPERTY_UUIDS_LE, + BT_PROPERTY_REMOTE_DEVICE_TIMESTAMP = 0xFF, } bt_property_type_t; @@ -915,6 +923,11 @@ typedef struct { int (*disconnect_all_acls)(); /** + * Call to disconnect ACL connection to device + */ + int (*disconnect_acl)(const RawAddress& bd_addr, int transport); + + /** * Call to retrieve a generated random */ int (*le_rand)(); diff --git a/system/include/hardware/bt_le_audio.h b/system/include/hardware/bt_le_audio.h index 91ce17c388..94c3762596 100644 --- a/system/include/hardware/bt_le_audio.h +++ b/system/include/hardware/bt_le_audio.h @@ -70,6 +70,7 @@ enum class GroupStreamStatus { IDLE = 0, STREAMING, RELEASING, + RELEASING_AUTONOMOUS, SUSPENDING, SUSPENDED, CONFIGURED_AUTONOMOUS, diff --git a/system/main/shim/distance_measurement_manager.cc b/system/main/shim/distance_measurement_manager.cc index cdd069b215..fd7eb3591d 100644 --- a/system/main/shim/distance_measurement_manager.cc +++ b/system/main/shim/distance_measurement_manager.cc @@ -252,9 +252,10 @@ public: bluetooth::ToGdAddress(address), GetConnectionHandleAndRole(address), conn_interval); } - void OnDisconnected(const RawAddress& address) { + void OnDisconnected(const RawAddress& address, + const ras::RasDisconnectReason& ras_disconnect_reason) { bluetooth::shim::GetDistanceMeasurementManager()->HandleRasClientDisconnectedEvent( - bluetooth::ToGdAddress(address)); + bluetooth::ToGdAddress(address), ras_disconnect_reason); } // Must be called from main_thread diff --git a/system/rust/src/core/shared_box.rs b/system/rust/src/core/shared_box.rs index 5dd1118185..accd1ae6ec 100644 --- a/system/rust/src/core/shared_box.rs +++ b/system/rust/src/core/shared_box.rs @@ -79,7 +79,7 @@ impl<T: ?Sized> Clone for WeakBox<T> { /// A strong reference to the contents within a SharedBox<>. pub struct WeakBoxRef<'a, T: ?Sized>(&'a T, Weak<T>); -impl<'a, T: ?Sized> WeakBoxRef<'a, T> { +impl<T: ?Sized> WeakBoxRef<'_, T> { /// Downgrade to a weak reference (with static lifetime) to the contents /// within the underlying SharedBox<> pub fn downgrade(&self) -> WeakBox<T> { @@ -87,7 +87,7 @@ impl<'a, T: ?Sized> WeakBoxRef<'a, T> { } } -impl<'a, T: ?Sized> Deref for WeakBoxRef<'a, T> { +impl<T: ?Sized> Deref for WeakBoxRef<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -95,7 +95,7 @@ impl<'a, T: ?Sized> Deref for WeakBoxRef<'a, T> { } } -impl<'a, T: ?Sized> Clone for WeakBoxRef<'a, T> { +impl<T: ?Sized> Clone for WeakBoxRef<'_, T> { fn clone(&self) -> Self { Self(self.0, self.1.clone()) } diff --git a/system/stack/Android.bp b/system/stack/Android.bp index 384c31f010..783117ddc3 100644 --- a/system/stack/Android.bp +++ b/system/stack/Android.bp @@ -349,6 +349,10 @@ cc_defaults { defaults: [ "bluetooth_cflags", ], + cflags: [ + "-Wno-missing-prototypes", + "-Wno-unused-parameter", + ], include_dirs: [ "packages/modules/Bluetooth/system", "packages/modules/Bluetooth/system/gd", @@ -563,6 +567,7 @@ cc_fuzz { ":TestCommonMockFunctions", ":TestCommonStackConfig", ":TestFakeOsi", + ":TestMockBtaDm", ":TestMockBtif", ":TestMockDevice", ":TestMockMainShim", @@ -945,6 +950,7 @@ cc_test { srcs: [ ":TestCommonMainHandler", ":TestCommonMockFunctions", + ":TestMockBtaDm", ":TestMockBtif", ":TestMockDevice", ":TestMockMainShim", diff --git a/system/stack/avrc/avrc_api.cc b/system/stack/avrc/avrc_api.cc index 13184ed058..6b3c1700cb 100644 --- a/system/stack/avrc/avrc_api.cc +++ b/system/stack/avrc/avrc_api.cc @@ -25,6 +25,7 @@ #include <android_bluetooth_sysprop.h> #include <bluetooth/log.h> +#include <com_android_bluetooth_flags.h> #include <string.h> #include <cstdint> @@ -1009,7 +1010,12 @@ uint16_t AVRC_GetControlProfileVersion() { uint16_t AVRC_GetProfileVersion() { uint16_t profile_version = AVRC_REV_1_4; char avrcp_version[PROPERTY_VALUE_MAX] = {0}; - osi_property_get(AVRC_VERSION_PROPERTY, avrcp_version, AVRC_DEFAULT_VERSION); + + if (!com::android::bluetooth::flags::avrcp_16_default()) { + osi_property_get(AVRC_VERSION_PROPERTY, avrcp_version, AVRC_1_5_STRING); + } else { + osi_property_get(AVRC_VERSION_PROPERTY, avrcp_version, AVRC_1_6_STRING); + } if (!strncmp(AVRC_1_6_STRING, avrcp_version, sizeof(AVRC_1_6_STRING))) { profile_version = AVRC_REV_1_6; diff --git a/system/stack/btm/btm_ble_gap.cc b/system/stack/btm/btm_ble_gap.cc index 16e46168ba..808c9491d1 100644 --- a/system/stack/btm/btm_ble_gap.cc +++ b/system/stack/btm/btm_ble_gap.cc @@ -1909,7 +1909,7 @@ static DEV_CLASS btm_ble_appearance_to_cod(uint16_t appearance) { dev_class[1] = BTM_COD_MAJOR_PERIPHERAL; dev_class[2] = BTM_COD_MINOR_DIGITAL_PAN; break; - case BTM_BLE_APPEARANCE_UKNOWN: + case BTM_BLE_APPEARANCE_UNKNOWN: case BTM_BLE_APPEARANCE_GENERIC_CLOCK: case BTM_BLE_APPEARANCE_GENERIC_TAG: case BTM_BLE_APPEARANCE_GENERIC_KEYRING: diff --git a/system/stack/fuzzers/avrc_fuzzer.cc b/system/stack/fuzzers/avrc_fuzzer.cc index be0c80f32c..d2c4acf4f8 100644 --- a/system/stack/fuzzers/avrc_fuzzer.cc +++ b/system/stack/fuzzers/avrc_fuzzer.cc @@ -33,10 +33,6 @@ #include "test/mock/mock_stack_l2cap_api.h" #include "types/bluetooth/uuid.h" -// TODO(b/369381361) Enfore -Wmissing-prototypes -#pragma GCC diagnostic ignored "-Wmissing-prototypes" -#pragma GCC diagnostic ignored "-Wunused-parameter" - using bluetooth::Uuid; using namespace bluetooth; diff --git a/system/stack/fuzzers/bnep_fuzzer.cc b/system/stack/fuzzers/bnep_fuzzer.cc index 79a70556e4..6976ccd602 100644 --- a/system/stack/fuzzers/bnep_fuzzer.cc +++ b/system/stack/fuzzers/bnep_fuzzer.cc @@ -31,10 +31,6 @@ #include "test/mock/mock_stack_l2cap_ble.h" #include "types/bluetooth/uuid.h" -// TODO(b/369381361) Enfore -Wmissing-prototypes -#pragma GCC diagnostic ignored "-Wmissing-prototypes" -#pragma GCC diagnostic ignored "-Wunused-parameter" - using bluetooth::Uuid; namespace { diff --git a/system/stack/fuzzers/gatt_fuzzer.cc b/system/stack/fuzzers/gatt_fuzzer.cc index 3a9fb81276..064a203a81 100644 --- a/system/stack/fuzzers/gatt_fuzzer.cc +++ b/system/stack/fuzzers/gatt_fuzzer.cc @@ -33,11 +33,8 @@ #include "test/mock/mock_stack_l2cap_api.h" #include "test/mock/mock_stack_l2cap_ble.h" -// TODO(b/369381361) Enfore -Wmissing-prototypes -#pragma GCC diagnostic ignored "-Wmissing-prototypes" -#pragma GCC diagnostic ignored "-Wunused-parameter" - using bluetooth::Uuid; + bt_status_t do_in_main_thread(base::OnceCallback<void()>) { // this is not properly mocked, so we use abort to catch if this is used in // any test cases diff --git a/system/stack/fuzzers/l2cap_fuzzer.cc b/system/stack/fuzzers/l2cap_fuzzer.cc index 004e5b0920..6cb4d5170f 100644 --- a/system/stack/fuzzers/l2cap_fuzzer.cc +++ b/system/stack/fuzzers/l2cap_fuzzer.cc @@ -42,9 +42,6 @@ #include "test/mock/mock_stack_acl.h" #include "test/mock/mock_stack_btm_devctl.h" -// TODO(b/369381361) Enfore -Wmissing-prototypes -#pragma GCC diagnostic ignored "-Wmissing-prototypes" - using bluetooth::Uuid; using testing::Return; using namespace bluetooth; diff --git a/system/stack/fuzzers/rfcomm_fuzzer.cc b/system/stack/fuzzers/rfcomm_fuzzer.cc index 1445156aec..3418f3a6ed 100644 --- a/system/stack/fuzzers/rfcomm_fuzzer.cc +++ b/system/stack/fuzzers/rfcomm_fuzzer.cc @@ -39,10 +39,6 @@ #include "test/mock/mock_stack_l2cap_interface.h" #include "test/rfcomm/stack_rfcomm_test_utils.h" -// TODO(b/369381361) Enfore -Wmissing-prototypes -#pragma GCC diagnostic ignored "-Wmissing-prototypes" -#pragma GCC diagnostic ignored "-Wunused-parameter" - using ::testing::_; using ::testing::NiceMock; using ::testing::Return; diff --git a/system/stack/fuzzers/sdp_fuzzer.cc b/system/stack/fuzzers/sdp_fuzzer.cc index bd5b6f77c2..de994d415e 100644 --- a/system/stack/fuzzers/sdp_fuzzer.cc +++ b/system/stack/fuzzers/sdp_fuzzer.cc @@ -31,10 +31,6 @@ #include "test/mock/mock_stack_l2cap_api.h" #include "types/bluetooth/uuid.h" -// TODO(b/369381361) Enfore -Wmissing-prototypes -#pragma GCC diagnostic ignored "-Wmissing-prototypes" -#pragma GCC diagnostic ignored "-Wunused-parameter" - namespace { #define SDP_DB_SIZE 0x10000 diff --git a/system/stack/fuzzers/smp_fuzzer.cc b/system/stack/fuzzers/smp_fuzzer.cc index a99010254a..b4dad542ea 100644 --- a/system/stack/fuzzers/smp_fuzzer.cc +++ b/system/stack/fuzzers/smp_fuzzer.cc @@ -33,13 +33,10 @@ #include "test/mock/mock_stack_l2cap_api.h" #include "test/mock/mock_stack_l2cap_ble.h" -// TODO(b/369381361) Enfore -Wmissing-prototypes -#pragma GCC diagnostic ignored "-Wmissing-prototypes" -#pragma GCC diagnostic ignored "-Wunused-parameter" - bluetooth::common::MessageLoopThread* main_thread_ptr = nullptr; bluetooth::common::MessageLoopThread* get_main_thread() { return main_thread_ptr; } + namespace { #define SDP_DB_SIZE 0x10000 diff --git a/system/stack/include/avrc_api.h b/system/stack/include/avrc_api.h index 589ddb5021..b2f683cc0c 100644 --- a/system/stack/include/avrc_api.h +++ b/system/stack/include/avrc_api.h @@ -133,10 +133,6 @@ #define AVRC_1_3_STRING "avrcp13" #endif -#ifndef AVRC_DEFAULT_VERSION -#define AVRC_DEFAULT_VERSION AVRC_1_5_STRING -#endif - /* Configurable dynamic avrcp version enable key*/ #ifndef AVRC_DYNAMIC_AVRCP_ENABLE_PROPERTY #define AVRC_DYNAMIC_AVRCP_ENABLE_PROPERTY "persist.bluetooth.dynamic_avrcp.enable" diff --git a/system/stack/include/ble_appearance.h b/system/stack/include/ble_appearance.h new file mode 100644 index 0000000000..6dbb32996c --- /dev/null +++ b/system/stack/include/ble_appearance.h @@ -0,0 +1,446 @@ +/****************************************************************************** + * + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +#ifndef BLE_APPEARANCE_H +#define BLE_APPEARANCE_H + +/* BLE appearance values as per BT spec assigned numbers */ +/* Category[15:6] 0x000 */ +#define BLE_APPEARANCE_UNKNOWN 0x0000 + +/* Category[15:6] 0x001 */ +#define BLE_APPEARANCE_GENERIC_PHONE 0x0040 + +/* Category[15:6] 0x002 */ +#define BLE_APPEARANCE_GENERIC_COMPUTER 0x0080 + +#define BLE_APPEARANCE_DESKTOP_WORKSTATION 0x81 +#define BLE_APPEARANCE_SERVER_CLASS_COMPUTER 0x82 +#define BLE_APPEARANCE_LAPTOP 0x83 +#define BLE_APPEARANCE_HANDHELD_PC_PDA 0x84 +#define BLE_APPEARANCE_PALM_SIZE_PC_PDA 0x85 +#define BLE_APPEARANCE_WEARABLE_COMPUTER_WATCH_SIZE 0x86 +#define BLE_APPEARANCE_TABLET 0x87 +#define BLE_APPEARANCE_DOCKING_STATION 0x88 +#define BLE_APPEARANCE_ALL_IN_ONE 0x89 +#define BLE_APPEARANCE_BLADE_SERVER 0x8A +#define BLE_APPEARANCE_CONVERTIBLE 0x8B +#define BLE_APPEARANCE_DETACHABLE 0x8C +#define BLE_APPEARANCE_IOT_GATEWAY 0x8D +#define BLE_APPEARANCE_MINI_PC 0x8E +#define BLE_APPEARANCE_STICK_PC 0x8F + +/* Category[15:6] 0x003 */ +#define BLE_APPEARANCE_GENERIC_WATCH 0x00C0 +#define BLE_APPEARANCE_SPORTS_WATCH 0x00C1 +#define BLE_APPEARANCE_SMART_WATCH 0x00C2 + +/* Category[15:6] 0x004 */ +#define BLE_APPEARANCE_GENERIC_CLOCK 0x0100 + +/* Category[15:6] 0x005 */ +#define BLE_APPEARANCE_GENERIC_DISPLAY 0x0140 + +/* Category[15:6] 0x006 */ +#define BLE_APPEARANCE_GENERIC_REMOTE 0x0180 + +/* Category[15:6] 0x007 */ +#define BLE_APPEARANCE_GENERIC_EYEGLASSES 0x01C0 + +/* Category[15:6] 0x008 */ +#define BLE_APPEARANCE_GENERIC_TAG 0x0200 + +/* Category[15:6] 0x009 */ +#define BLE_APPEARANCE_GENERIC_KEYRING 0x0240 + +/* Category[15:6] 0x00A */ +#define BLE_APPEARANCE_GENERIC_MEDIA_PLAYER 0x0280 + +/* Category[15:6] 0x00B */ +#define BLE_APPEARANCE_GENERIC_BARCODE_SCANNER 0x02C0 + +/* Category[15:6] 0x00C */ +#define BLE_APPEARANCE_GENERIC_THERMOMETER 0x0300 +#define BLE_APPEARANCE_THERMOMETER_EAR 0x0301 + +/* Category[15:6] 0x00D */ +#define BLE_APPEARANCE_GENERIC_HEART_RATE 0x0340 +#define BLE_APPEARANCE_HEART_RATE_BELT 0x0341 + +/* Category[15:6] 0x00E */ +#define BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE 0x0380 +#define BLE_APPEARANCE_BLOOD_PRESSURE_ARM 0x0381 +#define BLE_APPEARANCE_BLOOD_PRESSURE_WRIST 0x0382 + +/* Category[15:6] 0x00F */ +#define BLE_APPEARANCE_GENERIC_HID 0x03C0 +#define BLE_APPEARANCE_HID_KEYBOARD 0x03C1 +#define BLE_APPEARANCE_HID_MOUSE 0x03C2 +#define BLE_APPEARANCE_HID_JOYSTICK 0x03C3 +#define BLE_APPEARANCE_HID_GAMEPAD 0x03C4 +#define BLE_APPEARANCE_HID_DIGITIZER_TABLET 0x03C5 +#define BLE_APPEARANCE_HID_CARD_READER 0x03C6 +#define BLE_APPEARANCE_HID_DIGITAL_PEN 0x03C7 +#define BLE_APPEARANCE_HID_BARCODE_SCANNER 0x03C8 +#define BLE_APPEARANCE_HID_TOUCHPAD 0x03C9 +#define BLE_APPEARANCE_HID_PRESENTATION_REMOTE 0x03CA + +/* Category[15:6] 0x010 */ +#define BLE_APPEARANCE_GENERIC_GLUCOSE 0x0400 + +/* Category[15:6] 0x011 */ +#define BLE_APPEARANCE_GENERIC_WALKING 0x0440 +#define BLE_APPEARANCE_WALKING_IN_SHOE 0x0441 +#define BLE_APPEARANCE_WALKING_ON_SHOE 0x0442 +#define BLE_APPEARANCE_WALKING_ON_HIP 0x0443 + +/* Category[15:6] 0x012 */ +#define BLE_APPEARANCE_GENERIC_CYCLING 0x0480 +#define BLE_APPEARANCE_CYCLING_COMPUTER 0x0481 +#define BLE_APPEARANCE_CYCLING_SPEED 0x0482 +#define BLE_APPEARANCE_CYCLING_CADENCE 0x0483 +#define BLE_APPEARANCE_CYCLING_POWER 0x0484 +#define BLE_APPEARANCE_CYCLING_SPEED_CADENCE 0x0485 + +/* Category[15:6] 0x013 */ +#define BLE_APPEARANCE_GENERIC_CONTROL_DEVICE 0x04C0 +#define BLE_APPEARANCE_SWITCH 0x04C1 +#define BLE_APPEARANCE_MULTI_SWITCH 0x04C2 +#define BLE_APPEARANCE_SWITCH_BUTTON 0x04C3 +#define BLE_APPEARANCE_SWITCH_SLIDER 0x04C4 +#define BLE_APPEARANCE_ROTARY_SWITCH 0x04C5 +#define BLE_APPEARANCE_TOUCH_PANEL 0x04C6 +#define BLE_APPEARANCE_SINGLE_SWITCH 0x04C7 +#define BLE_APPEARANCE_DOUBLE_SWITCH 0x04C8 +#define BLE_APPEARANCE_TRIPLE_SWITCH 0x04C9 +#define BLE_APPEARANCE_BATTERY_SWITCH 0x04CA +#define BLE_APPEARANCE_ENERGY_HARVESTING_SWITCH 0x04CB +#define BLE_APPEARANCE_SWITCH_PUSH_BUTTON 0x04CC +#define BLE_APPEARANCE_SWITCH_DIAL 0x04CD + +/* Category[15:6] 0x014 */ +#define BLE_APPEARANCE_GENERIC_NETWORK_DEVICE 0x0500 +#define BLE_APPEARANCE_NETWORK_DEVICE_ACCESS_POINT 0x0501 +#define BLE_APPEARANCE_NETWORK_DEVICE_MESH_DEVICE 0x0502 +#define BLE_APPEARANCE_NETWORK_DEVICE_MESH_NETWORK_PROXY 0x0503 + +/* Category[15:6] 0x015 */ +#define BLE_APPEARANCE_GENERIC_SENSOR 0x0540 +#define BLE_APPEARANCE_MOTION_SENSOR 0x0541 +#define BLE_APPEARANCE_AIR_QUALITY_SENSOR 0x0542 +#define BLE_APPEARANCE_TEMPERATURE_SENSOR 0x0543 +#define BLE_APPEARANCE_HUMIDITY_SENSOR 0x0544 +#define BLE_APPEARANCE_LEAK_SENSOR 0x05 +#define BLE_APPEARANCE_SMOKE_SENSOR 0x0546 +#define BLE_APPEARANCE_OCCUPANCY_SENSOR 0x0547 +#define BLE_APPEARANCE_CONTACT_SENSOR 0x0548 +#define BLE_APPEARANCE_CARBON_MONOXIDE_SENSOR 0x0549 +#define BLE_APPEARANCE_CARBON_DIOXIDE_SENSOR 0x054A +#define BLE_APPEARANCE_AMBIENT_LIGHT_SENSOR 0x054B +#define BLE_APPEARANCE_ENERGY_SENSOR 0x054C +#define BLE_APPEARANCE_COLOR_LIGHT_SENSOR 0x054D +#define BLE_APPEARANCE_RAIN_SENSOR 0x054E +#define BLE_APPEARANCE_FIRE_SENSOR 0x054F +#define BLE_APPEARANCE_WIND_SENSOR 0x0550 +#define BLE_APPEARANCE_PROXIMITY_SENSOR 0x0551 +#define BLE_APPEARANCE_MULTI_SENSOR 0x0552 +#define BLE_APPEARANCE_FLUSH_MOUNTED_SENSOR 0x0553 +#define BLE_APPEARANCE_CEILING_MOUNTED_SENSOR 0x0554 +#define BLE_APPEARANCE_WALL_MOUNTED_SENSOR 0x0555 +#define BLE_APPEARANCE_MULTISENSOR 0x0556 +#define BLE_APPEARANCE_SENSOR_ENERGY_METER 0x0557 +#define BLE_APPEARANCE_SENSOR_FLAME_DETECTOR 0x0558 +#define BLE_APPEARANCE_VEHICLE_TIRE_PRESSURE_SENSOR 0x0559 + +/* Category[15:6] 0x016 */ +#define BLE_APPEARANCE_GENERIC_LIGHT_FIXTURE 0x0580 +#define BLE_APPEARANCE_WALL_LIGHT 0x0581 +#define BLE_APPEARANCE_CEILING_LIGHT 0x0582 +#define BLE_APPEARANCE_FLOOR_LIGHT 0x0583 +#define BLE_APPEARANCE_CABINET_LIGHT 0x0584 +#define BLE_APPEARANCE_DESK_LIGHT 0x0585 +#define BLE_APPEARANCE_TROFFER_LIGHT 0x0586 +#define BLE_APPEARANCE_PENDANT_LIGHT 0x0587 +#define BLE_APPEARANCE_IN_GROUND_LIGHT 0x0588 +#define BLE_APPEARANCE_FLOOD_LIGHT 0x0589 +#define BLE_APPEARANCE_UNDERWATER_LIGHT 0x058A +#define BLE_APPEARANCE_BOLLARD_WITH_LIGHT 0x058B +#define BLE_APPEARANCE_PATHWAY_LIGHT 0x058C +#define BLE_APPEARANCE_GARDEN_LIGHT 0x058D +#define BLE_APPEARANCE_POLE_TOP_LIGHT 0x058E +#define BLE_APPEARANCE_SPOTLIGHT 0x058F +#define BLE_APPEARANCE_LINEAR_LIGHT 0x0590 +#define BLE_APPEARANCE_STREET_LIGHT 0x0591 +#define BLE_APPEARANCE_SHELVES_LIGHT 0x0592 +#define BLE_APPEARANCE_BAY_LIGHT 0x0593 +#define BLE_APPEARANCE_EMERGENCY_EXIT_LIGHT 0x0594 +#define BLE_APPEARANCE_LIGHT_CONTROLLER 0x0595 +#define BLE_APPEARANCE_LIGHT_DRIVER 0x0596 +#define BLE_APPEARANCE_BULB 0x0597 +#define BLE_APPEARANCE_LOW_BAY_LIGHT 0x0598 +#define BLE_APPEARANCE_HIGH_BAY_LIGHT 0x0599 + +/* Category[15:6] 0x017 */ +#define BLE_APPEARANCE_GENERIC_FAN 0x05C0 +#define BLE_APPEARANCE_CEILING_FAN 0x05C1 +#define BLE_APPEARANCE_AXIAL_FAN 0x05C2 +#define BLE_APPEARANCE_EXHAUST_FAN 0x05C3 +#define BLE_APPEARANCE_PEDESTAL_FAN 0x05C4 +#define BLE_APPEARANCE_DESK_FAN 0x05C5 +#define BLE_APPEARANCE_WALL_FAN 0x05C6 + +/* Category[15:6] 0x018 */ +#define BLE_APPEARANCE_GENERIC_HVAC 0x0600 +#define BLE_APPEARANCE_HVAC_THERMOSTAT 0x0601 +#define BLE_APPEARANCE_HVAC_HUMIDIFIER 0x0602 +#define BLE_APPEARANCE_HVAC_DEHUMIDIFIER 0x0603 +#define BLE_APPEARANCE_HVAC_HEATER 0x0604 +#define BLE_APPEARANCE_HVAC_RADIATOR 0x0605 +#define BLE_APPEARANCE_HVAC_BOILER 0x0606 +#define BLE_APPEARANCE_HVAC_HEAT_PUMP 0x0607 +#define BLE_APPEARANCE_HVAC_INFRARED_HEATER 0x0608 +#define BLE_APPEARANCE_HVAC_RADIANT_PANEL_HEATER 0x0609 +#define BLE_APPEARANCE_HVAC_FAN_HEATER 0x060A +#define BLE_APPEARANCE_HVAC_AIR_CURTAIN 0x060B + +/* Category[15:6] 0x019 */ +#define BLE_APPEARANCE_GENERIC_AIR_CONDITIONING 0x0640 + +/* Category[15:6] 0x01A */ +#define BLE_APPEARANCE_GENERIC_HUMIDIFIER 0x0680 + +/* Category[15:6] 0x01B */ +#define BLE_APPEARANCE_GENERIC_HEATING 0x06C0 +#define BLE_APPEARANCE_HEATING_RADIATOR 0x06C1 +#define BLE_APPEARANCE_HEATING_BOILER 0x06C2 +#define BLE_APPEARANCE_HEATING_HEAT_PUMP 0x06C3 +#define BLE_APPEARANCE_HEATING_INFRARED_HEATER 0x06C4 +#define BLE_APPEARANCE_HEATING_RADIANT_PANEL_HEATER 0x06C5 +#define BLE_APPEARANCE_HEATING_FAN_HEATER 0x06C6 +#define BLE_APPEARANCE_HEATING_AIR_CURTAIN 0x06C7 + +/* Category[15:6] 0x01C */ +#define BLE_APPEARANCE_GENERIC_ACCESS_CONTROL 0x0700 +#define BLE_APPEARANCE_ACCESS_DOOR 0x0701 +#define BLE_APPEARANCE_ACCESS_CONTROL_GARAGE_DOOR 0x0702 +#define BLE_APPEARANCE_ACCESS_CONTROL_EMERGENCY_EXIT_DOOR 0x0703 +#define BLE_APPEARANCE_ACCESS_CONTROL_ACCESS_LOCK 0x0704 +#define BLE_APPEARANCE_ACCESS_CONTROL_ELEVATOR 0x0705 +#define BLE_APPEARANCE_ACCESS_CONTROL_WINDOW 0x0706 +#define BLE_APPEARANCE_ACCESS_CONTROL_ENTRANCE_GATE 0x0707 +#define BLE_APPEARANCE_ACCESS_CONTROL_DOOR_LOCK 0x0708 +#define BLE_APPEARANCE_ACCESS_CONTROL_LOCKER 0x0709 + +/* Category[15:6] 0x01D */ +#define BLE_APPEARANCE_GENERIC_MOTORIZED_DEVICE 0x0740 +#define BLE_APPEARANCE_MOTORIZED_GATE 0x0741 +#define BLE_APPEARANCE_MOTORIZED_AWNING 0x0742 +#define BLE_APPEARANCE_MOTORIZED_BLINDS_OR_SHADES 0x0743 +#define BLE_APPEARANCE_MOTORIZED_CURTAINS 0x0744 +#define BLE_APPEARANCE_MOTORIZED_SCREEN 0x0745 + +/* Category[15:6] 0x01E */ +#define BLE_APPEARANCE_GENERIC_POWER_DEVICE 0x0780 +#define BLE_APPEARANCE_POWER_OUTLET 0x0781 +#define BLE_APPEARANCE_POWER_STRIP 0x0782 +#define BLE_APPEARANCE_POWER_PLUG 0x0783 +#define BLE_APPEARANCE_POWER_SUPPLY 0x0784 + +/* Category[15:6] 0x01F */ +#define BLE_APPEARANCE_GENERIC_LIGHT_SOURCE 0x07C0 +#define BLE_APPEARANCE_LIGHT_SOURCE_INCANDESCENT_LIGHT_BULB 0x07C1 +#define BLE_APPEARANCE_LIGHT_SOURCE_LED_LAMP 0x07C2 +#define BLE_APPEARANCE_LIGHT_SOURCE_HID_LAMP 0x07C3 +#define BLE_APPEARANCE_LIGHT_SOURCE_FLUORESCENT_LAMP 0x07C4 +#define BLE_APPEARANCE_LIGHT_SOURCE_LED_ARRAY 0x07C5 +#define BLE_APPEARANCE_LIGHT_SOURCE_MULTI_COLOR_LED_ARRAY 0x07C6 +#define BLE_APPEARANCE_LIGHT_SOURCE_LOW_VOLTAGE_HALOGEN 0x07C7 +#define BLE_APPEARANCE_LIGHT_SOURCE_ORGANIC_LIGHT_EMITTING_DIODE_OLED 0x07C8 + +/* Category[15:6] 0x020 */ +#define BLE_APPEARANCE_GENERIC_WINDOW_COVERING 0x0800 +#define BLE_APPEARANCE_WINDOW_COVERING_WINDOW_SHADES 0x0801 +#define BLE_APPEARANCE_WINDOW_COVERING_WINDOW_BLINDS 0x0802 +#define BLE_APPEARANCE_WINDOW_COVERING_WINDOW_AWNING 0x0803 +#define BLE_APPEARANCE_WINDOW_COVERING_WINDOW_CURTAIN 0x0804 +#define BLE_APPEARANCE_WINDOW_COVERING_EXTERIOR_SHUTTER 0x0805 +#define BLE_APPEARANCE_WINDOW_COVERING_EXTERIOR_SCREEN 0x0806 + +/* Category[15:6] 0x021 */ +#define BLE_APPEARANCE_GENERIC_AUDIO_SINK 0x0840 +#define BLE_APPEARANCE_AUDIO_SINK_STANDALONE_SPEAKER 0x0841 +#define BLE_APPEARANCE_AUDIO_SINK_SOUNDBAR 0x0842 +#define BLE_APPEARANCE_AUDIO_SINK_BOOKSHELF_SPEAKER 0x0843 +#define BLE_APPEARANCE_AUDIO_SINK_STANDMOUNTED_SPEAKER 0x0844 +#define BLE_APPEARANCE_AUDIO_SINK_SPEAKERPHONE 0x0845 + +/* Category[15:6] 0x022 */ +#define BLE_APPEARANCE_GENERIC_AUDIO_SOURCE 0x0880 +#define BLE_APPEARANCE_AUDIO_SOURCE_MICROPHONE 0x0881 +#define BLE_APPEARANCE_AUDIO_SOURCE_ALARM 0x0882 +#define BLE_APPEARANCE_AUDIO_SOURCE_BELL 0x0883 +#define BLE_APPEARANCE_AUDIO_SOURCE_HORN 0x0884 +#define BLE_APPEARANCE_AUDIO_SOURCE_BROADCASTING_DEVICE 0x0885 +#define BLE_APPEARANCE_AUDIO_SOURCE_SERVICE_DESK 0x0886 +#define BLE_APPEARANCE_AUDIO_SOURCE_KIOSK 0x0887 +#define BLE_APPEARANCE_AUDIO_SOURCE_BROADCASTING_ROOM 0x0888 +#define BLE_APPEARANCE_AUDIO_SOURCE_AUDITORIUM 0x0889 + +/* Category[15:6] 0x023 */ +#define BLE_APPEARANCE_GENERIC_MOTORIZED_VEHICLE 0x08C0 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_CAR 0x08C1 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_LARGE_GOODS 0x08C2 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_2_WHEELED 0x08C3 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_MOTORBIKE 0x08C4 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_SCOOTER 0x08C5 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_MOPED 0x08C6 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_3_WHEELED 0x08C7 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_LIGHT_VEHICLE 0x08C8 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_QUAD_BIKE 0x08C9 +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_MINIBUS 0x08CA +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_BUS 0x08CB +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_TROLLEY 0x08CC +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_AGRICULTURAL_VEHICLE 0x08CD +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_CAMPER_CARAVAN 0x08CE +#define BLE_APPEARANCE_MOTORIZED_VEHICLE_RECREATIONAL_VEHICLE_MOTOR_HOME 0x08CF + +/* Category[15:6] 0x024 */ +#define BLE_APPEARANCE_GENERIC_DOMESTIC_APPLIANCE 0x0900 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_REFRIGERATOR 0x0901 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_FREEZER 0x0902 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_OVEN 0x0903 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_MICROWAVE 0x0904 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_TOASTER 0x0905 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_WASHING_MACHINE 0x0906 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_DRYER 0x0907 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_COFFEE_MAKER 0x0908 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_CLOTHES_IRON 0x0909 +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_CURLING_IRON 0x090A +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_HAIR_DRYER 0x090B +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_VACUUM_CLEANER 0x090C +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_ROBOTIC_VACUUM_CLEANER 0x090D +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_RICE_COOKER 0x090E +#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_CLOTHES_STEAMER 0x090F + +/* Category[15:6] 0x025 */ +#define BLE_APPEARANCE_GENERIC_WEARABLE_AUDIO_DEVICE 0x0940 +#define BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_EARBUD 0x0941 +#define BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADSET 0x0942 +#define BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADPHONES 0x0943 +#define BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_NECK_BAND 0x0944 + +/* Category[15:6] 0x026 */ +#define BLE_APPEARANCE_GENERIC_AIRCRAFT 0x0980 +#define BLE_APPEARANCE_AIRCRAFT_LIGHT 0x0981 +#define BLE_APPEARANCE_AIRCRAFT_MICROLIGHT 0x0982 +#define BLE_APPEARANCE_AIRCRAFT_PARAGLIDER 0x0983 +#define BLE_APPEARANCE_AIRCRAFT_LARGE_PASSENGER 0x0984 + +/* Category[15:6] 0x027 */ +#define BLE_APPEARANCE_GENERIC_AV_EQUIPMENT 0x09C0 +#define BLE_APPEARANCE_AV_EQUIPMENT_AMPLIFIER 0x09C1 +#define BLE_APPEARANCE_AV_EQUIPMENT_RECEIVER 0x09C2 +#define BLE_APPEARANCE_AV_EQUIPMENT_RADIO 0x09C3 +#define BLE_APPEARANCE_AV_EQUIPMENT_TUNER 0x09C4 +#define BLE_APPEARANCE_AV_EQUIPMENT_TURNTABLE 0x09C5 +#define BLE_APPEARANCE_AV_EQUIPMENT_CD_PLAYER 0x09C6 +#define BLE_APPEARANCE_AV_EQUIPMENT_DVD_PLAYER 0x09C7 +#define BLE_APPEARANCE_AV_EQUIPMENT_BLU_RAY_PLAYER 0x09C8 +#define BLE_APPEARANCE_AV_EQUIPMENT_OPTICAL_DISC_PLAYER 0x09C9 +#define BLE_APPEARANCE_AV_EQUIPMENT_SET_TOP_BOX 0x09CA + +/* Category[15:6] 0x028 */ +#define BLE_APPEARANCE_GENERIC_DISPLAY_EQUIPMENT 0x0A00 +#define BLE_APPEARANCE_DISPLAY_EQUIPMENT_TELEVISION 0x0A01 +#define BLE_APPEARANCE_DISPLAY_EQUIPMENT_MONITOR 0x0A02 +#define BLE_APPEARANCE_DISPLAY_EQUIPMENT_PROJECTOR 0x0A03 + +/* Category[15:6] 0x029 */ +#define BLE_APPEARANCE_GENERIC_HEARING_AID 0x0A40 +#define BLE_APPEARANCE_HEARING_AID_IN_EAR 0x0A41 +#define BLE_APPEARANCE_HEARING_AID_BEHIND_EAR 0x0A42 +#define BLE_APPEARANCE_HEARING_AID_COCHLLEAR_IMPLANT 0x0A43 + +/* Category[15:6] 0x02A */ +#define BLE_APPEARANCE_GENERIC_GAMING 0x0A80 +#define BLE_APPEARANCE_GAMING_HOME_VIDEO_GAME_CONSOLE 0x0A81 +#define BLE_APPEARANCE_GAMING_PORTABLE_HANDHELD_CONSOLE 0x0A82 + +/* Category[15:6] 0x02B */ +#define BLE_APPEARANCE_GENERIC_SIGNAGE 0x0AC0 +#define BLE_APPEARANCE_SIGNAGE_DIGITAL 0x0AC1 +#define BLE_APPEARANCE_SIGNAGE_ELECTRONIC_LABEL 0x0AC2 + +/* Category[15:6] 0x031 */ +#define BLE_APPEARANCE_GENERIC_PULSE_OXIMETER 0x0C40 +#define BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP 0x0C41 +#define BLE_APPEARANCE_PULSE_OXIMETER_WRIST 0x0C42 + +/* Category[15:6] 0x032 */ +#define BLE_APPEARANCE_GENERIC_WEIGHT 0x0C80 + +/* Category[15:6] 0x033 */ +#define BLE_APPEARANCE_GENERIC_PERSONAL_MOBILITY_DEVICE 0x0CC0 +#define BLE_APPEARANCE_PERSONAL_MOBILITY_DEVICE_POWERED_WHEELCHAIR 0x0CC1 +#define BLE_APPEARANCE_PERSONAL_MOBILITY_DEVICE_MOBILITY_SCOOTER 0x0CC2 + +/* Category[15:6] 0x034 */ +#define BLE_APPEARANCE_GENERIC_CONTINUOUS_GLUCOSE_MONITOR 0x0D00 + +/* Category[15:6] 0x035 */ +#define BLE_APPEARANCE_GENERIC_INSULIN_PUMP 0x0D40 +#define BLE_APPEARANCE_INSULIN_PUMP_DURABLE 0x0D41 +#define BLE_APPEARANCE_INSULIN_PUMP_PATCH 0x0D44 +#define BLE_APPEARANCE_INSULIN_PUMP_PEN 0x0D48 + +/* Category[15:6] 0x036 */ +#define BLE_APPEARANCE_GENERIC_MEDICATION_DELIVERY 0x0D80 + +/* Category[15:6] 0x037 */ +#define BLE_APPEARANCE_GENERIC_SPIROMETER 0x0DC0 +#define BLE_APPEARANCE_SPIROMETER_HANDHELD 0x0DC1 + +/* Category[15:6] 0x051 */ +#define BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS 0x1440 +#define BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION 0x1441 +#define BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_AND_NAV 0x1442 +#define BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD 0x1443 +#define BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD_AND_NAV 0x1444 + +/* Category[15:6] 0x052 */ +#define BLE_APPEARANCE_GENERIC_INDUSTRIAL_MEASUREMENT_DEVICE 0x1480 +#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_TORQUE_TESTING 0x1481 +#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_CALIPER 0x1482 +#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_DIAL_INDICATOR 0x1483 +#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_MICROMETER 0x1484 +#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_HEIGHT_GAUGE 0x1485 +#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_FORCE_GAUGE 0x1486 + +/* Category[15:6] 0x053 */ +#define BLE_APPEARANCE_GENERIC_INDUSTRIAL_TOOLS 0x14C0 +#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_MACHINE_TOOL_HOLDER 0x14C1 +#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_GENERIC_CLAMPING_DEVICE 0x14C2 +#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_CLAMPING_JAWS_JAWS_CHUCK 0x14C3 +#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_CLAMPING_COLLET_CHUCK 0x14C4 +#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_CLAMPING_MANDREL 0x14C5 +#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_VISE 0x14C6 +#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_ZERO_POINT_CLAMPING_SYSTEM 0x14C7 +#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_TORQUE_WRENCH 0x14C8 +#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_TORQUE_SCREWDRIVER 0x14C9 + +#endif // BLE_APPEARANCE_H
\ No newline at end of file diff --git a/system/stack/include/bt_dev_class.h b/system/stack/include/bt_dev_class.h index 1455f66fd2..ca951532ce 100644 --- a/system/stack/include/bt_dev_class.h +++ b/system/stack/include/bt_dev_class.h @@ -26,72 +26,222 @@ typedef std::array<uint8_t, kDevClassLength> DEV_CLASS; /* Device class */ inline constexpr DEV_CLASS kDevClassEmpty = {}; +/*************************** + * major device class field + * Note: All values are deduced by basing BIT_X to BIT_8, values as per + * BT-spec assigned-numbers. + ***************************/ +#define COD_MAJOR_MISC 0x00 +#define COD_MAJOR_COMPUTER 0x01 // BIT8 +#define COD_MAJOR_PHONE 0x02 // BIT9 +#define COD_MAJOR_LAN_NAP 0x03 // BIT8 | BIT9 +#define COD_MAJOR_AUDIO 0x04 // BIT10 +#define COD_MAJOR_PERIPHERAL 0x05 // BIT8 | BIT10 +#define COD_MAJOR_IMAGING 0x06 // BIT9 | BIT10 +#define COD_MAJOR_WEARABLE 0x07 // BIT8 | BIT9 | BIT10 +#define COD_MAJOR_TOY 0x08 // BIT11 +#define COD_MAJOR_HEALTH 0x09 // BIT8 | BIT11 +#define COD_MAJOR_UNCLASSIFIED 0x1F // BIT8 | BIT9 | BIT10 | BIT11 | BIT12 + +/*************************** + * service class fields + * Note: All values are deduced by basing BIT_X to BIT_8, values as per + * BT-spec assigned-numbers. + ***************************/ +#define COD_SERVICE_LMTD_DISCOVER 0x0020 // BIT13 (eg. 13-8 = BIT5 = 0x0020) +#define COD_SERVICE_LE_AUDIO 0x0040 // BIT14 +#define COD_SERVICE_POSITIONING 0x0100 // BIT16 +#define COD_SERVICE_NETWORKING 0x0200 // BIT17 +#define COD_SERVICE_RENDERING 0x0400 // BIT18 +#define COD_SERVICE_CAPTURING 0x0800 // BIT19 +#define COD_SERVICE_OBJ_TRANSFER 0x1000 // BIT20 +#define COD_SERVICE_AUDIO 0x2000 // BIT21 +#define COD_SERVICE_TELEPHONY 0x4000 // BIT22 +#define COD_SERVICE_INFORMATION 0x8000 // BIT23 + +/*************************** + * minor device class field + * Note: LSB[1:0] (2 bits) is don't care for minor device class. + ***************************/ +/* Minor Device class field - Computer Major Class (COD_MAJOR_COMPUTER) */ +#define COD_MAJOR_COMPUTER_MINOR_UNCATEGORIZED 0x00 +#define COD_MAJOR_COMPUTER_MINOR_DESKTOP_WORKSTATION 0x04 // BIT2 +#define COD_MAJOR_COMPUTER_MINOR_SERVER_CLASS_COMPUTER 0x08 // BIT3 +#define COD_MAJOR_COMPUTER_MINOR_LAPTOP 0x0C // BIT2 | BIT3 +#define COD_MAJOR_COMPUTER_MINOR_HANDHELD_PC_PDA 0x10 // BIT4 +#define COD_MAJOR_COMPUTER_MINOR_PALM_SIZE_PC_PDA 0x14 // BIT2 | BIT4 +#define COD_MAJOR_COMPUTER_MINOR_WEARABLE_COMPUTER_WATCH_SIZE 0x18 // BIT3 | BIT4 +#define COD_MAJOR_COMPUTER_MINOR_TABLET 0x1C // BIT2 | BIT3 | BIT4 + +/* Minor Device class field - Phone Major Class (COD_MAJOR_PHONE) */ +#define COD_MAJOR_PHONE_MINOR_UNCATEGORIZED 0x00 +#define COD_MAJOR_PHONE_MINOR_CELLULAR 0x04 // BIT2 +#define COD_MAJOR_PHONE_MINOR_CORDLESS 0x08 // BIT3 +#define COD_MAJOR_PHONE_MINOR_SMARTPHONE 0x0C // BIT2 | BIT3 +#define COD_MAJOR_PHONE_MINOR_WIRED_MODEM_OR_VOICE_GATEWAY 0x10 // BIT4 +#define COD_MAJOR_PHONE_MINOR_COMMON_ISDN_ACCESS 0x14 // BIT2 | BIT4 + +/* + * Minor Device class field - + * LAN/Network Access Point Major Class (COD_MAJOR_LAN_NAP) + */ +#define COD_MAJOR_LAN_NAP_MINOR_FULLY_AVAILABLE 0x00 +#define COD_MAJOR_LAN_NAP_MINOR_1_TO_17_PER_UTILIZED 0x20 // BIT5 +#define COD_MAJOR_LAN_NAP_MINOR_17_TO_33_PER_UTILIZED 0x40 // BIT6 +#define COD_MAJOR_LAN_NAP_MINOR_33_TO_50_PER_UTILIZED 0x60 // BIT5 | BIT6 +#define COD_MAJOR_LAN_NAP_MINOR_50_TO_67_PER_UTILIZED 0x80 // BIT7 +#define COD_MAJOR_LAN_NAP_MINOR_67_TO_83_PER_UTILIZED 0xA0 // BIT5 | BIT7 +#define COD_MAJOR_LAN_NAP_MINOR_83_TO_99_PER_UTILIZED 0xC0 // BIT6 | BIT7 +#define COD_MAJOR_LAN_NAP_MINOR_NO_SERVICE_AVAILABLE 0xE0 // BIT5 | BIT6 | BIT7 + +/* Minor Device class field - Audio/Video Major Class (COD_MAJOR_AUDIO) */ +/* 0x00 is used as unclassified for all minor device classes */ +#define COD_MINOR_UNCATEGORIZED 0x00 +#define COD_MAJOR_AUDIO_MINOR_WEARABLE_HEADSET 0x04 // BIT2 +#define COD_MAJOR_AUDIO_MINOR_CONFM_HANDSFREE 0x08 // BIT3 +#define COD_MAJOR_AUDIO_MINOR_MICROPHONE 0x10 // BIT4 +#define COD_MAJOR_AUDIO_MINOR_LOUDSPEAKER 0x14 // BIT2 | BIT4 +#define COD_MAJOR_AUDIO_MINOR_HEADPHONES 0x18 // BIT3 | BIT4 +#define COD_MAJOR_AUDIO_MINOR_PORTABLE_AUDIO 0x1C // BIT2 | BIT3 | BIT4 +#define COD_MAJOR_AUDIO_MINOR_CAR_AUDIO 0x20 // BIT5 +#define COD_MAJOR_AUDIO_MINOR_SET_TOP_BOX 0x24 // BIT2 | BIT5 +#define COD_MAJOR_AUDIO_MINOR_HIFI_AUDIO 0x28 // BIT3 | BIT5 +#define COD_MAJOR_AUDIO_MINOR_VCR 0x2C // BIT2 | BIT3 | BIT5 +#define COD_MAJOR_AUDIO_MINOR_VIDEO_CAMERA 0x30 // BIT4 | BIT5 +#define COD_MAJOR_AUDIO_MINOR_CAMCORDER 0x34 // BIT2 | BIT4 | BIT5 +#define COD_MAJOR_AUDIO_MINOR_VIDEO_MONITOR 0x38 // BIT3 | BIT4 | BIT5 +#define COD_MAJOR_AUDIO_MINOR_VIDEO_DISPLAY_AND_LOUDSPEAKER 0x3C // BIT2 | + // BIT3 | BIT4 | BIT5 +#define COD_MAJOR_AUDIO_MINOR_VIDEO_CONFERENCING 0x40 // BIT6 +#define COD_MAJOR_AUDIO_MINOR_GAMING_OR_TOY 0x48 // BIT3 | BIT6 + +/* Minor Device class field - Peripheral Major Class (COD_MAJOR_PERIPHERAL) */ +/* Bits 6-7 independently specify mouse, keyboard, or combo mouse/keyboard */ +#define COD_MAJOR_PERIPH_MINOR_KEYBOARD 0x40 // BIT6 +#define COD_MAJOR_PERIPH_MINOR_POINTING 0x80 // BIT7 +#define COD_MAJOR_PERIPH_MINOR_KEYBOARD_AND_POINTING_DEVICE 0xC0 // BIT6 | BIT7 + +/* Bits 2-5 OR'd with selection from bits 6-7 */ +#define COD_MAJOR_PERIPH_MINOR_JOYSTICK 0x04 // BIT2 +#define COD_MAJOR_PERIPH_MINOR_GAMEPAD 0x08 // BIT3 +#define COD_MAJOR_PERIPH_MINOR_REMOTE_CONTROL 0x0C // BIT2 | BIT3 +#define COD_MAJOR_PERIPH_MINOR_SENSING_DEVICE 0x10 // BIT4 +#define COD_MAJOR_PERIPH_MINOR_DIGITIZING_TABLET 0x14 // BIT2 | BIT4 +#define COD_MAJOR_PERIPH_MINOR_CARD_READER 0x18 /* e.g. SIM card reader, BIT3 | BIT4 */ +#define COD_MAJOR_PERIPH_MINOR_DIGITAL_PEN 0x1C // Pen, BIT2 | BIT3 | BIT4 +#define COD_MAJOR_PERIPH_MINOR_HANDHELD_SCANNER 0x20 // e.g. Barcode, RFID, BIT5 +#define COD_MAJOR_PERIPH_MINOR_HANDHELD_GESTURAL_INP_DEVICE 0x24 + // e.g. "wand" form factor, BIT2 | BIT5 + +/* Minor Device class field - Imaging Major Class (COD_MAJOR_IMAGING) + * + * Bits 5-7 independently specify display, camera, scanner, or printer + * Note: Apart from the set bit, all other bits are don't care. + */ +#define COD_MAJOR_IMAGING_MINOR_DISPLAY 0x10 // BIT4 +#define COD_MAJOR_IMAGING_MINOR_CAMERA 0x20 // BIT5 +#define COD_MAJOR_IMAGING_MINOR_SCANNER 0x40 // BIT6 +#define COD_MAJOR_IMAGING_MINOR_PRINTER 0x80 // BIT7 + +/* Minor Device class field - Wearable Major Class (COD_MAJOR_WEARABLE) */ +#define COD_MAJOR_WEARABLE_MINOR_WRIST_WATCH 0x04 // BIT2 +#define COD_MAJOR_WEARABLE_MINOR_PAGER 0x08 // BIT3 +#define COD_MJAOR_WEARABLE_MINOR_JACKET 0x0C // BIT2 | BIT3 +#define COD_MAJOR_WEARABLE_MINOR_HELMET 0x10 // BIT4 +#define COD_MAJOR_WEARABLE_MINOR_GLASSES 0x14 // BIT2 | BIT4 +#define COD_MAJOR_WEARABLE_MINOR_PIN 0x18 + // e.g. Label pin, broach, badge BIT3 | BIT4 + +/* Minor Device class field - Toy Major Class (COD_MAJOR_TOY) */ +#define COD_MAJOR_TOY_MINOR_ROBOT 0x04 // BIT2 +#define COD_MAJOR_TOY_MINOR_VEHICLE 0x08 // BIT3 +#define COD_MAJOR_TOY_MINOR_DOLL_OR_ACTION_FIGURE 0x0C // BIT2 | BIT3 +#define COD_MAJOR_TOY_MINOR_CONTROLLER 0x10 // BIT4 +#define COD_MAJOR_TOY_MINOR_GAME 0x14 // BIT2 | BIT4 + +/* Minor Device class field - Health Major Class (COD_MAJOR_HEALTH) */ +#define COD_MAJOR_HEALTH_MINOR_BLOOD_MONITOR 0x04 // Blood pressure monitor, BIT2 +#define COD_MAJOR_HEALTH_MINOR_THERMOMETER 0x08 // BIT3 +#define COD_MAJOR_HEALTH_MINOR_WEIGHING_SCALE 0x0C // BIT2 | BIT3 +#define COD_MAJOR_HEALTH_MINOR_GLUCOSE_METER 0x10 // BIT4 +#define COD_MAJOR_HEALTH_MINOR_PULSE_OXIMETER 0x14 // BIT2 | BIT4 +#define COD_MAJOR_HEALTH_MINOR_HEART_PULSE_MONITOR 0x18 // BIT3 | BIT4 +#define COD_MAJOR_HEALTH_MINOR_HEALTH_DATA_DISPLAY 0x1C // BIT2 | BIT3 | BIT4 +#define COD_MAJO_HEALTH_MINOR_STEP_COUNTER 0x20 // BIT5 +#define COD_MAJOR_HEALTH_MINOR_BODY_COMPOSITION_ANALYZER 0x24 // BIT2 | BIT5 +#define COD_MAJOR_HEALTH_MINOR_PEAK_FLOW_MONITOR 0x28 // BIT3 | BIT5 +#define COD_MAJOR_HEALTH_MINOR_MEDICATION_MONITOR 0x2C // BIT2 | BIT3 | BIT5 +#define COD_MAJOR_HEALTH_MINOR_KNEE_PROSTHESIS 0x30 // BIT4 | BIT5 +#define COD_MAJOR_HEALTH_MINOR_ANKLE_PROSTHESIS 0x34 // BIT3 | BIT4 | BIT5 +#define COD_MAJOR_HEALTH_MINOR_GENERIC_HEALTH_MANAGER 0x38 // BIT2 | BIT3 | BIT4 | BIT5 +#define COD_MAJOR_HEALTH_MINOR_PERSONAL_MOBILITY_DEVICE 0x3C // BIT4 | BIT5 + /* 0x00 is used as unclassified for all minor device classes */ -#define BTM_COD_MINOR_UNCLASSIFIED 0x00 -#define BTM_COD_MINOR_WEARABLE_HEADSET 0x04 -#define BTM_COD_MINOR_CONFM_HANDSFREE 0x08 -#define BTM_COD_MINOR_CAR_AUDIO 0x20 -#define BTM_COD_MINOR_SET_TOP_BOX 0x24 +#define BTM_COD_MINOR_UNCLASSIFIED COD_MINOR_UNCATEGORIZED +#define BTM_COD_MINOR_WEARABLE_HEADSET COD_MAJOR_AUDIO_MINOR_WEARABLE_HEADSET +#define BTM_COD_MINOR_CONFM_HANDSFREE COD_MAJOR_AUDIO_MINOR_CONFM_HANDSFREE +#define BTM_COD_MINOR_CAR_AUDIO COD_MAJOR_AUDIO_MINOR_CAR_AUDIO +#define BTM_COD_MINOR_SET_TOP_BOX COD_MAJOR_AUDIO_MINOR_SET_TOP_BOX /* minor device class field for Peripheral Major Class */ /* Bits 6-7 independently specify mouse, keyboard, or combo mouse/keyboard */ -#define BTM_COD_MINOR_KEYBOARD 0x40 -#define BTM_COD_MINOR_POINTING 0x80 +#define BTM_COD_MINOR_KEYBOARD COD_MAJOR_PERIPH_MINOR_KEYBOARD +#define BTM_COD_MINOR_POINTING COD_MAJOR_PERIPH_MINOR_POINTING /* Bits 2-5 OR'd with selection from bits 6-7 */ /* #define BTM_COD_MINOR_UNCLASSIFIED 0x00 */ -#define BTM_COD_MINOR_JOYSTICK 0x04 -#define BTM_COD_MINOR_GAMEPAD 0x08 -#define BTM_COD_MINOR_REMOTE_CONTROL 0x0C -#define BTM_COD_MINOR_DIGITIZING_TABLET 0x14 -#define BTM_COD_MINOR_CARD_READER 0x18 /* e.g. SIM card reader */ -#define BTM_COD_MINOR_DIGITAL_PAN 0x1C +#define BTM_COD_MINOR_JOYSTICK COD_MAJOR_PERIPH_MINOR_JOYSTICK +#define BTM_COD_MINOR_GAMEPAD COD_MAJOR_PERIPH_MINOR_GAMEPAD +#define BTM_COD_MINOR_REMOTE_CONTROL COD_MAJOR_PERIPH_MINOR_REMOTE_CONTROL +#define BTM_COD_MINOR_DIGITIZING_TABLET COD_MAJOR_PERIPH_MINOR_DIGITIZING_TABLET +#define BTM_COD_MINOR_CARD_READER COD_MAJOR_PERIPH_MINOR_CARD_READER +#define BTM_COD_MINOR_DIGITAL_PAN COD_MAJOR_PERIPH_MINOR_DIGITAL_PEN /* minor device class field for Imaging Major Class */ /* Bits 5-7 independently specify display, camera, scanner, or printer */ -#define BTM_COD_MINOR_DISPLAY 0x10 +#define BTM_COD_MINOR_DISPLAY COD_MAJOR_IMAGING_MINOR_DISPLAY /* Bits 2-3 Reserved */ /* #define BTM_COD_MINOR_UNCLASSIFIED 0x00 */ /* minor device class field for Wearable Major Class */ /* Bits 2-7 meaningful */ -#define BTM_COD_MINOR_WRIST_WATCH 0x04 -#define BTM_COD_MINOR_GLASSES 0x14 +#define BTM_COD_MINOR_WRIST_WATCH COD_MAJOR_WEARABLE_MINOR_WRIST_WATCH +#define BTM_COD_MINOR_GLASSES COD_MAJOR_WEARABLE_MINOR_GLASSES /* minor device class field for Health Major Class */ /* Bits 2-7 meaningful */ -#define BTM_COD_MINOR_BLOOD_MONITOR 0x04 -#define BTM_COD_MINOR_THERMOMETER 0x08 -#define BTM_COD_MINOR_WEIGHING_SCALE 0x0C -#define BTM_COD_MINOR_GLUCOSE_METER 0x10 -#define BTM_COD_MINOR_PULSE_OXIMETER 0x14 -#define BTM_COD_MINOR_HEART_PULSE_MONITOR 0x18 -#define BTM_COD_MINOR_STEP_COUNTER 0x20 +#define BTM_COD_MINOR_BLOOD_MONITOR COD_MAJOR_HEALTH_MINOR_BLOOD_MONITOR +#define BTM_COD_MINOR_THERMOMETER COD_MAJOR_HEALTH_MINOR_THERMOMETER +#define BTM_COD_MINOR_WEIGHING_SCALE COD_MAJOR_HEALTH_MINOR_WEIGHING_SCALE +#define BTM_COD_MINOR_GLUCOSE_METER COD_MAJOR_HEALTH_MINOR_GLUCOSE_METER +#define BTM_COD_MINOR_PULSE_OXIMETER COD_MAJOR_HEALTH_MINOR_PULSE_OXIMETER +#define BTM_COD_MINOR_HEART_PULSE_MONITOR COD_MAJOR_HEALTH_MINOR_HEART_PULSE_MONITOR +#define BTM_COD_MINOR_STEP_COUNTER COD_MAJO_HEALTH_MINOR_STEP_COUNTER /*************************** * major device class field ***************************/ -#define BTM_COD_MAJOR_COMPUTER 0x01 -#define BTM_COD_MAJOR_PHONE 0x02 -#define BTM_COD_MAJOR_AUDIO 0x04 -#define BTM_COD_MAJOR_PERIPHERAL 0x05 -#define BTM_COD_MAJOR_IMAGING 0x06 -#define BTM_COD_MAJOR_WEARABLE 0x07 -#define BTM_COD_MAJOR_HEALTH 0x09 -#define BTM_COD_MAJOR_UNCLASSIFIED 0x1F +#define BTM_COD_MAJOR_COMPUTER COD_MAJOR_COMPUTER +#define BTM_COD_MAJOR_PHONE COD_MAJOR_PHONE +#define BTM_COD_MAJOR_AUDIO COD_MAJOR_AUDIO +#define BTM_COD_MAJOR_PERIPHERAL COD_MAJOR_PERIPHERAL +#define BTM_COD_MAJOR_IMAGING COD_MAJOR_IMAGING +#define BTM_COD_MAJOR_WEARABLE COD_MAJOR_WEARABLE +#define BTM_COD_MAJOR_HEALTH COD_MAJOR_HEALTH +#define BTM_COD_MAJOR_UNCLASSIFIED COD_MAJOR_UNCLASSIFIED /*************************** * service class fields ***************************/ -#define BTM_COD_SERVICE_LMTD_DISCOVER 0x0020 -#define BTM_COD_SERVICE_LE_AUDIO 0x0040 -#define BTM_COD_SERVICE_POSITIONING 0x0100 -#define BTM_COD_SERVICE_NETWORKING 0x0200 -#define BTM_COD_SERVICE_RENDERING 0x0400 -#define BTM_COD_SERVICE_CAPTURING 0x0800 -#define BTM_COD_SERVICE_OBJ_TRANSFER 0x1000 -#define BTM_COD_SERVICE_AUDIO 0x2000 -#define BTM_COD_SERVICE_TELEPHONY 0x4000 -#define BTM_COD_SERVICE_INFORMATION 0x8000 +#define BTM_COD_SERVICE_LMTD_DISCOVER COD_SERVICE_LMTD_DISCOVER +#define BTM_COD_SERVICE_LE_AUDIO COD_SERVICE_LE_AUDIO +#define BTM_COD_SERVICE_POSITIONING COD_SERVICE_POSITIONING +#define BTM_COD_SERVICE_NETWORKING COD_SERVICE_NETWORKING +#define BTM_COD_SERVICE_RENDERING COD_SERVICE_RENDERING +#define BTM_COD_SERVICE_CAPTURING COD_SERVICE_CAPTURING +#define BTM_COD_SERVICE_OBJ_TRANSFER COD_SERVICE_OBJ_TRANSFER +#define BTM_COD_SERVICE_AUDIO COD_SERVICE_AUDIO +#define BTM_COD_SERVICE_TELEPHONY COD_SERVICE_TELEPHONY +#define BTM_COD_SERVICE_INFORMATION COD_SERVICE_INFORMATION /* the COD masks */ #define BTM_COD_MINOR_CLASS_MASK 0xFC diff --git a/system/stack/include/btm_ble_api_types.h b/system/stack/include/btm_ble_api_types.h index 9a1679768b..d89028d27f 100644 --- a/system/stack/include/btm_ble_api_types.h +++ b/system/stack/include/btm_ble_api_types.h @@ -29,6 +29,7 @@ #include "stack/include/bt_octets.h" #include "stack/include/btm_status.h" #include "stack/include/hci_error_code.h" +#include "stack/include/ble_appearance.h" #include "types/ble_address_with_type.h" #include "types/raw_address.h" @@ -216,60 +217,70 @@ typedef uint8_t BLE_SIGNATURE[BTM_BLE_AUTH_SIGN_LEN]; /* Device address */ #endif /* Appearance Values Reported with BTM_BLE_AD_TYPE_APPEARANCE */ -#define BTM_BLE_APPEARANCE_UKNOWN 0x0000 -#define BTM_BLE_APPEARANCE_GENERIC_PHONE 0x0040 -#define BTM_BLE_APPEARANCE_GENERIC_COMPUTER 0x0080 -#define BTM_BLE_APPEARANCE_GENERIC_WATCH 0x00C0 -#define BTM_BLE_APPEARANCE_SPORTS_WATCH 0x00C1 -#define BTM_BLE_APPEARANCE_GENERIC_CLOCK 0x0100 -#define BTM_BLE_APPEARANCE_GENERIC_DISPLAY 0x0140 -#define BTM_BLE_APPEARANCE_GENERIC_REMOTE 0x0180 -#define BTM_BLE_APPEARANCE_GENERIC_EYEGLASSES 0x01C0 -#define BTM_BLE_APPEARANCE_GENERIC_TAG 0x0200 -#define BTM_BLE_APPEARANCE_GENERIC_KEYRING 0x0240 -#define BTM_BLE_APPEARANCE_GENERIC_MEDIA_PLAYER 0x0280 -#define BTM_BLE_APPEARANCE_GENERIC_BARCODE_SCANNER 0x02C0 -#define BTM_BLE_APPEARANCE_GENERIC_THERMOMETER 0x0300 -#define BTM_BLE_APPEARANCE_THERMOMETER_EAR 0x0301 -#define BTM_BLE_APPEARANCE_GENERIC_HEART_RATE 0x0340 -#define BTM_BLE_APPEARANCE_HEART_RATE_BELT 0x0341 -#define BTM_BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE 0x0380 -#define BTM_BLE_APPEARANCE_BLOOD_PRESSURE_ARM 0x0381 -#define BTM_BLE_APPEARANCE_BLOOD_PRESSURE_WRIST 0x0382 -#define BTM_BLE_APPEARANCE_GENERIC_HID 0x03C0 -#define BTM_BLE_APPEARANCE_HID_KEYBOARD 0x03C1 -#define BTM_BLE_APPEARANCE_HID_MOUSE 0x03C2 -#define BTM_BLE_APPEARANCE_HID_JOYSTICK 0x03C3 -#define BTM_BLE_APPEARANCE_HID_GAMEPAD 0x03C4 -#define BTM_BLE_APPEARANCE_HID_DIGITIZER_TABLET 0x03C5 -#define BTM_BLE_APPEARANCE_HID_CARD_READER 0x03C6 -#define BTM_BLE_APPEARANCE_HID_DIGITAL_PEN 0x03C7 -#define BTM_BLE_APPEARANCE_HID_BARCODE_SCANNER 0x03C8 -#define BTM_BLE_APPEARANCE_GENERIC_GLUCOSE 0x0400 -#define BTM_BLE_APPEARANCE_GENERIC_WALKING 0x0440 -#define BTM_BLE_APPEARANCE_WALKING_IN_SHOE 0x0441 -#define BTM_BLE_APPEARANCE_WALKING_ON_SHOE 0x0442 -#define BTM_BLE_APPEARANCE_WALKING_ON_HIP 0x0443 -#define BTM_BLE_APPEARANCE_GENERIC_CYCLING 0x0480 -#define BTM_BLE_APPEARANCE_CYCLING_COMPUTER 0x0481 -#define BTM_BLE_APPEARANCE_CYCLING_SPEED 0x0482 -#define BTM_BLE_APPEARANCE_CYCLING_CADENCE 0x0483 -#define BTM_BLE_APPEARANCE_CYCLING_POWER 0x0484 -#define BTM_BLE_APPEARANCE_CYCLING_SPEED_CADENCE 0x0485 -#define BTM_BLE_APPEARANCE_GENERIC_WEARABLE_AUDIO_DEVICE 0x0940 -#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_EARBUD 0x0941 -#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADSET 0x0942 -#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADPHONES 0x0943 -#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_NECK_BAND 0x0944 -#define BTM_BLE_APPEARANCE_GENERIC_PULSE_OXIMETER 0x0C40 -#define BTM_BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP 0x0C41 -#define BTM_BLE_APPEARANCE_PULSE_OXIMETER_WRIST 0x0C42 -#define BTM_BLE_APPEARANCE_GENERIC_WEIGHT 0x0C80 -#define BTM_BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS 0x1440 -#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION 0x1441 -#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_AND_NAV 0x1442 -#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD 0x1443 -#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD_AND_NAV 0x1444 +#define BTM_BLE_APPEARANCE_UNKNOWN BLE_APPEARANCE_UNKNOWN +#define BTM_BLE_APPEARANCE_GENERIC_PHONE BLE_APPEARANCE_GENERIC_PHONE +#define BTM_BLE_APPEARANCE_GENERIC_COMPUTER BLE_APPEARANCE_GENERIC_COMPUTER +#define BTM_BLE_APPEARANCE_GENERIC_WATCH BLE_APPEARANCE_GENERIC_WATCH +#define BTM_BLE_APPEARANCE_SPORTS_WATCH BLE_APPEARANCE_SPORTS_WATCH +#define BTM_BLE_APPEARANCE_GENERIC_CLOCK BLE_APPEARANCE_GENERIC_CLOCK +#define BTM_BLE_APPEARANCE_GENERIC_DISPLAY BLE_APPEARANCE_GENERIC_DISPLAY +#define BTM_BLE_APPEARANCE_GENERIC_REMOTE BLE_APPEARANCE_GENERIC_REMOTE +#define BTM_BLE_APPEARANCE_GENERIC_EYEGLASSES BLE_APPEARANCE_GENERIC_EYEGLASSES +#define BTM_BLE_APPEARANCE_GENERIC_TAG BLE_APPEARANCE_GENERIC_TAG +#define BTM_BLE_APPEARANCE_GENERIC_KEYRING BLE_APPEARANCE_GENERIC_KEYRING +#define BTM_BLE_APPEARANCE_GENERIC_MEDIA_PLAYER BLE_APPEARANCE_GENERIC_MEDIA_PLAYER +#define BTM_BLE_APPEARANCE_GENERIC_BARCODE_SCANNER BLE_APPEARANCE_GENERIC_BARCODE_SCANNER +#define BTM_BLE_APPEARANCE_GENERIC_THERMOMETER BLE_APPEARANCE_GENERIC_THERMOMETER +#define BTM_BLE_APPEARANCE_THERMOMETER_EAR BLE_APPEARANCE_THERMOMETER_EAR +#define BTM_BLE_APPEARANCE_GENERIC_HEART_RATE BLE_APPEARANCE_GENERIC_HEART_RATE +#define BTM_BLE_APPEARANCE_HEART_RATE_BELT BLE_APPEARANCE_HEART_RATE_BELT +#define BTM_BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE +#define BTM_BLE_APPEARANCE_BLOOD_PRESSURE_ARM BLE_APPEARANCE_BLOOD_PRESSURE_ARM +#define BTM_BLE_APPEARANCE_BLOOD_PRESSURE_WRIST BLE_APPEARANCE_BLOOD_PRESSURE_WRIST +#define BTM_BLE_APPEARANCE_GENERIC_HID BLE_APPEARANCE_GENERIC_HID +#define BTM_BLE_APPEARANCE_HID_KEYBOARD BLE_APPEARANCE_HID_KEYBOARD +#define BTM_BLE_APPEARANCE_HID_MOUSE BLE_APPEARANCE_HID_MOUSE +#define BTM_BLE_APPEARANCE_HID_JOYSTICK BLE_APPEARANCE_HID_JOYSTICK +#define BTM_BLE_APPEARANCE_HID_GAMEPAD BLE_APPEARANCE_HID_GAMEPAD +#define BTM_BLE_APPEARANCE_HID_DIGITIZER_TABLET BLE_APPEARANCE_HID_DIGITIZER_TABLET +#define BTM_BLE_APPEARANCE_HID_CARD_READER BLE_APPEARANCE_HID_CARD_READER +#define BTM_BLE_APPEARANCE_HID_DIGITAL_PEN BLE_APPEARANCE_HID_DIGITAL_PEN +#define BTM_BLE_APPEARANCE_HID_BARCODE_SCANNER BLE_APPEARANCE_HID_BARCODE_SCANNER +#define BTM_BLE_APPEARANCE_GENERIC_GLUCOSE BLE_APPEARANCE_GENERIC_GLUCOSE +#define BTM_BLE_APPEARANCE_GENERIC_WALKING BLE_APPEARANCE_GENERIC_WALKING +#define BTM_BLE_APPEARANCE_WALKING_IN_SHOE BLE_APPEARANCE_WALKING_IN_SHOE +#define BTM_BLE_APPEARANCE_WALKING_ON_SHOE BLE_APPEARANCE_WALKING_ON_SHOE +#define BTM_BLE_APPEARANCE_WALKING_ON_HIP BLE_APPEARANCE_WALKING_ON_HIP +#define BTM_BLE_APPEARANCE_GENERIC_CYCLING BLE_APPEARANCE_GENERIC_CYCLING +#define BTM_BLE_APPEARANCE_CYCLING_COMPUTER BLE_APPEARANCE_CYCLING_COMPUTER +#define BTM_BLE_APPEARANCE_CYCLING_SPEED BLE_APPEARANCE_CYCLING_SPEED +#define BTM_BLE_APPEARANCE_CYCLING_CADENCE BLE_APPEARANCE_CYCLING_CADENCE +#define BTM_BLE_APPEARANCE_CYCLING_POWER BLE_APPEARANCE_CYCLING_POWER +#define BTM_BLE_APPEARANCE_CYCLING_SPEED_CADENCE BLE_APPEARANCE_CYCLING_SPEED_CADENCE +#define BTM_BLE_APPEARANCE_GENERIC_WEARABLE_AUDIO_DEVICE \ + BLE_APPEARANCE_GENERIC_WEARABLE_AUDIO_DEVICE +#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_EARBUD \ + BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_EARBUD +#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADSET \ + BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADSET +#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADPHONES \ + BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADPHONES +#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_NECK_BAND \ + BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_NECK_BAND +#define BTM_BLE_APPEARANCE_GENERIC_PULSE_OXIMETER BLE_APPEARANCE_GENERIC_PULSE_OXIMETER +#define BTM_BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP +#define BTM_BLE_APPEARANCE_PULSE_OXIMETER_WRIST BLE_APPEARANCE_PULSE_OXIMETER_WRIST +#define BTM_BLE_APPEARANCE_GENERIC_WEIGHT BLE_APPEARANCE_GENERIC_WEIGHT +#define BTM_BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS \ + BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS +#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION \ + BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION +#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_AND_NAV \ + BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_AND_NAV +#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD \ + BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD +#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD_AND_NAV \ + BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD_AND_NAV /* Structure returned with Rand/Encrypt complete callback */ typedef struct { diff --git a/system/stack/include/smp_status.h b/system/stack/include/smp_status.h index 86670a5161..1d222ab419 100644 --- a/system/stack/include/smp_status.h +++ b/system/stack/include/smp_status.h @@ -41,25 +41,27 @@ typedef enum : uint8_t { SMP_NUMERIC_COMPAR_FAIL = 0x0C, SMP_BR_PARING_IN_PROGR = 0x0D, SMP_XTRANS_DERIVE_NOT_ALLOW = 0x0E, - SMP_MAX_FAIL_RSN_PER_SPEC = SMP_XTRANS_DERIVE_NOT_ALLOW, + SMP_KEY_REJECTED = 0x0F, + SMP_BUSY = 0x10, /*device is not ready to perform a pairing procedure*/ + SMP_MAX_FAIL_RSN_PER_SPEC = SMP_BUSY, /* self defined error code */ - SMP_PAIR_INTERNAL_ERR = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x01), /* 0x0F */ + SMP_PAIR_INTERNAL_ERR = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x01), /* 0x11 */ /* Unknown IO capability, unable to decide association model */ - SMP_UNKNOWN_IO_CAP = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x02), /* 0x10 */ + SMP_UNKNOWN_IO_CAP = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x02), /* 0x12 */ - SMP_BUSY = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x05), /* 0x13 */ - SMP_ENC_FAIL = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x06), /* 0x14 */ - SMP_STARTED = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x07), /* 0x15 */ - SMP_RSP_TIMEOUT = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x08), /* 0x16 */ + SMP_IMPL_BUSY = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x05), /* 0x15 */ + SMP_ENC_FAIL = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x06), /* 0x16 */ + SMP_STARTED = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x07), /* 0x17 */ + SMP_RSP_TIMEOUT = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x08), /* 0x18 */ /* Unspecified failure reason */ - SMP_FAIL = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0A), /* 0x18 */ + SMP_FAIL = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0A), /* 0x1A */ - SMP_CONN_TOUT = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0B), /* 0x19 */ - SMP_SIRK_DEVICE_INVALID = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0C), /* 0x1a */ - SMP_USER_CANCELLED = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0D), /* 0x1b */ + SMP_CONN_TOUT = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0B), /* 0x1B */ + SMP_SIRK_DEVICE_INVALID = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0C), /* 0x1C */ + SMP_USER_CANCELLED = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0D), /* 0x1D */ } tSMP_STATUS; inline std::string smp_status_text(const tSMP_STATUS& status) { @@ -79,9 +81,11 @@ inline std::string smp_status_text(const tSMP_STATUS& status) { CASE_RETURN_TEXT(SMP_NUMERIC_COMPAR_FAIL); CASE_RETURN_TEXT(SMP_BR_PARING_IN_PROGR); CASE_RETURN_TEXT(SMP_XTRANS_DERIVE_NOT_ALLOW); + CASE_RETURN_TEXT(SMP_KEY_REJECTED); + CASE_RETURN_TEXT(SMP_BUSY); CASE_RETURN_TEXT(SMP_PAIR_INTERNAL_ERR); CASE_RETURN_TEXT(SMP_UNKNOWN_IO_CAP); - CASE_RETURN_TEXT(SMP_BUSY); + CASE_RETURN_TEXT(SMP_IMPL_BUSY); CASE_RETURN_TEXT(SMP_ENC_FAIL); CASE_RETURN_TEXT(SMP_STARTED); CASE_RETURN_TEXT(SMP_RSP_TIMEOUT); diff --git a/system/stack/rfcomm/port_rfc.cc b/system/stack/rfcomm/port_rfc.cc index e7813a240f..14a9cb5bf0 100644 --- a/system/stack/rfcomm/port_rfc.cc +++ b/system/stack/rfcomm/port_rfc.cc @@ -1052,7 +1052,7 @@ void port_rfc_closed(tPORT* p_port, uint8_t res) { p_port->rfc.sm_cb.state = RFC_STATE_CLOSED; log::info( - "RFCOMM connection closed, index={}, state={}, reason={}[{}], " + "RFCOMM connection closed, port_handle={}, state={}, reason={}[{}], " "UUID=0x{:x}, bd_addr={}, is_server={}", p_port->handle, p_port->state, PORT_GetResultString(res), res, p_port->uuid, p_port->bd_addr, p_port->is_server); diff --git a/system/stack/rfcomm/rfc_port_fsm.cc b/system/stack/rfcomm/rfc_port_fsm.cc index 040315d276..be47e22617 100644 --- a/system/stack/rfcomm/rfc_port_fsm.cc +++ b/system/stack/rfcomm/rfc_port_fsm.cc @@ -452,12 +452,12 @@ void rfc_port_sm_orig_wait_sec_check(tPORT* p_port, tRFC_PORT_EVENT event, void* void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) { switch (event) { case RFC_PORT_EVENT_OPEN: - log::error("RFC_PORT_EVENT_OPEN bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr, + log::error("RFC_PORT_EVENT_OPEN bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); return; case RFC_PORT_EVENT_CLOSE: - log::info("RFC_PORT_EVENT_CLOSE bd_addr:{}, handle:{} dlci:{} scn:{}", p_port->bd_addr, + log::info("RFC_PORT_EVENT_CLOSE bd_addr:{}, port_handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); rfc_port_timer_start(p_port, RFC_DISC_TIMEOUT); rfc_send_disc(p_port->rfc.p_mcb, p_port->dlci); @@ -466,7 +466,7 @@ void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) { return; case RFC_PORT_EVENT_CLEAR: - log::warn("RFC_PORT_EVENT_CLEAR bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr, + log::warn("RFC_PORT_EVENT_CLEAR bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); rfc_port_closed(p_port); return; @@ -475,7 +475,7 @@ void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) { // Send credits in the frame. Pass them in the layer specific member of the hdr. // There might be an initial case when we reduced rx_max and credit_rx is still bigger. // Make sure that we do not send 255 - log::verbose("RFC_PORT_EVENT_DATA bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr, + log::verbose("RFC_PORT_EVENT_DATA bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); if ((p_port->rfc.p_mcb->flow == PORT_FC_CREDIT) && (((BT_HDR*)p_data)->len < p_port->peer_mtu) && (!p_port->rx.user_fc) && @@ -490,25 +490,25 @@ void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) { return; case RFC_PORT_EVENT_UA: - log::verbose("RFC_PORT_EVENT_UA bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr, + log::verbose("RFC_PORT_EVENT_UA bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); return; case RFC_PORT_EVENT_SABME: - log::verbose("RFC_PORT_EVENT_SABME bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr, + log::verbose("RFC_PORT_EVENT_SABME bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); rfc_send_ua(p_port->rfc.p_mcb, p_port->dlci); return; case RFC_PORT_EVENT_DM: - log::info("RFC_EVENT_DM bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, - p_port->dlci, p_port->scn); + log::info("RFC_EVENT_DM bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr, + p_port->handle, p_port->dlci, p_port->scn); PORT_DlcReleaseInd(p_port->rfc.p_mcb, p_port->dlci); rfc_port_closed(p_port); return; case RFC_PORT_EVENT_DISC: - log::info("RFC_PORT_EVENT_DISC bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr, + log::info("RFC_PORT_EVENT_DISC bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); p_port->rfc.sm_cb.state = RFC_STATE_CLOSED; rfc_send_ua(p_port->rfc.p_mcb, p_port->dlci); @@ -522,19 +522,19 @@ void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) { return; case RFC_PORT_EVENT_UIH: - log::verbose("RFC_PORT_EVENT_UIH bd_addr:{}, handle:{} dlci:{} scn:{}", p_port->bd_addr, + log::verbose("RFC_PORT_EVENT_UIH bd_addr:{}, port_handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); rfc_port_uplink_data(p_port, (BT_HDR*)p_data); return; case RFC_PORT_EVENT_TIMEOUT: PORT_TimeOutCloseMux(p_port->rfc.p_mcb); - log::error("RFC_PORT_EVENT_TIMEOUT bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr, + log::error("RFC_PORT_EVENT_TIMEOUT bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); return; default: - log::error("Received unexpected event:{} bd_addr:{} handle:{} dlci:{} scn:{}", + log::error("Received unexpected event:{} bd_addr:{} port_handle:{} dlci:{} scn:{}", rfcomm_port_event_text(event), p_port->bd_addr, p_port->handle, p_port->dlci, p_port->scn); break; @@ -560,7 +560,7 @@ void rfc_port_sm_disc_wait_ua(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data return; case RFC_PORT_EVENT_CLEAR: - log::warn("RFC_PORT_EVENT_CLEAR, handle:{}", p_port->handle); + log::warn("RFC_PORT_EVENT_CLEAR, port_handle:{}", p_port->handle); rfc_port_closed(p_port); return; @@ -573,7 +573,7 @@ void rfc_port_sm_disc_wait_ua(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data FALLTHROUGH_INTENDED; /* FALLTHROUGH */ case RFC_PORT_EVENT_DM: - log::warn("RFC_EVENT_DM|RFC_EVENT_UA[{}], handle:{}", event, p_port->handle); + log::warn("RFC_EVENT_DM|RFC_EVENT_UA[{}], port_handle:{}", event, p_port->handle); if (com::android::bluetooth::flags::rfcomm_always_disc_initiator_in_disc_wait_ua()) { // If we got a DM in RFC_STATE_DISC_WAIT_UA, it's likely that both ends // attempt to DISC at the same time and both get a DM. @@ -602,7 +602,7 @@ void rfc_port_sm_disc_wait_ua(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data return; case RFC_PORT_EVENT_TIMEOUT: - log::error("RFC_EVENT_TIMEOUT, handle:{}", p_port->handle); + log::error("RFC_EVENT_TIMEOUT, port_handle:{}", p_port->handle); rfc_port_closed(p_port); return; default: diff --git a/system/stack/rfcomm/rfc_port_if.cc b/system/stack/rfcomm/rfc_port_if.cc index af6906fbb5..4fdd46059d 100644 --- a/system/stack/rfcomm/rfc_port_if.cc +++ b/system/stack/rfcomm/rfc_port_if.cc @@ -333,7 +333,12 @@ void RFCOMM_LineStatusReq(tRFC_MCB* p_mcb, uint8_t dlci, uint8_t status) { * ******************************************************************************/ void RFCOMM_DlcReleaseReq(tRFC_MCB* p_mcb, uint8_t dlci) { - rfc_port_sm_execute(port_find_mcb_dlci_port(p_mcb, dlci), RFC_PORT_EVENT_CLOSE, nullptr); + tPORT* p_port = port_find_mcb_dlci_port(p_mcb, dlci); + if (p_port == nullptr) { + log::warn("Unable to find DLCI port dlci:{}", dlci); + return; + } + rfc_port_sm_execute(p_port, RFC_PORT_EVENT_CLOSE, nullptr); } /******************************************************************************* diff --git a/system/stack/smp/smp_act.cc b/system/stack/smp/smp_act.cc index c625217c7c..fd77fabbed 100644 --- a/system/stack/smp/smp_act.cc +++ b/system/stack/smp/smp_act.cc @@ -23,6 +23,7 @@ #include <cstring> +#include "bta/dm/bta_dm_sec_int.h" #include "btif/include/btif_common.h" #include "btif/include/core_callbacks.h" #include "btif/include/stack_manager_t.h" @@ -34,6 +35,7 @@ #include "stack/btm/btm_ble_sec.h" #include "stack/btm/btm_dev.h" #include "stack/btm/btm_sec.h" +#include "stack/include/acl_api.h" #include "stack/include/bt_octets.h" #include "stack/include/bt_types.h" #include "stack/include/btm_client_interface.h" @@ -548,6 +550,21 @@ void smp_proc_pair_cmd(tSMP_CB* p_cb, tSMP_INT_DATA* p_data) { /* erase all keys if it is peripheral proc pairing req */ if (p_dev_rec && (p_cb->role == HCI_ROLE_PERIPHERAL)) { + if (com::android::bluetooth::flags::key_missing_ble_peripheral()) { + tBTM_SEC_DEV_REC* p_rec = btm_find_dev(p_cb->pairing_bda); + /* If we bonded, but not encrypted, it's a key missing - disconnect. + * If we are bonded, its key upgrade and ok to continue. + * If we are not bonded, its new device pairing and ok. + */ + if (p_rec != NULL && p_rec->sec_rec.is_le_link_key_known() && + !p_rec->sec_rec.is_le_device_encrypted()) { + log::warn("bonded unencrypted central wants to pair {}", p_cb->pairing_bda); + bta_dm_remote_key_missing(p_cb->pairing_bda); + acl_disconnect_from_handle(p_rec->ble_hci_handle, HCI_ERR_AUTH_FAILURE, + "bonded unencrypted central wants to pair"); + return; + } + } btm_sec_clear_ble_keys(p_dev_rec); } diff --git a/system/stack/smp/smp_api.cc b/system/stack/smp/smp_api.cc index 0d66b0a064..a46f113aec 100644 --- a/system/stack/smp/smp_api.cc +++ b/system/stack/smp/smp_api.cc @@ -89,7 +89,7 @@ tSMP_STATUS SMP_Pair(const RawAddress& bd_addr, tBLE_ADDR_TYPE addr_type) { if (p_cb->state != SMP_STATE_IDLE || p_cb->flags & SMP_PAIR_FLAGS_WE_STARTED_DD || p_cb->smp_over_br) { /* pending security on going, reject this one */ - return SMP_BUSY; + return SMP_IMPL_BUSY; } else { p_cb->flags = SMP_PAIR_FLAGS_WE_STARTED_DD; p_cb->pairing_bda = bd_addr; @@ -135,7 +135,7 @@ tSMP_STATUS SMP_BR_PairWith(const RawAddress& bd_addr) { if (p_cb->state != SMP_STATE_IDLE || p_cb->smp_over_br || p_cb->flags & SMP_PAIR_FLAGS_WE_STARTED_DD) { /* pending security on going, reject this one */ - return SMP_BUSY; + return SMP_IMPL_BUSY; } p_cb->role = HCI_ROLE_CENTRAL; diff --git a/system/stack/smp/smp_utils.cc b/system/stack/smp/smp_utils.cc index 81b005a78c..e5aefe3c6a 100644 --- a/system/stack/smp/smp_utils.cc +++ b/system/stack/smp/smp_utils.cc @@ -1231,7 +1231,7 @@ void smp_reject_unexpected_pairing_command(const RawAddress& bd_addr) { p = (uint8_t*)(p_buf + 1) + L2CAP_MIN_OFFSET; UINT8_TO_STREAM(p, SMP_OPCODE_PAIRING_FAILED); - UINT8_TO_STREAM(p, SMP_PAIR_NOT_SUPPORT); + UINT8_TO_STREAM(p, SMP_BUSY); p_buf->offset = L2CAP_MIN_OFFSET; p_buf->len = SMP_PAIR_FAIL_SIZE; diff --git a/system/stack/test/stack_smp_test.cc b/system/stack/test/stack_smp_test.cc index 41d61e738b..8f020ae1c5 100644 --- a/system/stack/test/stack_smp_test.cc +++ b/system/stack/test/stack_smp_test.cc @@ -375,11 +375,13 @@ TEST(SmpStatusText, smp_status_text) { std::make_pair(SMP_NUMERIC_COMPAR_FAIL, "SMP_NUMERIC_COMPAR_FAIL"), std::make_pair(SMP_BR_PARING_IN_PROGR, "SMP_BR_PARING_IN_PROGR"), std::make_pair(SMP_XTRANS_DERIVE_NOT_ALLOW, "SMP_XTRANS_DERIVE_NOT_ALLOW"), + std::make_pair(SMP_KEY_REJECTED, "SMP_KEY_REJECTED"), + std::make_pair(SMP_BUSY, "SMP_BUSY"), std::make_pair(SMP_MAX_FAIL_RSN_PER_SPEC, - "SMP_XTRANS_DERIVE_NOT_ALLOW"), // NOTE: Dup + "SMP_BUSY"), // NOTE: Dup std::make_pair(SMP_PAIR_INTERNAL_ERR, "SMP_PAIR_INTERNAL_ERR"), std::make_pair(SMP_UNKNOWN_IO_CAP, "SMP_UNKNOWN_IO_CAP"), - std::make_pair(SMP_BUSY, "SMP_BUSY"), + std::make_pair(SMP_IMPL_BUSY, "SMP_IMPL_BUSY"), std::make_pair(SMP_ENC_FAIL, "SMP_ENC_FAIL"), std::make_pair(SMP_STARTED, "SMP_STARTED"), std::make_pair(SMP_RSP_TIMEOUT, "SMP_RSP_TIMEOUT"), diff --git a/system/test/headless/property.cc b/system/test/headless/property.cc index fe6e21f1ed..2b5df50ff2 100644 --- a/system/test/headless/property.cc +++ b/system/test/headless/property.cc @@ -99,6 +99,10 @@ std::map<::bt_property_type_t, return new headless::property::void_t(data, len, BT_PROPERTY_REMOTE_DEVICE_TIMESTAMP); }}, + {BT_PROPERTY_UUIDS_LE, + [](const uint8_t* data, const size_t len) -> headless::bt_property_t* { + return new headless::property::uuid_t(data, len); + }}, }; } // namespace diff --git a/system/test/headless/property.h b/system/test/headless/property.h index 6af17a244f..1113495f24 100644 --- a/system/test/headless/property.h +++ b/system/test/headless/property.h @@ -52,6 +52,7 @@ inline std::string bt_property_type_text(const ::bt_property_type_t type) { CASE_RETURN_TEXT(BT_PROPERTY_REMOTE_MODEL_NUM); CASE_RETURN_TEXT(BT_PROPERTY_REMOTE_DEVICE_TIMESTAMP); CASE_RETURN_TEXT(BT_PROPERTY_REMOTE_ADDR_TYPE); + CASE_RETURN_TEXT(BT_PROPERTY_UUIDS_LE); CASE_RETURN_TEXT(BT_PROPERTY_RESERVED_0x14); default: RETURN_UNKNOWN_TYPE_STRING(::bt_property_type_t, type); diff --git a/tools/rootcanal/hal/bluetooth_hci.cc b/tools/rootcanal/hal/bluetooth_hci.cc index b08d59995b..b36c18687d 100644 --- a/tools/rootcanal/hal/bluetooth_hci.cc +++ b/tools/rootcanal/hal/bluetooth_hci.cc @@ -53,24 +53,6 @@ bool BtTestConsoleEnabled() { } // namespace -class BluetoothDeathRecipient : public hidl_death_recipient { -public: - BluetoothDeathRecipient(const sp<IBluetoothHci> hci) : mHci(hci) {} - - void serviceDied(uint64_t /* cookie */, - const wp<::android::hidl::base::V1_0::IBase>& /* who */) override { - ALOGE("BluetoothDeathRecipient::serviceDied - Bluetooth service died"); - has_died_ = true; - mHci->close(); - } - sp<IBluetoothHci> mHci; - bool getHasDied() const { return has_died_; } - void setHasDied(bool has_died) { has_died_ = has_died; } - -private: - bool has_died_{false}; -}; - BluetoothHci::BluetoothHci() : death_recipient_(new BluetoothDeathRecipient(this)) {} Return<void> BluetoothHci::initialize(const sp<V1_0::IBluetoothHciCallbacks>& cb) { diff --git a/tools/rootcanal/hal/bluetooth_hci.h b/tools/rootcanal/hal/bluetooth_hci.h index 95bda38957..95e3e45334 100644 --- a/tools/rootcanal/hal/bluetooth_hci.h +++ b/tools/rootcanal/hal/bluetooth_hci.h @@ -18,6 +18,7 @@ #include <android/hardware/bluetooth/1.1/IBluetoothHci.h> #include <hidl/MQDescriptor.h> +#include <log/log.h> #include "hci_packetizer.h" #include "model/controller/dual_mode_controller.h" @@ -44,6 +45,25 @@ using android::net::ConnectCallback; using rootcanal::Device; using rootcanal::Phy; +class BluetoothDeathRecipient : public hidl_death_recipient { +public: + BluetoothDeathRecipient(const sp<IBluetoothHci> hci) : mHci(hci) {} + + void serviceDied(uint64_t /* cookie */, + const wp<::android::hidl::base::V1_0::IBase>& /* who */) override { + ALOGE("BluetoothDeathRecipient::serviceDied - Bluetooth service died"); + has_died_ = true; + mHci->close(); + } + + sp<IBluetoothHci> mHci; + bool getHasDied() const { return has_died_; } + void setHasDied(bool has_died) { has_died_ = has_died; } + +private: + bool has_died_{false}; +}; + class BluetoothHci : public IBluetoothHci { public: BluetoothHci(); diff --git a/tools/rootcanal/model/controller/controller_properties.cc b/tools/rootcanal/model/controller/controller_properties.cc index 26718a9747..3f6c918bfe 100644 --- a/tools/rootcanal/model/controller/controller_properties.cc +++ b/tools/rootcanal/model/controller/controller_properties.cc @@ -1743,6 +1743,40 @@ ControllerProperties::ControllerProperties(rootcanal::configuration::Controller le_supported_states = 0x3ffffffffff; break; + case ControllerPreset::INTEL_BE200: + // Configuration extracted with the helper script controller_info.py + supports_csr_vendor_command = true; + br_supported = true; + le_supported = true; + hci_version = bluetooth::hci::HciVersion::V_5_4; + hci_subversion = 0x4363; + lmp_version = bluetooth::hci::LmpVersion::V_5_4; + lmp_subversion = 0x4363; + company_identifier = 0x2; + supported_commands = std::array<uint8_t, 64>{ + 0xbf, 0xff, 0xfb, 0x03, 0xcc, 0xff, 0x0f, 0xff, 0xbf, 0xff, 0xfc, 0x1f, 0xf2, + 0x0f, 0xe8, 0xfe, 0x3f, 0xf7, 0x8f, 0xff, 0x1c, 0x00, 0x04, 0x00, 0x61, 0xf7, + 0xff, 0xff, 0x7f, 0x38, 0x00, 0x00, 0xfe, 0xf0, 0xff, 0xff, 0xff, 0xe3, 0x80, + 0x07, 0x00, 0xe8, 0x1f, 0xfc, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + lmp_features = std::array<uint64_t, 3>{0x877bffdbfe0ffebf, 0x0, 0x300}; + acl_data_packet_length = 1021; + total_num_acl_data_packets = 4; + sco_data_packet_length = 96; + total_num_sco_data_packets = 6; + num_supported_iac = 2; + le_features = 0x80059ff; + le_acl_data_packet_length = 251; + total_num_le_acl_data_packets = 3; + le_filter_accept_list_size = 25; + le_resolving_list_size = 25; + le_supported_states = 0x3ffffffffff; + le_max_advertising_data_length = 160; + le_num_supported_advertising_sets = 12; + le_periodic_advertiser_list_size = 12; + break; + default: break; } diff --git a/tools/rootcanal/model/controller/dual_mode_controller.cc b/tools/rootcanal/model/controller/dual_mode_controller.cc index 15f788b438..c7b5d0bbe2 100644 --- a/tools/rootcanal/model/controller/dual_mode_controller.cc +++ b/tools/rootcanal/model/controller/dual_mode_controller.cc @@ -3290,6 +3290,11 @@ void DualModeController::WriteLoopbackMode(CommandView command) { ErrorCode::SUCCESS)); } +void DualModeController::IntelDdcConfigWrite(CommandView /*command*/) { + send_event_(bluetooth::hci::CommandCompleteBuilder::Create(kNumCommandPackets, OpCode::INTEL_DDC_CONFIG_WRITE, + std::vector<uint8_t> { static_cast<uint8_t>(ErrorCode::SUCCESS) })); +} + // Note: the list does not contain all defined opcodes. // Notable exceptions: // - Vendor commands @@ -4274,6 +4279,8 @@ const std::unordered_map<OpCode, DualModeController::CommandHandler> {OpCode::LE_GET_CONTROLLER_ACTIVITY_ENERGY_INFO, &DualModeController::LeGetControllerActivityEnergyInfo}, {OpCode::LE_EX_SET_SCAN_PARAMETERS, &DualModeController::LeExSetScanParameters}, - {OpCode::GET_CONTROLLER_DEBUG_INFO, &DualModeController::GetControllerDebugInfo}}; + {OpCode::GET_CONTROLLER_DEBUG_INFO, &DualModeController::GetControllerDebugInfo}, + {OpCode::INTEL_DDC_CONFIG_WRITE, &DualModeController::IntelDdcConfigWrite}, + }; } // namespace rootcanal diff --git a/tools/rootcanal/model/controller/dual_mode_controller.h b/tools/rootcanal/model/controller/dual_mode_controller.h index d0394a7bfd..c5574bda85 100644 --- a/tools/rootcanal/model/controller/dual_mode_controller.h +++ b/tools/rootcanal/model/controller/dual_mode_controller.h @@ -525,6 +525,7 @@ public: void LeGetControllerActivityEnergyInfo(CommandView command); void LeExSetScanParameters(CommandView command); void GetControllerDebugInfo(CommandView command); + void IntelDdcConfigWrite(CommandView command); // CSR vendor command. // Implement the command specific to the CSR controller diff --git a/tools/rootcanal/model/setup/test_command_handler.cc b/tools/rootcanal/model/setup/test_command_handler.cc index 93315ae16e..59071842a5 100644 --- a/tools/rootcanal/model/setup/test_command_handler.cc +++ b/tools/rootcanal/model/setup/test_command_handler.cc @@ -246,6 +246,8 @@ void TestCommandHandler::SetDeviceConfiguration(const vector<std::string>& args) preset = rootcanal::configuration::ControllerPreset::LAIRD_BL654; } else if (args[1] == "csr_rck_pts_dongle") { preset = rootcanal::configuration::ControllerPreset::CSR_RCK_PTS_DONGLE; + } else if (args[1] == "intel_be200") { + preset = rootcanal::configuration::ControllerPreset::INTEL_BE200; } else { response_string_ = "TestCommandHandler 'set_device_configuration' invalid configuration preset"; send_response_(response_string_); diff --git a/tools/rootcanal/packets/hci_packets.pdl b/tools/rootcanal/packets/hci_packets.pdl index 23abb93b29..8f3d3c1588 100644 --- a/tools/rootcanal/packets/hci_packets.pdl +++ b/tools/rootcanal/packets/hci_packets.pdl @@ -400,6 +400,7 @@ enum OpCode : 16 { // VENDOR_SPECIFIC // MSFT_OPCODE_xxxx below is needed for the tests. MSFT_OPCODE_INTEL = 0xFC1E, + INTEL_DDC_CONFIG_WRITE = 0xFC8B, LE_GET_VENDOR_CAPABILITIES = 0xFD53, LE_BATCH_SCAN = 0xFD56, LE_APCF = 0xFD57, diff --git a/tools/rootcanal/proto/rootcanal/configuration.proto b/tools/rootcanal/proto/rootcanal/configuration.proto index 0b6db15c89..0beb961291 100644 --- a/tools/rootcanal/proto/rootcanal/configuration.proto +++ b/tools/rootcanal/proto/rootcanal/configuration.proto @@ -24,6 +24,8 @@ enum ControllerPreset { LAIRD_BL654 = 1; // Official PTS dongle, CSR rck. CSR_RCK_PTS_DONGLE = 2; + // Official PTS dongle, Intel BE200. + INTEL_BE200 = 3; } message ControllerFeatures { diff --git a/tools/rootcanal/rust/src/lmp/procedure/mod.rs b/tools/rootcanal/rust/src/lmp/procedure/mod.rs index 5f93e62da9..d73c5d39e9 100644 --- a/tools/rootcanal/rust/src/lmp/procedure/mod.rs +++ b/tools/rootcanal/rust/src/lmp/procedure/mod.rs @@ -66,7 +66,7 @@ pub trait Context { /// Future for Context::receive_hci_command and Context::receive_lmp_packet pub struct ReceiveFuture<'a, C: ?Sized, P>(fn(&'a C) -> Poll<P>, &'a C); -impl<'a, C, O> Future for ReceiveFuture<'a, C, O> +impl<C, O> Future for ReceiveFuture<'_, C, O> where C: Context, { @@ -80,7 +80,7 @@ where /// Future for Context::receive_hci_command and Context::receive_lmp_packet pub struct SendAcceptedLmpPacketFuture<'a, C: ?Sized>(&'a C, lmp::Opcode); -impl<'a, C> Future for SendAcceptedLmpPacketFuture<'a, C> +impl<C> Future for SendAcceptedLmpPacketFuture<'_, C> where C: Context, { diff --git a/tools/rootcanal/scripts/controller_info.py b/tools/rootcanal/scripts/controller_info.py index 10a74a0daf..cca4f0970b 100755 --- a/tools/rootcanal/scripts/controller_info.py +++ b/tools/rootcanal/scripts/controller_info.py @@ -98,66 +98,74 @@ async def br_edr_properties(host: Host): page2 = await host.expect_evt(hci.ReadLocalExtendedFeaturesComplete) print( - f"lmp_features: {{ 0x{page0.lmp_features:x}, 0x{page1.extended_lmp_features:x}, 0x{page2.extended_lmp_features:x} }}" + f"lmp_features = {{ 0x{page0.lmp_features:x}, 0x{page1.extended_lmp_features:x}, 0x{page2.extended_lmp_features:x} }};" ) await host.send_cmd(hci.ReadBufferSize()) evt = await host.expect_evt(hci.ReadBufferSizeComplete) - print(f"acl_data_packet_length: {evt.acl_data_packet_length}") - print(f"total_num_acl_data_packets: {evt.total_num_acl_data_packets}") - print(f"sco_data_packet_length: {evt.synchronous_data_packet_length}") - print(f"total_num_sco_data_packets: {evt.total_num_synchronous_data_packets}") + print(f"acl_data_packet_length = {evt.acl_data_packet_length};") + print(f"total_num_acl_data_packets = {evt.total_num_acl_data_packets};") + print(f"sco_data_packet_length = {evt.synchronous_data_packet_length};") + print(f"total_num_sco_data_packets = {evt.total_num_synchronous_data_packets};") await host.send_cmd(hci.ReadNumberOfSupportedIac()) evt = await host.expect_evt(hci.ReadNumberOfSupportedIacComplete) - print(f"num_supported_iac: {evt.num_support_iac}") + print(f"num_supported_iac = {evt.num_support_iac};") async def le_properties(host: Host): - await host.send_cmd(hci.LeReadLocalSupportedFeatures()) - evt = await host.expect_evt(hci.LeReadLocalSupportedFeaturesComplete) + await host.send_cmd(hci.LeReadLocalSupportedFeaturesPage0()) + evt = await host.expect_evt(hci.LeReadLocalSupportedFeaturesPage0Complete) - print(f"le_features: 0x{evt.le_features:x}") + print(f"le_features = 0x{evt.le_features:x};") - await host.send_cmd(hci.LeReadBufferSizeV2()) - evt = await host.expect_evt(hci.LeReadBufferSizeV2Complete) + try: + await host.send_cmd(hci.LeReadBufferSizeV2()) + evt = await host.expect_evt(hci.LeReadBufferSizeV2Complete) + + print(f"le_acl_data_packet_length = {evt.le_buffer_size.le_data_packet_length};") + print(f"total_num_le_acl_data_packets = {evt.le_buffer_size.total_num_le_packets};") + print(f"iso_data_packet_length = {evt.iso_buffer_size.le_data_packet_length};") + print(f"total_num_iso_data_packets = {evt.iso_buffer_size.total_num_le_packets};") + + except Exception: + await host.send_cmd(hci.LeReadBufferSizeV1()) + evt = await host.expect_evt(hci.LeReadBufferSizeV1Complete) - print(f"le_acl_data_packet_length: {evt.le_buffer_size.le_data_packet_length}") - print(f"total_num_le_acl_data_packets: {evt.le_buffer_size.total_num_le_packets}") - print(f"iso_data_packet_length: {evt.iso_buffer_size.le_data_packet_length}") - print(f"total_num_iso_data_packets: {evt.iso_buffer_size.total_num_le_packets}") + print(f"le_acl_data_packet_length = {evt.le_buffer_size.le_data_packet_length};") + print(f"total_num_le_acl_data_packets = {evt.le_buffer_size.total_num_le_packets};") await host.send_cmd(hci.LeReadFilterAcceptListSize()) evt = await host.expect_evt(hci.LeReadFilterAcceptListSizeComplete) - print(f"le_filter_accept_list_size: {evt.filter_accept_list_size}") + print(f"le_filter_accept_list_size = {evt.filter_accept_list_size};") await host.send_cmd(hci.LeReadResolvingListSize()) evt = await host.expect_evt(hci.LeReadResolvingListSizeComplete) - print(f"le_resolving_list_size: {evt.resolving_list_size}") + print(f"le_resolving_list_size = {evt.resolving_list_size};") await host.send_cmd(hci.LeReadSupportedStates()) evt = await host.expect_evt(hci.LeReadSupportedStatesComplete) - print(f"le_supported_states: 0x{evt.le_states:x}") + print(f"le_supported_states: 0x{evt.le_states:x};") await host.send_cmd(hci.LeReadMaximumAdvertisingDataLength()) evt = await host.expect_evt(hci.LeReadMaximumAdvertisingDataLengthComplete) - print(f"le_max_advertising_data_length: {evt.maximum_advertising_data_length}") + print(f"le_max_advertising_data_length = {evt.maximum_advertising_data_length};") await host.send_cmd(hci.LeReadNumberOfSupportedAdvertisingSets()) evt = await host.expect_evt(hci.LeReadNumberOfSupportedAdvertisingSetsComplete) - print(f"le_num_supported_advertising_sets: {evt.number_supported_advertising_sets}") + print(f"le_num_supported_advertising_sets = {evt.number_supported_advertising_sets};") await host.send_cmd(hci.LeReadPeriodicAdvertiserListSize()) evt = await host.expect_evt(hci.LeReadPeriodicAdvertiserListSizeComplete) - print(f"le_periodic_advertiser_list_size: {evt.periodic_advertiser_list_size}") + print(f"le_periodic_advertiser_list_size = {evt.periodic_advertiser_list_size};") async def run(tcp_port: int): @@ -170,16 +178,16 @@ async def run(tcp_port: int): await host.send_cmd(hci.ReadLocalVersionInformation()) evt = await host.expect_evt(hci.ReadLocalVersionInformationComplete) - print(f"hci_version: {evt.local_version_information.hci_version}") - print(f"hci_subversion: 0x{evt.local_version_information.hci_revision:x}") - print(f"lmp_version: {evt.local_version_information.lmp_version}") - print(f"lmp_subversion: 0x{evt.local_version_information.lmp_subversion:x}") - print(f"company_identifier: 0x{evt.local_version_information.manufacturer_name:x}") + print(f"hci_version = {evt.local_version_information.hci_version};") + print(f"hci_subversion = 0x{evt.local_version_information.hci_revision:x};") + print(f"lmp_version = {evt.local_version_information.lmp_version};") + print(f"lmp_subversion = 0x{evt.local_version_information.lmp_subversion:x};") + print(f"company_identifier = 0x{evt.local_version_information.manufacturer_name:x};") await host.send_cmd(hci.ReadLocalSupportedCommands()) evt = await host.expect_evt(hci.ReadLocalSupportedCommandsComplete) - print(f"supported_commands: {{ {', '.join([f'0x{b:x}' for b in evt.supported_commands])} }}") + print(f"supported_commands = {{ {', '.join([f'0x{b:02x}' for b in evt.supported_commands])} }};") try: await br_edr_properties(host) |