munim-bluetooth 0.3.24 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/com/munimbluetooth/HybridMunimBluetooth.kt +87 -0
- package/android/src/main/java/com/munimbluetooth/MunimBluetoothBackgroundService.kt +344 -0
- package/ios/HybridMunimBluetooth.swift +264 -22
- package/lib/commonjs/index.js +21 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +15 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts +25 -3
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JBackgroundSessionOptions.hpp +107 -0
- package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.cpp +12 -0
- package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BackgroundSessionOptions.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/HybridMunimBluetoothSpec.kt +28 -20
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridMunimBluetoothSpecSwift.hpp +15 -0
- package/nitrogen/generated/ios/swift/BackgroundSessionOptions.swift +154 -0
- package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec_cxx.swift +43 -21
- package/nitrogen/generated/shared/c++/BackgroundSessionOptions.hpp +115 -0
- package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.hpp +5 -0
- package/package.json +1 -1
- package/src/index.ts +23 -0
- package/src/specs/munim-bluetooth.nitro.ts +28 -3
|
@@ -13,8 +13,18 @@
|
|
|
13
13
|
<!-- Location permissions required for BLE scanning on Android 6.0+ -->
|
|
14
14
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
15
15
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
16
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
17
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
|
16
18
|
|
|
17
19
|
<!-- Feature declarations -->
|
|
18
20
|
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
|
|
19
21
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
|
22
|
+
|
|
23
|
+
<application>
|
|
24
|
+
<service
|
|
25
|
+
android:name=".MunimBluetoothBackgroundService"
|
|
26
|
+
android:enabled="true"
|
|
27
|
+
android:exported="false"
|
|
28
|
+
android:foregroundServiceType="location" />
|
|
29
|
+
</application>
|
|
20
30
|
</manifest>
|
|
@@ -22,6 +22,7 @@ import android.bluetooth.le.ScanRecord
|
|
|
22
22
|
import android.bluetooth.le.ScanResult
|
|
23
23
|
import android.bluetooth.le.ScanSettings
|
|
24
24
|
import android.content.Context
|
|
25
|
+
import android.content.Intent
|
|
25
26
|
import android.os.Build
|
|
26
27
|
import android.os.ParcelUuid
|
|
27
28
|
import android.util.Log
|
|
@@ -33,6 +34,7 @@ import com.margelo.nitro.NitroModules
|
|
|
33
34
|
import com.margelo.nitro.core.Promise
|
|
34
35
|
import com.margelo.nitro.munimbluetooth.AdvertisingDataTypes
|
|
35
36
|
import com.margelo.nitro.munimbluetooth.AdvertisingOptions
|
|
37
|
+
import com.margelo.nitro.munimbluetooth.BackgroundSessionOptions
|
|
36
38
|
import com.margelo.nitro.munimbluetooth.CharacteristicValue
|
|
37
39
|
import com.margelo.nitro.munimbluetooth.GATTCharacteristic
|
|
38
40
|
import com.margelo.nitro.munimbluetooth.GATTService
|
|
@@ -61,6 +63,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
61
63
|
private var currentServiceUUIDs: Array<String> = emptyArray()
|
|
62
64
|
private var currentLocalName: String? = null
|
|
63
65
|
private var currentManufacturerData: String? = null
|
|
66
|
+
private var previousAdapterName: String? = null
|
|
64
67
|
private var bluetoothManager: BluetoothManager? = null
|
|
65
68
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
66
69
|
|
|
@@ -111,6 +114,17 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
111
114
|
options.manufacturerData
|
|
112
115
|
)
|
|
113
116
|
|
|
117
|
+
if (!currentLocalName.isNullOrBlank() && previousAdapterName == null) {
|
|
118
|
+
previousAdapterName = adapter.name
|
|
119
|
+
}
|
|
120
|
+
if (!currentLocalName.isNullOrBlank()) {
|
|
121
|
+
try {
|
|
122
|
+
adapter.name = currentLocalName
|
|
123
|
+
} catch (error: SecurityException) {
|
|
124
|
+
Log.w(TAG, "Unable to apply custom localName to Bluetooth adapter", error)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
114
128
|
if (!gattServerReady) {
|
|
115
129
|
setServicesFromOptions(options.serviceUUIDs)
|
|
116
130
|
}
|
|
@@ -143,6 +157,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
143
157
|
currentServiceUUIDs = emptyArray()
|
|
144
158
|
currentLocalName = null
|
|
145
159
|
currentManufacturerData = null
|
|
160
|
+
restoreAdapterName()
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
override fun setServices(services: Array<GATTService>) {
|
|
@@ -432,6 +447,67 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
432
447
|
return promise
|
|
433
448
|
}
|
|
434
449
|
|
|
450
|
+
override fun startBackgroundSession(options: BackgroundSessionOptions) {
|
|
451
|
+
val context = NitroModules.applicationContext ?: run {
|
|
452
|
+
Log.w(TAG, "Unable to start background BLE session: application context unavailable")
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
val intent = Intent(context, MunimBluetoothBackgroundService::class.java).apply {
|
|
457
|
+
action = MunimBluetoothBackgroundService.ACTION_START
|
|
458
|
+
putExtra(
|
|
459
|
+
MunimBluetoothBackgroundService.EXTRA_SERVICE_UUIDS,
|
|
460
|
+
options.serviceUUIDs
|
|
461
|
+
)
|
|
462
|
+
putExtra(
|
|
463
|
+
MunimBluetoothBackgroundService.EXTRA_LOCAL_NAME,
|
|
464
|
+
options.localName
|
|
465
|
+
)
|
|
466
|
+
putExtra(
|
|
467
|
+
MunimBluetoothBackgroundService.EXTRA_ALLOW_DUPLICATES,
|
|
468
|
+
options.allowDuplicates ?: false
|
|
469
|
+
)
|
|
470
|
+
putExtra(
|
|
471
|
+
MunimBluetoothBackgroundService.EXTRA_SCAN_MODE,
|
|
472
|
+
options.scanMode?.name ?: ScanMode.LOWPOWER.name
|
|
473
|
+
)
|
|
474
|
+
putExtra(
|
|
475
|
+
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_CHANNEL_ID,
|
|
476
|
+
options.androidNotificationChannelId
|
|
477
|
+
?: MunimBluetoothBackgroundService.DEFAULT_CHANNEL_ID
|
|
478
|
+
)
|
|
479
|
+
putExtra(
|
|
480
|
+
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_CHANNEL_NAME,
|
|
481
|
+
options.androidNotificationChannelName
|
|
482
|
+
?: MunimBluetoothBackgroundService.DEFAULT_CHANNEL_NAME
|
|
483
|
+
)
|
|
484
|
+
putExtra(
|
|
485
|
+
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_TITLE,
|
|
486
|
+
options.androidNotificationTitle
|
|
487
|
+
?: MunimBluetoothBackgroundService.DEFAULT_NOTIFICATION_TITLE
|
|
488
|
+
)
|
|
489
|
+
putExtra(
|
|
490
|
+
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_TEXT,
|
|
491
|
+
options.androidNotificationText
|
|
492
|
+
?: MunimBluetoothBackgroundService.DEFAULT_NOTIFICATION_TEXT
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
497
|
+
context.startForegroundService(intent)
|
|
498
|
+
} else {
|
|
499
|
+
context.startService(intent)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
override fun stopBackgroundSession() {
|
|
504
|
+
val context = NitroModules.applicationContext ?: return
|
|
505
|
+
val intent = Intent(context, MunimBluetoothBackgroundService::class.java).apply {
|
|
506
|
+
action = MunimBluetoothBackgroundService.ACTION_STOP
|
|
507
|
+
}
|
|
508
|
+
context.startService(intent)
|
|
509
|
+
}
|
|
510
|
+
|
|
435
511
|
override fun addListener(eventName: String) {
|
|
436
512
|
// Nitro uses JS-side listener registration. No native bookkeeping required here.
|
|
437
513
|
}
|
|
@@ -938,6 +1014,17 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
938
1014
|
gattServerReady = true
|
|
939
1015
|
}
|
|
940
1016
|
|
|
1017
|
+
private fun restoreAdapterName() {
|
|
1018
|
+
val adapter = bluetoothAdapter ?: return
|
|
1019
|
+
val originalName = previousAdapterName ?: return
|
|
1020
|
+
try {
|
|
1021
|
+
adapter.name = originalName
|
|
1022
|
+
} catch (error: SecurityException) {
|
|
1023
|
+
Log.w(TAG, "Unable to restore Bluetooth adapter name", error)
|
|
1024
|
+
}
|
|
1025
|
+
previousAdapterName = null
|
|
1026
|
+
}
|
|
1027
|
+
|
|
941
1028
|
companion object {
|
|
942
1029
|
private const val TAG = "HybridMunimBluetooth"
|
|
943
1030
|
private val CLIENT_CHARACTERISTIC_CONFIG_UUID =
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
package com.munimbluetooth
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.Service
|
|
7
|
+
import android.bluetooth.BluetoothAdapter
|
|
8
|
+
import android.bluetooth.BluetoothManager
|
|
9
|
+
import android.bluetooth.le.AdvertiseCallback
|
|
10
|
+
import android.bluetooth.le.AdvertiseData
|
|
11
|
+
import android.bluetooth.le.AdvertiseSettings
|
|
12
|
+
import android.bluetooth.le.BluetoothLeAdvertiser
|
|
13
|
+
import android.bluetooth.le.BluetoothLeScanner
|
|
14
|
+
import android.bluetooth.le.ScanCallback
|
|
15
|
+
import android.bluetooth.le.ScanFilter
|
|
16
|
+
import android.bluetooth.le.ScanResult
|
|
17
|
+
import android.bluetooth.le.ScanSettings
|
|
18
|
+
import android.content.Context
|
|
19
|
+
import android.content.Intent
|
|
20
|
+
import android.os.Build
|
|
21
|
+
import android.os.IBinder
|
|
22
|
+
import android.os.ParcelUuid
|
|
23
|
+
import android.util.Log
|
|
24
|
+
import com.margelo.nitro.munimbluetooth.ScanMode
|
|
25
|
+
import java.util.Locale
|
|
26
|
+
|
|
27
|
+
class MunimBluetoothBackgroundService : Service() {
|
|
28
|
+
private var bluetoothManager: BluetoothManager? = null
|
|
29
|
+
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
30
|
+
private var advertiser: BluetoothLeAdvertiser? = null
|
|
31
|
+
private var advertiseCallback: AdvertiseCallback? = null
|
|
32
|
+
private var scanner: BluetoothLeScanner? = null
|
|
33
|
+
private var scanCallback: ScanCallback? = null
|
|
34
|
+
private var previousAdapterName: String? = null
|
|
35
|
+
|
|
36
|
+
private val discoveredDeviceIds = linkedSetOf<String>()
|
|
37
|
+
private var notificationChannelId = DEFAULT_CHANNEL_ID
|
|
38
|
+
private var notificationChannelName = DEFAULT_CHANNEL_NAME
|
|
39
|
+
private var notificationTitle = DEFAULT_NOTIFICATION_TITLE
|
|
40
|
+
private var notificationText = DEFAULT_NOTIFICATION_TEXT
|
|
41
|
+
|
|
42
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
43
|
+
|
|
44
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
45
|
+
when (intent?.action) {
|
|
46
|
+
ACTION_STOP -> {
|
|
47
|
+
stopSelf()
|
|
48
|
+
return START_NOT_STICKY
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ACTION_START -> {
|
|
52
|
+
notificationChannelId =
|
|
53
|
+
intent.getStringExtra(EXTRA_NOTIFICATION_CHANNEL_ID)
|
|
54
|
+
?: DEFAULT_CHANNEL_ID
|
|
55
|
+
notificationChannelName =
|
|
56
|
+
intent.getStringExtra(EXTRA_NOTIFICATION_CHANNEL_NAME)
|
|
57
|
+
?: DEFAULT_CHANNEL_NAME
|
|
58
|
+
notificationTitle =
|
|
59
|
+
intent.getStringExtra(EXTRA_NOTIFICATION_TITLE)
|
|
60
|
+
?: DEFAULT_NOTIFICATION_TITLE
|
|
61
|
+
notificationText =
|
|
62
|
+
intent.getStringExtra(EXTRA_NOTIFICATION_TEXT)
|
|
63
|
+
?: DEFAULT_NOTIFICATION_TEXT
|
|
64
|
+
|
|
65
|
+
startForeground(
|
|
66
|
+
NOTIFICATION_ID,
|
|
67
|
+
buildNotification(neighborCount = discoveredDeviceIds.size)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
val serviceUUIDs =
|
|
71
|
+
intent.getStringArrayExtra(EXTRA_SERVICE_UUIDS) ?: emptyArray()
|
|
72
|
+
val localName = intent.getStringExtra(EXTRA_LOCAL_NAME)
|
|
73
|
+
val allowDuplicates =
|
|
74
|
+
intent.getBooleanExtra(EXTRA_ALLOW_DUPLICATES, false)
|
|
75
|
+
val scanMode = parseScanMode(
|
|
76
|
+
intent.getStringExtra(EXTRA_SCAN_MODE) ?: ScanMode.LOWPOWER.name
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
startBleSession(
|
|
80
|
+
serviceUUIDs = serviceUUIDs,
|
|
81
|
+
localName = localName,
|
|
82
|
+
allowDuplicates = allowDuplicates,
|
|
83
|
+
scanMode = scanMode
|
|
84
|
+
)
|
|
85
|
+
return START_STICKY
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return START_NOT_STICKY
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override fun onDestroy() {
|
|
93
|
+
stopBleSession()
|
|
94
|
+
super.onDestroy()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun startBleSession(
|
|
98
|
+
serviceUUIDs: Array<String>,
|
|
99
|
+
localName: String?,
|
|
100
|
+
allowDuplicates: Boolean,
|
|
101
|
+
scanMode: ScanMode
|
|
102
|
+
) {
|
|
103
|
+
bluetoothManager =
|
|
104
|
+
applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
105
|
+
bluetoothAdapter = bluetoothManager?.adapter
|
|
106
|
+
|
|
107
|
+
val adapter = bluetoothAdapter
|
|
108
|
+
if (adapter == null || !adapter.isEnabled) {
|
|
109
|
+
Log.w(TAG, "Unable to start background BLE session: Bluetooth unavailable")
|
|
110
|
+
stopSelf()
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!localName.isNullOrBlank() && previousAdapterName == null) {
|
|
115
|
+
previousAdapterName = adapter.name
|
|
116
|
+
try {
|
|
117
|
+
adapter.name = localName
|
|
118
|
+
} catch (error: SecurityException) {
|
|
119
|
+
Log.w(TAG, "Unable to set adapter name for background advertising", error)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
startAdvertising(adapter, serviceUUIDs, !localName.isNullOrBlank())
|
|
124
|
+
startScan(adapter, serviceUUIDs, allowDuplicates, scanMode)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private fun stopBleSession() {
|
|
128
|
+
scanCallback?.let { callback ->
|
|
129
|
+
try {
|
|
130
|
+
scanner?.stopScan(callback)
|
|
131
|
+
} catch (error: SecurityException) {
|
|
132
|
+
Log.w(TAG, "Unable to stop background scan cleanly", error)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
scanCallback = null
|
|
136
|
+
scanner = null
|
|
137
|
+
|
|
138
|
+
advertiseCallback?.let { callback ->
|
|
139
|
+
try {
|
|
140
|
+
advertiser?.stopAdvertising(callback)
|
|
141
|
+
} catch (error: SecurityException) {
|
|
142
|
+
Log.w(TAG, "Unable to stop background advertising cleanly", error)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
advertiseCallback = null
|
|
146
|
+
advertiser = null
|
|
147
|
+
|
|
148
|
+
val adapter = bluetoothAdapter
|
|
149
|
+
val previousName = previousAdapterName
|
|
150
|
+
if (adapter != null && previousName != null) {
|
|
151
|
+
try {
|
|
152
|
+
adapter.name = previousName
|
|
153
|
+
} catch (error: SecurityException) {
|
|
154
|
+
Log.w(TAG, "Unable to restore adapter name", error)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
previousAdapterName = null
|
|
158
|
+
discoveredDeviceIds.clear()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fun startAdvertising(
|
|
162
|
+
adapter: BluetoothAdapter,
|
|
163
|
+
serviceUUIDs: Array<String>,
|
|
164
|
+
includeDeviceName: Boolean
|
|
165
|
+
) {
|
|
166
|
+
val activeAdvertiser = adapter.bluetoothLeAdvertiser ?: run {
|
|
167
|
+
Log.w(TAG, "Bluetooth advertiser unavailable for background session")
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
advertiser = activeAdvertiser
|
|
171
|
+
|
|
172
|
+
val data = AdvertiseData.Builder()
|
|
173
|
+
.setIncludeDeviceName(includeDeviceName)
|
|
174
|
+
|
|
175
|
+
serviceUUIDs.forEach { uuid ->
|
|
176
|
+
runCatching { ParcelUuid.fromString(uuid) }.getOrNull()?.let(data::addServiceUuid)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
val settings = AdvertiseSettings.Builder()
|
|
180
|
+
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER)
|
|
181
|
+
.setConnectable(true)
|
|
182
|
+
.setTimeout(0)
|
|
183
|
+
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
|
|
184
|
+
.build()
|
|
185
|
+
|
|
186
|
+
advertiseCallback = object : AdvertiseCallback() {
|
|
187
|
+
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
|
188
|
+
Log.i(TAG, "Background advertising started")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
override fun onStartFailure(errorCode: Int) {
|
|
192
|
+
Log.e(TAG, "Background advertising failed: $errorCode")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
activeAdvertiser.startAdvertising(settings, data.build(), advertiseCallback)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private fun startScan(
|
|
200
|
+
adapter: BluetoothAdapter,
|
|
201
|
+
serviceUUIDs: Array<String>,
|
|
202
|
+
allowDuplicates: Boolean,
|
|
203
|
+
scanMode: ScanMode
|
|
204
|
+
) {
|
|
205
|
+
val activeScanner = adapter.bluetoothLeScanner ?: run {
|
|
206
|
+
Log.w(TAG, "Bluetooth scanner unavailable for background session")
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
scanner = activeScanner
|
|
210
|
+
|
|
211
|
+
val filters = serviceUUIDs.mapNotNull { uuid ->
|
|
212
|
+
runCatching {
|
|
213
|
+
ScanFilter.Builder()
|
|
214
|
+
.setServiceUuid(ParcelUuid.fromString(uuid))
|
|
215
|
+
.build()
|
|
216
|
+
}.getOrNull()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
val androidScanMode = when (scanMode) {
|
|
220
|
+
ScanMode.LOWLATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY
|
|
221
|
+
ScanMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED
|
|
222
|
+
ScanMode.LOWPOWER -> ScanSettings.SCAN_MODE_LOW_POWER
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
val settingsBuilder = ScanSettings.Builder().setScanMode(androidScanMode)
|
|
226
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
227
|
+
settingsBuilder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
|
|
228
|
+
settingsBuilder.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
scanCallback = object : ScanCallback() {
|
|
232
|
+
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
233
|
+
handleScanResult(result, allowDuplicates)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
|
237
|
+
results.forEach { handleScanResult(it, allowDuplicates) }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
override fun onScanFailed(errorCode: Int) {
|
|
241
|
+
Log.e(TAG, "Background scan failed: $errorCode")
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
activeScanner.startScan(filters, settingsBuilder.build(), scanCallback)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private fun handleScanResult(result: ScanResult, allowDuplicates: Boolean) {
|
|
249
|
+
val deviceId = result.device.address ?: return
|
|
250
|
+
if (!allowDuplicates && !discoveredDeviceIds.add(deviceId)) {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
if (allowDuplicates) {
|
|
254
|
+
discoveredDeviceIds.add(deviceId)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
updateForegroundNotification()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private fun updateForegroundNotification() {
|
|
261
|
+
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
|
262
|
+
manager?.notify(
|
|
263
|
+
NOTIFICATION_ID,
|
|
264
|
+
buildNotification(neighborCount = discoveredDeviceIds.size)
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private fun buildNotification(neighborCount: Int): Notification {
|
|
269
|
+
ensureNotificationChannel()
|
|
270
|
+
|
|
271
|
+
val text = if (neighborCount <= 0) {
|
|
272
|
+
notificationText
|
|
273
|
+
} else {
|
|
274
|
+
String.format(
|
|
275
|
+
Locale.US,
|
|
276
|
+
"%s (%d nearby)",
|
|
277
|
+
notificationText,
|
|
278
|
+
neighborCount
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
283
|
+
Notification.Builder(this, notificationChannelId)
|
|
284
|
+
} else {
|
|
285
|
+
Notification.Builder(this)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return builder
|
|
289
|
+
.setContentTitle(notificationTitle)
|
|
290
|
+
.setContentText(text)
|
|
291
|
+
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
|
|
292
|
+
.setOngoing(true)
|
|
293
|
+
.setOnlyAlertOnce(true)
|
|
294
|
+
.setForegroundServiceBehaviorCompat()
|
|
295
|
+
.build()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private fun Notification.Builder.setForegroundServiceBehaviorCompat(): Notification.Builder {
|
|
299
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
300
|
+
setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
|
|
301
|
+
}
|
|
302
|
+
return this
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private fun ensureNotificationChannel() {
|
|
306
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
|
311
|
+
val channel = NotificationChannel(
|
|
312
|
+
notificationChannelId,
|
|
313
|
+
notificationChannelName,
|
|
314
|
+
NotificationManager.IMPORTANCE_LOW
|
|
315
|
+
)
|
|
316
|
+
manager?.createNotificationChannel(channel)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private fun parseScanMode(rawValue: String): ScanMode {
|
|
320
|
+
return runCatching { ScanMode.valueOf(rawValue) }.getOrElse { ScanMode.LOWPOWER }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
companion object {
|
|
324
|
+
const val ACTION_START = "com.munimbluetooth.action.START_BACKGROUND_SESSION"
|
|
325
|
+
const val ACTION_STOP = "com.munimbluetooth.action.STOP_BACKGROUND_SESSION"
|
|
326
|
+
|
|
327
|
+
const val EXTRA_SERVICE_UUIDS = "serviceUUIDs"
|
|
328
|
+
const val EXTRA_LOCAL_NAME = "localName"
|
|
329
|
+
const val EXTRA_ALLOW_DUPLICATES = "allowDuplicates"
|
|
330
|
+
const val EXTRA_SCAN_MODE = "scanMode"
|
|
331
|
+
const val EXTRA_NOTIFICATION_CHANNEL_ID = "notificationChannelId"
|
|
332
|
+
const val EXTRA_NOTIFICATION_CHANNEL_NAME = "notificationChannelName"
|
|
333
|
+
const val EXTRA_NOTIFICATION_TITLE = "notificationTitle"
|
|
334
|
+
const val EXTRA_NOTIFICATION_TEXT = "notificationText"
|
|
335
|
+
|
|
336
|
+
const val DEFAULT_CHANNEL_ID = "munim-bluetooth-background"
|
|
337
|
+
const val DEFAULT_CHANNEL_NAME = "Bluetooth background session"
|
|
338
|
+
const val DEFAULT_NOTIFICATION_TITLE = "Bluetooth nearby mode"
|
|
339
|
+
const val DEFAULT_NOTIFICATION_TEXT = "Scanning for nearby Bluetooth devices"
|
|
340
|
+
|
|
341
|
+
private const val NOTIFICATION_ID = 48231
|
|
342
|
+
private const val TAG = "MunimBluetoothBgSvc"
|
|
343
|
+
}
|
|
344
|
+
}
|