Add Android scan mode setting (#212)

* Add Android scan mode setting

* Format pigeon generated code

* format whole project

* Fix windows compilation because of generated code order

* Fix unwanted imports

* Add doc comment for AndroidOptions

* Update code doc
This commit is contained in:
Rohit Sangwan
2026-01-21 15:32:40 +05:30
committed by GitHub
parent dae2f5dc4d
commit 2f7f1f693d
24 changed files with 744 additions and 108 deletions
@@ -96,6 +96,20 @@ enum class UniversalBleLogLevel(val raw: Int) {
}
}
/** Scan config */
enum class AndroidScanMode(val raw: Int) {
BALANCED(0),
LOW_LATENCY(1),
LOW_POWER(2),
OPPORTUNISTIC(3);
companion object {
fun ofRaw(raw: Int): AndroidScanMode? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Unified error codes for all platforms */
enum class UniversalBleErrorCode(val raw: Int) {
UNKNOWN_ERROR(0),
@@ -309,6 +323,77 @@ data class UniversalBleDescriptor (
override fun hashCode(): Int = toList().hashCode()
}
/**
* Android options to scan devices
* [requestLocationPermission] is used to request location permission on Android 12+ (API 31+).
* [scanMode] is used to set the scan mode for for Bluetooth LE scan.
* Set [reportDelayMillis] timestamp for Bluetooth LE scan. If set to 0, you will be notified of scan results immediately.
* If > 0, scan results are queued up and delivered after the requested delay or 5000 milliseconds (whichever is higher).
* Note scan results may be delivered sooner if the internal buffers fill up.
*
* Generated class from Pigeon that represents data sent in messages.
*/
data class AndroidOptions (
val requestLocationPermission: Boolean? = null,
val scanMode: AndroidScanMode? = null,
val reportDelayMillis: Long? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): AndroidOptions {
val requestLocationPermission = pigeonVar_list[0] as Boolean?
val scanMode = pigeonVar_list[1] as AndroidScanMode?
val reportDelayMillis = pigeonVar_list[2] as Long?
return AndroidOptions(requestLocationPermission, scanMode, reportDelayMillis)
}
}
fun toList(): List<Any?> {
return listOf(
requestLocationPermission,
scanMode,
reportDelayMillis,
)
}
override fun equals(other: Any?): Boolean {
if (other !is AndroidOptions) {
return false
}
if (this === other) {
return true
}
return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
/** Generated class from Pigeon that represents data sent in messages. */
data class UniversalScanConfig (
val android: AndroidOptions? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): UniversalScanConfig {
val android = pigeonVar_list[0] as AndroidOptions?
return UniversalScanConfig(android)
}
}
fun toList(): List<Any?> {
return listOf(
android,
)
}
override fun equals(other: Any?): Boolean {
if (other !is UniversalScanConfig) {
return false
}
if (this === other) {
return true
}
return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
/**
* Scan Filters
*
@@ -421,40 +506,55 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() {
}
130.toByte() -> {
return (readValue(buffer) as Long?)?.let {
UniversalBleErrorCode.ofRaw(it.toInt())
AndroidScanMode.ofRaw(it.toInt())
}
}
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalBleScanResult.fromList(it)
return (readValue(buffer) as Long?)?.let {
UniversalBleErrorCode.ofRaw(it.toInt())
}
}
132.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalBleService.fromList(it)
UniversalBleScanResult.fromList(it)
}
}
133.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalBleCharacteristic.fromList(it)
UniversalBleService.fromList(it)
}
}
134.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalBleDescriptor.fromList(it)
UniversalBleCharacteristic.fromList(it)
}
}
135.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalScanFilter.fromList(it)
UniversalBleDescriptor.fromList(it)
}
}
136.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalManufacturerDataFilter.fromList(it)
AndroidOptions.fromList(it)
}
}
137.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalScanConfig.fromList(it)
}
}
138.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalScanFilter.fromList(it)
}
}
139.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalManufacturerDataFilter.fromList(it)
}
}
140.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
UniversalManufacturerData.fromList(it)
}
@@ -468,38 +568,50 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() {
stream.write(129)
writeValue(stream, value.raw.toLong())
}
is UniversalBleErrorCode -> {
is AndroidScanMode -> {
stream.write(130)
writeValue(stream, value.raw.toLong())
}
is UniversalBleScanResult -> {
is UniversalBleErrorCode -> {
stream.write(131)
writeValue(stream, value.toList())
writeValue(stream, value.raw.toLong())
}
is UniversalBleService -> {
is UniversalBleScanResult -> {
stream.write(132)
writeValue(stream, value.toList())
}
is UniversalBleCharacteristic -> {
is UniversalBleService -> {
stream.write(133)
writeValue(stream, value.toList())
}
is UniversalBleDescriptor -> {
is UniversalBleCharacteristic -> {
stream.write(134)
writeValue(stream, value.toList())
}
is UniversalScanFilter -> {
is UniversalBleDescriptor -> {
stream.write(135)
writeValue(stream, value.toList())
}
is UniversalManufacturerDataFilter -> {
is AndroidOptions -> {
stream.write(136)
writeValue(stream, value.toList())
}
is UniversalManufacturerData -> {
is UniversalScanConfig -> {
stream.write(137)
writeValue(stream, value.toList())
}
is UniversalScanFilter -> {
stream.write(138)
writeValue(stream, value.toList())
}
is UniversalManufacturerDataFilter -> {
stream.write(139)
writeValue(stream, value.toList())
}
is UniversalManufacturerData -> {
stream.write(140)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
@@ -517,7 +629,7 @@ interface UniversalBlePlatformChannel {
fun requestPermissions(withAndroidFineLocation: Boolean, callback: (Result<Unit>) -> Unit)
fun enableBluetooth(callback: (Result<Boolean>) -> Unit)
fun disableBluetooth(callback: (Result<Boolean>) -> Unit)
fun startScan(filter: UniversalScanFilter?)
fun startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?)
fun stopScan()
fun isScanning(): Boolean
fun connect(deviceId: String, autoConnect: Boolean?)
@@ -640,8 +752,9 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val filterArg = args[0] as UniversalScanFilter?
val configArg = args[1] as UniversalScanConfig?
val wrapped: List<Any?> = try {
api.startScan(filterArg)
api.startScan(filterArg, configArg)
listOf(null)
} catch (exception: Throwable) {
UniversalBlePigeonUtils.wrapError(exception)
@@ -12,6 +12,8 @@ import android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR
import android.bluetooth.le.ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES
import android.bluetooth.le.ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.os.Build
import android.util.Log
import android.util.SparseArray
import java.nio.ByteBuffer
@@ -224,6 +226,20 @@ fun gattStatusToUniversalBleErrorCode(code: Int): UniversalBleErrorCode {
}
}
fun AndroidScanMode.parse(): Int? {
return when (this) {
AndroidScanMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED
AndroidScanMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY
AndroidScanMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER
AndroidScanMode.OPPORTUNISTIC -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ScanSettings.SCAN_MODE_OPPORTUNISTIC
} else {
UniversalBleLogger.logError("Scan mode OPPORTUNISTIC is not supported on this Android version.")
null
}
}
}
fun Int.parseBluetoothStatusCodeError(): UniversalBleErrorCode? {
if (this == BluetoothStatusCodes.SUCCESS) return null
return when (this) {
@@ -160,7 +160,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
bluetoothDisableRequestFuture = callback
}
override fun startScan(filter: UniversalScanFilter?) {
override fun startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) {
if (!isBluetoothAvailable()) throw createFlutterError(
UniversalBleErrorCode.BLUETOOTH_NOT_ENABLED,
"Bluetooth not enabled"
@@ -171,6 +171,14 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
builder.setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED)
builder.setLegacy(false)
}
config?.android?.let { androidConfig ->
androidConfig.scanMode?.parse()?.let { scanMode ->
builder.setScanMode(scanMode)
}
androidConfig.reportDelayMillis?.let { reportDelay ->
builder.setReportDelay(reportDelay)
}
}
val settings = builder.build()
val usesCustomFilters = filter?.usesCustomFilters() ?: false
@@ -1151,7 +1159,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
val deviceId = gatt.device.address
val shouldAutoConnect = autoConnectDevices.contains(deviceId)
// Always clean up internal state (futures, etc.)
cleanUpConnection(deviceId)
@@ -1161,7 +1169,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
deviceId, false, status.parseHciErrorCode()
) {}
}
if (!shouldAutoConnect) {
// Only close GATT resources when autoConnect is disabled
gatt.removeCache()