@capacitor-community/bluetooth-le 8.0.0-0 → 8.0.0
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/CapacitorCommunityBluetoothLe.podspec +17 -17
- package/LICENSE +21 -21
- package/Package.swift +27 -27
- package/README.md +2 -1
- package/android/build.gradle +73 -71
- package/android/src/main/AndroidManifest.xml +22 -22
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +1094 -1094
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Conversion.kt +51 -51
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +771 -771
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt +28 -28
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceScanner.kt +189 -189
- package/ios/Sources/BluetoothLe/Conversion.swift +83 -83
- package/ios/Sources/BluetoothLe/Device.swift +423 -423
- package/ios/Sources/BluetoothLe/DeviceListView.swift +121 -121
- package/ios/Sources/BluetoothLe/DeviceManager.swift +503 -503
- package/ios/Sources/BluetoothLe/Logging.swift +8 -8
- package/ios/Sources/BluetoothLe/Plugin.swift +775 -775
- package/ios/Sources/BluetoothLe/ThreadSafeDictionary.swift +15 -15
- package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -55
- package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -27
- package/package.json +115 -115
|
@@ -1,1094 +1,1094 @@
|
|
|
1
|
-
package com.capacitorjs.community.plugins.bluetoothle
|
|
2
|
-
|
|
3
|
-
import android.Manifest
|
|
4
|
-
import android.annotation.SuppressLint
|
|
5
|
-
import android.app.Activity
|
|
6
|
-
import android.bluetooth.BluetoothAdapter
|
|
7
|
-
import android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE
|
|
8
|
-
import android.bluetooth.BluetoothDevice
|
|
9
|
-
import android.bluetooth.BluetoothGatt
|
|
10
|
-
import android.bluetooth.BluetoothGattCharacteristic
|
|
11
|
-
import android.bluetooth.BluetoothManager
|
|
12
|
-
import android.bluetooth.BluetoothProfile
|
|
13
|
-
import android.bluetooth.le.ScanFilter
|
|
14
|
-
import android.bluetooth.le.ScanResult
|
|
15
|
-
import android.bluetooth.le.ScanSettings
|
|
16
|
-
import android.content.BroadcastReceiver
|
|
17
|
-
import android.content.Context
|
|
18
|
-
import android.content.Intent
|
|
19
|
-
import android.content.IntentFilter
|
|
20
|
-
import android.content.pm.PackageManager
|
|
21
|
-
import android.location.LocationManager
|
|
22
|
-
import android.net.Uri
|
|
23
|
-
import android.os.Build
|
|
24
|
-
import android.os.ParcelUuid
|
|
25
|
-
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
|
26
|
-
import android.provider.Settings.ACTION_BLUETOOTH_SETTINGS
|
|
27
|
-
import android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS
|
|
28
|
-
import androidx.activity.result.ActivityResult
|
|
29
|
-
import androidx.core.location.LocationManagerCompat
|
|
30
|
-
import com.getcapacitor.JSArray
|
|
31
|
-
import com.getcapacitor.JSObject
|
|
32
|
-
import com.getcapacitor.Logger
|
|
33
|
-
import com.getcapacitor.PermissionState
|
|
34
|
-
import com.getcapacitor.Plugin
|
|
35
|
-
import com.getcapacitor.PluginCall
|
|
36
|
-
import com.getcapacitor.PluginMethod
|
|
37
|
-
import com.getcapacitor.annotation.ActivityCallback
|
|
38
|
-
import com.getcapacitor.annotation.CapacitorPlugin
|
|
39
|
-
import com.getcapacitor.annotation.Permission
|
|
40
|
-
import com.getcapacitor.annotation.PermissionCallback
|
|
41
|
-
import java.util.UUID
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@SuppressLint("MissingPermission")
|
|
45
|
-
@CapacitorPlugin(
|
|
46
|
-
name = "BluetoothLe",
|
|
47
|
-
permissions = [
|
|
48
|
-
Permission(
|
|
49
|
-
strings = [
|
|
50
|
-
Manifest.permission.ACCESS_COARSE_LOCATION,
|
|
51
|
-
], alias = "ACCESS_COARSE_LOCATION"
|
|
52
|
-
),
|
|
53
|
-
Permission(
|
|
54
|
-
strings = [
|
|
55
|
-
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
56
|
-
], alias = "ACCESS_FINE_LOCATION"
|
|
57
|
-
),
|
|
58
|
-
Permission(
|
|
59
|
-
strings = [
|
|
60
|
-
Manifest.permission.BLUETOOTH,
|
|
61
|
-
], alias = "BLUETOOTH"
|
|
62
|
-
),
|
|
63
|
-
Permission(
|
|
64
|
-
strings = [
|
|
65
|
-
Manifest.permission.BLUETOOTH_ADMIN,
|
|
66
|
-
], alias = "BLUETOOTH_ADMIN"
|
|
67
|
-
),
|
|
68
|
-
Permission(
|
|
69
|
-
strings = [
|
|
70
|
-
// Manifest.permission.BLUETOOTH_SCAN
|
|
71
|
-
"android.permission.BLUETOOTH_SCAN",
|
|
72
|
-
], alias = "BLUETOOTH_SCAN"
|
|
73
|
-
),
|
|
74
|
-
Permission(
|
|
75
|
-
strings = [
|
|
76
|
-
// Manifest.permission.BLUETOOTH_ADMIN
|
|
77
|
-
"android.permission.BLUETOOTH_CONNECT",
|
|
78
|
-
], alias = "BLUETOOTH_CONNECT"
|
|
79
|
-
),
|
|
80
|
-
]
|
|
81
|
-
)
|
|
82
|
-
class BluetoothLe : Plugin() {
|
|
83
|
-
companion object {
|
|
84
|
-
private val TAG = BluetoothLe::class.java.simpleName
|
|
85
|
-
|
|
86
|
-
// maximal scan duration for requestDevice
|
|
87
|
-
private const val MAX_SCAN_DURATION: Long = 30000
|
|
88
|
-
private const val CONNECTION_TIMEOUT: Float = 10000.0F
|
|
89
|
-
private const val DEFAULT_TIMEOUT: Float = 5000.0F
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
93
|
-
private var stateReceiver: BroadcastReceiver? = null
|
|
94
|
-
private var deviceMap = HashMap<String, Device>()
|
|
95
|
-
private var deviceScanner: DeviceScanner? = null
|
|
96
|
-
private var displayStrings: DisplayStrings? = null
|
|
97
|
-
private var aliases: Array<String> = arrayOf()
|
|
98
|
-
|
|
99
|
-
override fun load() {
|
|
100
|
-
displayStrings = getDisplayStrings()
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
@PluginMethod
|
|
104
|
-
fun initialize(call: PluginCall) {
|
|
105
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
106
|
-
val neverForLocation = call.getBoolean("androidNeverForLocation", false) as Boolean
|
|
107
|
-
aliases = if (neverForLocation) {
|
|
108
|
-
arrayOf(
|
|
109
|
-
"BLUETOOTH_SCAN",
|
|
110
|
-
"BLUETOOTH_CONNECT",
|
|
111
|
-
)
|
|
112
|
-
} else {
|
|
113
|
-
arrayOf(
|
|
114
|
-
"BLUETOOTH_SCAN",
|
|
115
|
-
"BLUETOOTH_CONNECT",
|
|
116
|
-
"ACCESS_FINE_LOCATION",
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
aliases = arrayOf(
|
|
121
|
-
"ACCESS_COARSE_LOCATION",
|
|
122
|
-
"ACCESS_FINE_LOCATION",
|
|
123
|
-
"BLUETOOTH",
|
|
124
|
-
"BLUETOOTH_ADMIN",
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
requestPermissionForAliases(aliases, call, "checkPermission")
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
@PermissionCallback
|
|
131
|
-
private fun checkPermission(call: PluginCall) {
|
|
132
|
-
val granted: List<Boolean> = aliases.map { alias ->
|
|
133
|
-
getPermissionState(alias) == PermissionState.GRANTED
|
|
134
|
-
}
|
|
135
|
-
// all have to be true
|
|
136
|
-
if (granted.all { it }) {
|
|
137
|
-
runInitialization(call)
|
|
138
|
-
} else {
|
|
139
|
-
call.reject("Permission denied.")
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
private fun runInitialization(call: PluginCall) {
|
|
144
|
-
if (!activity.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
|
|
145
|
-
call.reject("BLE is not supported.")
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
bluetoothAdapter =
|
|
150
|
-
(activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
|
|
151
|
-
|
|
152
|
-
if (bluetoothAdapter == null) {
|
|
153
|
-
call.reject("BLE is not available.")
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
call.resolve()
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
@PluginMethod
|
|
160
|
-
fun isEnabled(call: PluginCall) {
|
|
161
|
-
assertBluetoothAdapter(call) ?: return
|
|
162
|
-
val enabled = bluetoothAdapter?.isEnabled == true
|
|
163
|
-
val result = JSObject()
|
|
164
|
-
result.put("value", enabled)
|
|
165
|
-
call.resolve(result)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
@PluginMethod
|
|
169
|
-
fun requestEnable(call: PluginCall) {
|
|
170
|
-
assertBluetoothAdapter(call) ?: return
|
|
171
|
-
val intent = Intent(ACTION_REQUEST_ENABLE)
|
|
172
|
-
startActivityForResult(call, intent, "handleRequestEnableResult")
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
@ActivityCallback
|
|
176
|
-
private fun handleRequestEnableResult(call: PluginCall, result: ActivityResult) {
|
|
177
|
-
if (result.resultCode == Activity.RESULT_OK) {
|
|
178
|
-
call.resolve()
|
|
179
|
-
} else {
|
|
180
|
-
call.reject("requestEnable failed.")
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
@PluginMethod
|
|
185
|
-
fun enable(call: PluginCall) {
|
|
186
|
-
assertBluetoothAdapter(call) ?: return
|
|
187
|
-
val result = bluetoothAdapter?.enable()
|
|
188
|
-
if (result != true) {
|
|
189
|
-
call.reject("Enable failed.")
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
call.resolve()
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
@PluginMethod
|
|
196
|
-
fun disable(call: PluginCall) {
|
|
197
|
-
assertBluetoothAdapter(call) ?: return
|
|
198
|
-
val result = bluetoothAdapter?.disable()
|
|
199
|
-
if (result != true) {
|
|
200
|
-
call.reject("Disable failed.")
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
call.resolve()
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
@PluginMethod
|
|
207
|
-
fun startEnabledNotifications(call: PluginCall) {
|
|
208
|
-
assertBluetoothAdapter(call) ?: return
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
createStateReceiver()
|
|
212
|
-
} catch (e: Error) {
|
|
213
|
-
Logger.error(
|
|
214
|
-
TAG, "Error while registering enabled state receiver: ${e.localizedMessage}", e
|
|
215
|
-
)
|
|
216
|
-
call.reject("startEnabledNotifications failed.")
|
|
217
|
-
return
|
|
218
|
-
}
|
|
219
|
-
call.resolve()
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private fun createStateReceiver() {
|
|
223
|
-
if (stateReceiver == null) {
|
|
224
|
-
stateReceiver = object : BroadcastReceiver() {
|
|
225
|
-
override fun onReceive(context: Context, intent: Intent) {
|
|
226
|
-
val action = intent.action
|
|
227
|
-
if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
|
|
228
|
-
val state = intent.getIntExtra(
|
|
229
|
-
BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR
|
|
230
|
-
)
|
|
231
|
-
val enabled = state == BluetoothAdapter.STATE_ON
|
|
232
|
-
val result = JSObject()
|
|
233
|
-
result.put("value", enabled)
|
|
234
|
-
try {
|
|
235
|
-
notifyListeners("onEnabledChanged", result)
|
|
236
|
-
} catch (e: ConcurrentModificationException) {
|
|
237
|
-
Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
|
243
|
-
context.registerReceiver(stateReceiver, intentFilter)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
@PluginMethod
|
|
248
|
-
fun stopEnabledNotifications(call: PluginCall) {
|
|
249
|
-
if (stateReceiver != null) {
|
|
250
|
-
context.unregisterReceiver(stateReceiver)
|
|
251
|
-
}
|
|
252
|
-
stateReceiver = null
|
|
253
|
-
call.resolve()
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
@PluginMethod
|
|
257
|
-
fun isLocationEnabled(call: PluginCall) {
|
|
258
|
-
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
259
|
-
val enabled = LocationManagerCompat.isLocationEnabled(lm)
|
|
260
|
-
Logger.debug(TAG, "location $enabled")
|
|
261
|
-
val result = JSObject()
|
|
262
|
-
result.put("value", enabled)
|
|
263
|
-
call.resolve(result)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
@PluginMethod
|
|
267
|
-
fun openLocationSettings(call: PluginCall) {
|
|
268
|
-
val intent = Intent(ACTION_LOCATION_SOURCE_SETTINGS)
|
|
269
|
-
activity.startActivity(intent)
|
|
270
|
-
call.resolve()
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
@PluginMethod
|
|
274
|
-
fun openBluetoothSettings(call: PluginCall) {
|
|
275
|
-
val intent = Intent(ACTION_BLUETOOTH_SETTINGS)
|
|
276
|
-
activity.startActivity(intent)
|
|
277
|
-
call.resolve()
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
@PluginMethod
|
|
281
|
-
fun openAppSettings(call: PluginCall) {
|
|
282
|
-
val intent = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
283
|
-
intent.data = Uri.parse("package:" + activity.packageName)
|
|
284
|
-
activity.startActivity(intent)
|
|
285
|
-
call.resolve()
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
@PluginMethod
|
|
289
|
-
fun setDisplayStrings(call: PluginCall) {
|
|
290
|
-
displayStrings = DisplayStrings(
|
|
291
|
-
call.getString(
|
|
292
|
-
"scanning", displayStrings!!.scanning
|
|
293
|
-
) as String,
|
|
294
|
-
call.getString(
|
|
295
|
-
"cancel", displayStrings!!.cancel
|
|
296
|
-
) as String,
|
|
297
|
-
call.getString(
|
|
298
|
-
"availableDevices", displayStrings!!.availableDevices
|
|
299
|
-
) as String,
|
|
300
|
-
call.getString(
|
|
301
|
-
"noDeviceFound", displayStrings!!.noDeviceFound
|
|
302
|
-
) as String,
|
|
303
|
-
)
|
|
304
|
-
call.resolve()
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
@PluginMethod
|
|
308
|
-
fun requestDevice(call: PluginCall) {
|
|
309
|
-
assertBluetoothAdapter(call) ?: return
|
|
310
|
-
val scanFilters = getScanFilters(call) ?: return
|
|
311
|
-
val scanSettings = getScanSettings(call) ?: return
|
|
312
|
-
val namePrefix = call.getString("namePrefix", "") as String
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
deviceScanner?.stopScanning()
|
|
316
|
-
} catch (e: IllegalStateException) {
|
|
317
|
-
Logger.error(TAG, "Error in requestDevice: ${e.localizedMessage}", e)
|
|
318
|
-
call.reject(e.localizedMessage)
|
|
319
|
-
return
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
deviceScanner = DeviceScanner(
|
|
323
|
-
context,
|
|
324
|
-
bluetoothAdapter!!,
|
|
325
|
-
scanDuration = MAX_SCAN_DURATION,
|
|
326
|
-
displayStrings = displayStrings!!,
|
|
327
|
-
showDialog = true,
|
|
328
|
-
)
|
|
329
|
-
deviceScanner?.startScanning(
|
|
330
|
-
scanFilters, scanSettings, false, namePrefix, { scanResponse ->
|
|
331
|
-
run {
|
|
332
|
-
if (scanResponse.success) {
|
|
333
|
-
if (scanResponse.device == null) {
|
|
334
|
-
call.reject("No device found.")
|
|
335
|
-
} else {
|
|
336
|
-
val bleDevice = getBleDevice(scanResponse.device)
|
|
337
|
-
call.resolve(bleDevice)
|
|
338
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
call.reject(scanResponse.message)
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}, null
|
|
345
|
-
)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
@PluginMethod
|
|
349
|
-
fun requestLEScan(call: PluginCall) {
|
|
350
|
-
assertBluetoothAdapter(call) ?: return
|
|
351
|
-
val scanFilters = getScanFilters(call) ?: return
|
|
352
|
-
val scanSettings = getScanSettings(call) ?: return
|
|
353
|
-
val namePrefix = call.getString("namePrefix", "") as String
|
|
354
|
-
val allowDuplicates = call.getBoolean("allowDuplicates", false) as Boolean
|
|
355
|
-
|
|
356
|
-
try {
|
|
357
|
-
deviceScanner?.stopScanning()
|
|
358
|
-
} catch (e: IllegalStateException) {
|
|
359
|
-
Logger.error(TAG, "Error in requestLEScan: ${e.localizedMessage}", e)
|
|
360
|
-
call.reject(e.localizedMessage)
|
|
361
|
-
return
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
deviceScanner = DeviceScanner(
|
|
365
|
-
context,
|
|
366
|
-
bluetoothAdapter!!,
|
|
367
|
-
scanDuration = null,
|
|
368
|
-
displayStrings = displayStrings!!,
|
|
369
|
-
showDialog = false,
|
|
370
|
-
)
|
|
371
|
-
deviceScanner?.startScanning(
|
|
372
|
-
scanFilters,
|
|
373
|
-
scanSettings,
|
|
374
|
-
allowDuplicates,
|
|
375
|
-
namePrefix,
|
|
376
|
-
{ scanResponse ->
|
|
377
|
-
run {
|
|
378
|
-
if (scanResponse.success) {
|
|
379
|
-
call.resolve()
|
|
380
|
-
} else {
|
|
381
|
-
call.reject(scanResponse.message)
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
},
|
|
385
|
-
{ result ->
|
|
386
|
-
run {
|
|
387
|
-
val scanResult = getScanResult(result)
|
|
388
|
-
try {
|
|
389
|
-
notifyListeners("onScanResult", scanResult)
|
|
390
|
-
} catch (e: ConcurrentModificationException) {
|
|
391
|
-
Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
})
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
@PluginMethod
|
|
398
|
-
fun stopLEScan(call: PluginCall) {
|
|
399
|
-
assertBluetoothAdapter(call) ?: return
|
|
400
|
-
try {
|
|
401
|
-
deviceScanner?.stopScanning()
|
|
402
|
-
} catch (e: IllegalStateException) {
|
|
403
|
-
Logger.error(TAG, "Error in stopLEScan: ${e.localizedMessage}", e)
|
|
404
|
-
}
|
|
405
|
-
call.resolve()
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
@PluginMethod
|
|
409
|
-
fun getDevices(call: PluginCall) {
|
|
410
|
-
assertBluetoothAdapter(call) ?: return
|
|
411
|
-
val deviceIds = (call.getArray("deviceIds", JSArray()) as JSArray).toList<String>()
|
|
412
|
-
val bleDevices = JSArray()
|
|
413
|
-
deviceIds.forEach { deviceId ->
|
|
414
|
-
val bleDevice = JSObject()
|
|
415
|
-
bleDevice.put("deviceId", deviceId)
|
|
416
|
-
bleDevices.put(bleDevice)
|
|
417
|
-
}
|
|
418
|
-
val result = JSObject()
|
|
419
|
-
result.put("devices", bleDevices)
|
|
420
|
-
call.resolve(result)
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
@PluginMethod
|
|
424
|
-
fun getConnectedDevices(call: PluginCall) {
|
|
425
|
-
assertBluetoothAdapter(call) ?: return
|
|
426
|
-
val bluetoothManager =
|
|
427
|
-
(activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager)
|
|
428
|
-
val devices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
|
|
429
|
-
val bleDevices = JSArray()
|
|
430
|
-
devices.forEach { device ->
|
|
431
|
-
bleDevices.put(getBleDevice(device))
|
|
432
|
-
}
|
|
433
|
-
val result = JSObject()
|
|
434
|
-
result.put("devices", bleDevices)
|
|
435
|
-
call.resolve(result)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
@PluginMethod
|
|
439
|
-
fun getBondedDevices(call: PluginCall) {
|
|
440
|
-
assertBluetoothAdapter(call) ?: return
|
|
441
|
-
|
|
442
|
-
val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
|
443
|
-
val bluetoothAdapter = bluetoothManager.adapter
|
|
444
|
-
|
|
445
|
-
if (bluetoothAdapter == null) {
|
|
446
|
-
call.reject("Bluetooth is not supported on this device")
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
val bondedDevices = bluetoothAdapter.bondedDevices
|
|
451
|
-
val bleDevices = JSArray()
|
|
452
|
-
|
|
453
|
-
bondedDevices.forEach { device ->
|
|
454
|
-
bleDevices.put(getBleDevice(device))
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
val result = JSObject()
|
|
458
|
-
result.put("devices", bleDevices)
|
|
459
|
-
call.resolve(result)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
@PluginMethod
|
|
463
|
-
fun connect(call: PluginCall) {
|
|
464
|
-
val device = getOrCreateDevice(call) ?: return
|
|
465
|
-
val timeout = call.getFloat("timeout", CONNECTION_TIMEOUT)!!.toLong()
|
|
466
|
-
device.connect(timeout) { response ->
|
|
467
|
-
run {
|
|
468
|
-
if (response.success) {
|
|
469
|
-
call.resolve()
|
|
470
|
-
} else {
|
|
471
|
-
call.reject(response.value)
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
private fun onDisconnect(deviceId: String) {
|
|
478
|
-
try {
|
|
479
|
-
notifyListeners("disconnected|${deviceId}", null)
|
|
480
|
-
} catch (e: ConcurrentModificationException) {
|
|
481
|
-
Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
@PluginMethod
|
|
486
|
-
fun createBond(call: PluginCall) {
|
|
487
|
-
val device = getOrCreateDevice(call) ?: return
|
|
488
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
489
|
-
device.createBond(timeout) { response ->
|
|
490
|
-
run {
|
|
491
|
-
if (response.success) {
|
|
492
|
-
call.resolve()
|
|
493
|
-
} else {
|
|
494
|
-
call.reject(response.value)
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
@PluginMethod
|
|
501
|
-
fun isBonded(call: PluginCall) {
|
|
502
|
-
val device = getOrCreateDevice(call) ?: return
|
|
503
|
-
val isBonded = device.isBonded()
|
|
504
|
-
val result = JSObject()
|
|
505
|
-
result.put("value", isBonded)
|
|
506
|
-
call.resolve(result)
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
@PluginMethod
|
|
510
|
-
fun disconnect(call: PluginCall) {
|
|
511
|
-
val device = getOrCreateDevice(call) ?: return
|
|
512
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
513
|
-
device.disconnect(timeout) { response ->
|
|
514
|
-
run {
|
|
515
|
-
if (response.success) {
|
|
516
|
-
device.cleanup()
|
|
517
|
-
deviceMap.remove(device.getId())
|
|
518
|
-
call.resolve()
|
|
519
|
-
} else {
|
|
520
|
-
call.reject(response.value)
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
@PluginMethod
|
|
527
|
-
fun getServices(call: PluginCall) {
|
|
528
|
-
val device = getDevice(call) ?: return
|
|
529
|
-
val services = device.getServices()
|
|
530
|
-
val bleServices = JSArray()
|
|
531
|
-
services.forEach { service ->
|
|
532
|
-
val bleCharacteristics = JSArray()
|
|
533
|
-
service.characteristics.forEach { characteristic ->
|
|
534
|
-
val bleCharacteristic = JSObject()
|
|
535
|
-
bleCharacteristic.put("uuid", characteristic.uuid)
|
|
536
|
-
bleCharacteristic.put("properties", getProperties(characteristic))
|
|
537
|
-
val bleDescriptors = JSArray()
|
|
538
|
-
characteristic.descriptors.forEach { descriptor ->
|
|
539
|
-
val bleDescriptor = JSObject()
|
|
540
|
-
bleDescriptor.put("uuid", descriptor.uuid)
|
|
541
|
-
bleDescriptors.put(bleDescriptor)
|
|
542
|
-
}
|
|
543
|
-
bleCharacteristic.put("descriptors", bleDescriptors)
|
|
544
|
-
bleCharacteristics.put(bleCharacteristic)
|
|
545
|
-
}
|
|
546
|
-
val bleService = JSObject()
|
|
547
|
-
bleService.put("uuid", service.uuid)
|
|
548
|
-
bleService.put("characteristics", bleCharacteristics)
|
|
549
|
-
bleServices.put(bleService)
|
|
550
|
-
}
|
|
551
|
-
val ret = JSObject()
|
|
552
|
-
ret.put("services", bleServices)
|
|
553
|
-
call.resolve(ret)
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
private fun getProperties(characteristic: BluetoothGattCharacteristic): JSObject {
|
|
557
|
-
val properties = JSObject()
|
|
558
|
-
properties.put(
|
|
559
|
-
"broadcast",
|
|
560
|
-
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_BROADCAST > 0
|
|
561
|
-
)
|
|
562
|
-
properties.put(
|
|
563
|
-
"read", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ > 0
|
|
564
|
-
)
|
|
565
|
-
properties.put(
|
|
566
|
-
"writeWithoutResponse",
|
|
567
|
-
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0
|
|
568
|
-
)
|
|
569
|
-
properties.put(
|
|
570
|
-
"write", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0
|
|
571
|
-
)
|
|
572
|
-
properties.put(
|
|
573
|
-
"notify", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
|
|
574
|
-
)
|
|
575
|
-
properties.put(
|
|
576
|
-
"indicate",
|
|
577
|
-
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0
|
|
578
|
-
)
|
|
579
|
-
properties.put(
|
|
580
|
-
"authenticatedSignedWrites",
|
|
581
|
-
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE > 0
|
|
582
|
-
)
|
|
583
|
-
properties.put(
|
|
584
|
-
"extendedProperties",
|
|
585
|
-
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS > 0
|
|
586
|
-
)
|
|
587
|
-
return properties
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
@PluginMethod
|
|
591
|
-
fun discoverServices(call: PluginCall) {
|
|
592
|
-
val device = getDevice(call) ?: return
|
|
593
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
594
|
-
device.discoverServices(timeout) { response ->
|
|
595
|
-
run {
|
|
596
|
-
if (response.success) {
|
|
597
|
-
call.resolve()
|
|
598
|
-
} else {
|
|
599
|
-
call.reject(response.value)
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
@PluginMethod
|
|
606
|
-
fun getMtu(call: PluginCall) {
|
|
607
|
-
val device = getDevice(call) ?: return
|
|
608
|
-
val mtu = device.getMtu()
|
|
609
|
-
val ret = JSObject()
|
|
610
|
-
ret.put("value", mtu)
|
|
611
|
-
call.resolve(ret)
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
@PluginMethod
|
|
615
|
-
fun requestConnectionPriority(call: PluginCall) {
|
|
616
|
-
val device = getDevice(call) ?: return
|
|
617
|
-
val connectionPriority = call.getInt("connectionPriority", -1) as Int
|
|
618
|
-
if (connectionPriority < BluetoothGatt.CONNECTION_PRIORITY_BALANCED || connectionPriority > BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
|
|
619
|
-
call.reject("Invalid connectionPriority.")
|
|
620
|
-
return
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
val result = device.requestConnectionPriority(connectionPriority)
|
|
624
|
-
if (result) {
|
|
625
|
-
call.resolve()
|
|
626
|
-
} else {
|
|
627
|
-
call.reject("requestConnectionPriority failed.")
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
@PluginMethod
|
|
632
|
-
fun readRssi(call: PluginCall) {
|
|
633
|
-
val device = getDevice(call) ?: return
|
|
634
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
635
|
-
device.readRssi(timeout) { response ->
|
|
636
|
-
run {
|
|
637
|
-
if (response.success) {
|
|
638
|
-
val ret = JSObject()
|
|
639
|
-
ret.put("value", response.value)
|
|
640
|
-
call.resolve(ret)
|
|
641
|
-
} else {
|
|
642
|
-
call.reject(response.value)
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
@PluginMethod
|
|
649
|
-
fun read(call: PluginCall) {
|
|
650
|
-
val device = getDevice(call) ?: return
|
|
651
|
-
val characteristic = getCharacteristic(call) ?: return
|
|
652
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
653
|
-
device.read(characteristic.first, characteristic.second, timeout) { response ->
|
|
654
|
-
run {
|
|
655
|
-
if (response.success) {
|
|
656
|
-
val ret = JSObject()
|
|
657
|
-
ret.put("value", response.value)
|
|
658
|
-
call.resolve(ret)
|
|
659
|
-
} else {
|
|
660
|
-
call.reject(response.value)
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
@PluginMethod
|
|
667
|
-
fun write(call: PluginCall) {
|
|
668
|
-
val device = getDevice(call) ?: return
|
|
669
|
-
val characteristic = getCharacteristic(call) ?: return
|
|
670
|
-
val value = call.getString("value", null)
|
|
671
|
-
if (value == null) {
|
|
672
|
-
call.reject("Value required.")
|
|
673
|
-
return
|
|
674
|
-
}
|
|
675
|
-
val writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
676
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
677
|
-
device.write(
|
|
678
|
-
characteristic.first, characteristic.second, value, writeType, timeout
|
|
679
|
-
) { response ->
|
|
680
|
-
run {
|
|
681
|
-
if (response.success) {
|
|
682
|
-
call.resolve()
|
|
683
|
-
} else {
|
|
684
|
-
call.reject(response.value)
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
@PluginMethod
|
|
691
|
-
fun writeWithoutResponse(call: PluginCall) {
|
|
692
|
-
val device = getDevice(call) ?: return
|
|
693
|
-
val characteristic = getCharacteristic(call) ?: return
|
|
694
|
-
val value = call.getString("value", null)
|
|
695
|
-
if (value == null) {
|
|
696
|
-
call.reject("Value required.")
|
|
697
|
-
return
|
|
698
|
-
}
|
|
699
|
-
val writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
700
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
701
|
-
device.write(
|
|
702
|
-
characteristic.first, characteristic.second, value, writeType, timeout
|
|
703
|
-
) { response ->
|
|
704
|
-
run {
|
|
705
|
-
if (response.success) {
|
|
706
|
-
call.resolve()
|
|
707
|
-
} else {
|
|
708
|
-
call.reject(response.value)
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
@PluginMethod
|
|
715
|
-
fun readDescriptor(call: PluginCall) {
|
|
716
|
-
val device = getDevice(call) ?: return
|
|
717
|
-
val descriptor = getDescriptor(call) ?: return
|
|
718
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
719
|
-
device.readDescriptor(
|
|
720
|
-
descriptor.first, descriptor.second, descriptor.third, timeout
|
|
721
|
-
) { response ->
|
|
722
|
-
run {
|
|
723
|
-
if (response.success) {
|
|
724
|
-
val ret = JSObject()
|
|
725
|
-
ret.put("value", response.value)
|
|
726
|
-
call.resolve(ret)
|
|
727
|
-
} else {
|
|
728
|
-
call.reject(response.value)
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
@PluginMethod
|
|
735
|
-
fun writeDescriptor(call: PluginCall) {
|
|
736
|
-
val device = getDevice(call) ?: return
|
|
737
|
-
val descriptor = getDescriptor(call) ?: return
|
|
738
|
-
val value = call.getString("value", null)
|
|
739
|
-
if (value == null) {
|
|
740
|
-
call.reject("Value required.")
|
|
741
|
-
return
|
|
742
|
-
}
|
|
743
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
744
|
-
device.writeDescriptor(
|
|
745
|
-
descriptor.first, descriptor.second, descriptor.third, value, timeout
|
|
746
|
-
) { response ->
|
|
747
|
-
run {
|
|
748
|
-
if (response.success) {
|
|
749
|
-
call.resolve()
|
|
750
|
-
} else {
|
|
751
|
-
call.reject(response.value)
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
@PluginMethod
|
|
758
|
-
fun startNotifications(call: PluginCall) {
|
|
759
|
-
val device = getDevice(call) ?: return
|
|
760
|
-
val characteristic = getCharacteristic(call) ?: return
|
|
761
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
762
|
-
device.setNotifications(characteristic.first, characteristic.second, true, { response ->
|
|
763
|
-
run {
|
|
764
|
-
val key =
|
|
765
|
-
"notification|${device.getId()}|${(characteristic.first)}|${(characteristic.second)}"
|
|
766
|
-
val ret = JSObject()
|
|
767
|
-
ret.put("value", response.value)
|
|
768
|
-
try {
|
|
769
|
-
notifyListeners(key, ret)
|
|
770
|
-
} catch (e: ConcurrentModificationException) {
|
|
771
|
-
Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}, timeout, { response ->
|
|
775
|
-
run {
|
|
776
|
-
if (response.success) {
|
|
777
|
-
call.resolve()
|
|
778
|
-
} else {
|
|
779
|
-
call.reject(response.value)
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
})
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
@PluginMethod
|
|
786
|
-
fun stopNotifications(call: PluginCall) {
|
|
787
|
-
val device = getDevice(call) ?: return
|
|
788
|
-
val characteristic = getCharacteristic(call) ?: return
|
|
789
|
-
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
790
|
-
device.setNotifications(
|
|
791
|
-
characteristic.first, characteristic.second, false, null, timeout
|
|
792
|
-
) { response ->
|
|
793
|
-
run {
|
|
794
|
-
if (response.success) {
|
|
795
|
-
call.resolve()
|
|
796
|
-
} else {
|
|
797
|
-
call.reject(response.value)
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
private fun assertBluetoothAdapter(call: PluginCall): Boolean? {
|
|
804
|
-
if (bluetoothAdapter == null) {
|
|
805
|
-
call.reject("Bluetooth LE not initialized.")
|
|
806
|
-
return null
|
|
807
|
-
}
|
|
808
|
-
return true
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
private fun getScanFilters(call: PluginCall): List<ScanFilter>? {
|
|
812
|
-
val filters: ArrayList<ScanFilter> = ArrayList()
|
|
813
|
-
|
|
814
|
-
val services = (call.getArray("services", JSArray()) as JSArray).toList<String>()
|
|
815
|
-
val manufacturerDataArray = call.getArray("manufacturerData", JSArray())
|
|
816
|
-
val serviceDataArray = call.getArray("serviceData", JSArray())
|
|
817
|
-
val name = call.getString("name", null)
|
|
818
|
-
|
|
819
|
-
try {
|
|
820
|
-
// Create filters based on services
|
|
821
|
-
for (service in services) {
|
|
822
|
-
val filter = ScanFilter.Builder()
|
|
823
|
-
filter.setServiceUuid(ParcelUuid.fromString(service))
|
|
824
|
-
if (name != null) {
|
|
825
|
-
filter.setDeviceName(name)
|
|
826
|
-
}
|
|
827
|
-
filters.add(filter.build())
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// Service Data Handling (for filtering by service data like OpenDroneID)
|
|
831
|
-
serviceDataArray?.let {
|
|
832
|
-
for (i in 0 until it.length()) {
|
|
833
|
-
val serviceDataObject = it.getJSONObject(i)
|
|
834
|
-
|
|
835
|
-
val serviceUuid = serviceDataObject.getString("serviceUuid")
|
|
836
|
-
val servicePuuid = ParcelUuid.fromString(serviceUuid)
|
|
837
|
-
|
|
838
|
-
val dataPrefix = if (serviceDataObject.has("dataPrefix")) {
|
|
839
|
-
val dataPrefixString = serviceDataObject.getString("dataPrefix")
|
|
840
|
-
stringToBytes(dataPrefixString)
|
|
841
|
-
} else null
|
|
842
|
-
|
|
843
|
-
val mask = if (serviceDataObject.has("mask")) {
|
|
844
|
-
val maskString = serviceDataObject.getString("mask")
|
|
845
|
-
stringToBytes(maskString)
|
|
846
|
-
} else null
|
|
847
|
-
|
|
848
|
-
val filterBuilder = ScanFilter.Builder()
|
|
849
|
-
|
|
850
|
-
if (dataPrefix != null && mask != null) {
|
|
851
|
-
filterBuilder.setServiceData(servicePuuid, dataPrefix, mask)
|
|
852
|
-
} else if (dataPrefix != null) {
|
|
853
|
-
filterBuilder.setServiceData(servicePuuid, dataPrefix)
|
|
854
|
-
} else {
|
|
855
|
-
// Set service data filter without data (just match the service UUID)
|
|
856
|
-
filterBuilder.setServiceData(servicePuuid, byteArrayOf())
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
if (name != null) {
|
|
860
|
-
filterBuilder.setDeviceName(name)
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
filters.add(filterBuilder.build())
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// Manufacturer Data Handling (with optional parameters)
|
|
868
|
-
manufacturerDataArray?.let {
|
|
869
|
-
for (i in 0 until it.length()) {
|
|
870
|
-
val manufacturerDataObject = it.getJSONObject(i)
|
|
871
|
-
|
|
872
|
-
val companyIdentifier = manufacturerDataObject.getInt("companyIdentifier")
|
|
873
|
-
|
|
874
|
-
val dataPrefix = if (manufacturerDataObject.has("dataPrefix")) {
|
|
875
|
-
val dataPrefixString = manufacturerDataObject.getString("dataPrefix")
|
|
876
|
-
stringToBytes(dataPrefixString)
|
|
877
|
-
} else null
|
|
878
|
-
|
|
879
|
-
val mask = if (manufacturerDataObject.has("mask")) {
|
|
880
|
-
val maskString = manufacturerDataObject.getString("mask")
|
|
881
|
-
stringToBytes(maskString)
|
|
882
|
-
} else null
|
|
883
|
-
|
|
884
|
-
val filterBuilder = ScanFilter.Builder()
|
|
885
|
-
|
|
886
|
-
if (dataPrefix != null && mask != null) {
|
|
887
|
-
filterBuilder.setManufacturerData(companyIdentifier, dataPrefix, mask)
|
|
888
|
-
} else if (dataPrefix != null) {
|
|
889
|
-
filterBuilder.setManufacturerData(companyIdentifier, dataPrefix)
|
|
890
|
-
} else {
|
|
891
|
-
// Android requires at least dataPrefix for manufacturer filters.
|
|
892
|
-
call.reject("dataPrefix is required when specifying manufacturerData.")
|
|
893
|
-
return null
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
if (name != null) {
|
|
897
|
-
filterBuilder.setDeviceName(name)
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
filters.add(filterBuilder.build())
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
// Create filters when providing only name
|
|
904
|
-
if (name != null && filters.isEmpty()) {
|
|
905
|
-
val filterBuilder = ScanFilter.Builder()
|
|
906
|
-
filterBuilder.setDeviceName(name)
|
|
907
|
-
filters.add(filterBuilder.build())
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
return filters;
|
|
911
|
-
} catch (e: IllegalArgumentException) {
|
|
912
|
-
call.reject("Invalid UUID or Manufacturer data provided.")
|
|
913
|
-
return null
|
|
914
|
-
} catch (e: Exception) {
|
|
915
|
-
call.reject("Invalid or malformed filter data provided.")
|
|
916
|
-
return null
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
private fun getScanSettings(call: PluginCall): ScanSettings? {
|
|
921
|
-
val scanSettings = ScanSettings.Builder()
|
|
922
|
-
val scanMode = call.getInt("scanMode", ScanSettings.SCAN_MODE_BALANCED) as Int
|
|
923
|
-
try {
|
|
924
|
-
scanSettings.setScanMode(scanMode)
|
|
925
|
-
} catch (e: IllegalArgumentException) {
|
|
926
|
-
call.reject("Invalid scan mode.")
|
|
927
|
-
return null
|
|
928
|
-
}
|
|
929
|
-
return scanSettings.build()
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
private fun getBleDevice(device: BluetoothDevice): JSObject {
|
|
933
|
-
val bleDevice = JSObject()
|
|
934
|
-
bleDevice.put("deviceId", device.address)
|
|
935
|
-
if (device.name != null) {
|
|
936
|
-
bleDevice.put("name", device.name)
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
val uuids = JSArray()
|
|
940
|
-
device.uuids?.forEach { uuid -> uuids.put(uuid.toString()) }
|
|
941
|
-
if (uuids.length() > 0) {
|
|
942
|
-
bleDevice.put("uuids", uuids)
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
return bleDevice
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
private fun getScanResult(result: ScanResult): JSObject {
|
|
949
|
-
val scanResult = JSObject()
|
|
950
|
-
|
|
951
|
-
val bleDevice = getBleDevice(result.device)
|
|
952
|
-
scanResult.put("device", bleDevice)
|
|
953
|
-
|
|
954
|
-
if (result.device.name != null) {
|
|
955
|
-
scanResult.put("localName", result.device.name)
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
scanResult.put("rssi", result.rssi)
|
|
959
|
-
|
|
960
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
961
|
-
scanResult.put("txPower", result.txPower)
|
|
962
|
-
} else {
|
|
963
|
-
scanResult.put("txPower", 127)
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
val manufacturerData = JSObject()
|
|
967
|
-
val manufacturerSpecificData = result.scanRecord?.manufacturerSpecificData
|
|
968
|
-
if (manufacturerSpecificData != null) {
|
|
969
|
-
for (i in 0 until manufacturerSpecificData.size()) {
|
|
970
|
-
val key = manufacturerSpecificData.keyAt(i)
|
|
971
|
-
val bytes = manufacturerSpecificData.get(key)
|
|
972
|
-
manufacturerData.put(key.toString(), bytesToString(bytes))
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
scanResult.put("manufacturerData", manufacturerData)
|
|
976
|
-
|
|
977
|
-
val serviceDataObject = JSObject()
|
|
978
|
-
val serviceData = result.scanRecord?.serviceData
|
|
979
|
-
serviceData?.forEach {
|
|
980
|
-
serviceDataObject.put(it.key.toString(), bytesToString(it.value))
|
|
981
|
-
}
|
|
982
|
-
scanResult.put("serviceData", serviceDataObject)
|
|
983
|
-
|
|
984
|
-
val uuids = JSArray()
|
|
985
|
-
result.scanRecord?.serviceUuids?.forEach { uuid -> uuids.put(uuid.toString()) }
|
|
986
|
-
scanResult.put("uuids", uuids)
|
|
987
|
-
|
|
988
|
-
scanResult.put("rawAdvertisement", result.scanRecord?.bytes?.let { bytesToString(it) })
|
|
989
|
-
return scanResult
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
private fun getDisplayStrings(): DisplayStrings {
|
|
993
|
-
return DisplayStrings(
|
|
994
|
-
config.getString(
|
|
995
|
-
"displayStrings.scanning", "Scanning..."
|
|
996
|
-
),
|
|
997
|
-
config.getString(
|
|
998
|
-
"displayStrings.cancel", "Cancel"
|
|
999
|
-
),
|
|
1000
|
-
config.getString(
|
|
1001
|
-
"displayStrings.availableDevices", "Available devices"
|
|
1002
|
-
),
|
|
1003
|
-
config.getString(
|
|
1004
|
-
"displayStrings.noDeviceFound", "No device found"
|
|
1005
|
-
),
|
|
1006
|
-
)
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
private fun getDeviceId(call: PluginCall): String? {
|
|
1010
|
-
val deviceId = call.getString("deviceId", null)
|
|
1011
|
-
if (deviceId == null) {
|
|
1012
|
-
call.reject("deviceId required.")
|
|
1013
|
-
return null
|
|
1014
|
-
}
|
|
1015
|
-
return deviceId
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
private fun getOrCreateDevice(call: PluginCall): Device? {
|
|
1019
|
-
assertBluetoothAdapter(call) ?: return null
|
|
1020
|
-
val deviceId = getDeviceId(call) ?: return null
|
|
1021
|
-
val device = deviceMap[deviceId]
|
|
1022
|
-
if (device != null) {
|
|
1023
|
-
return device
|
|
1024
|
-
}
|
|
1025
|
-
return try {
|
|
1026
|
-
val newDevice = Device(
|
|
1027
|
-
activity.applicationContext, bluetoothAdapter!!, deviceId
|
|
1028
|
-
) {
|
|
1029
|
-
onDisconnect(deviceId)
|
|
1030
|
-
}
|
|
1031
|
-
deviceMap[deviceId] = newDevice
|
|
1032
|
-
newDevice
|
|
1033
|
-
} catch (e: IllegalArgumentException) {
|
|
1034
|
-
call.reject("Invalid deviceId")
|
|
1035
|
-
null
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
private fun getDevice(call: PluginCall): Device? {
|
|
1040
|
-
assertBluetoothAdapter(call) ?: return null
|
|
1041
|
-
val deviceId = getDeviceId(call) ?: return null
|
|
1042
|
-
val device = deviceMap[deviceId]
|
|
1043
|
-
if (device == null || !device.isConnected()) {
|
|
1044
|
-
call.reject("Not connected to device.")
|
|
1045
|
-
return null
|
|
1046
|
-
}
|
|
1047
|
-
return device
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
private fun getCharacteristic(call: PluginCall): Pair<UUID, UUID>? {
|
|
1051
|
-
val serviceString = call.getString("service", null)
|
|
1052
|
-
val serviceUUID: UUID?
|
|
1053
|
-
try {
|
|
1054
|
-
serviceUUID = UUID.fromString(serviceString)
|
|
1055
|
-
} catch (e: IllegalArgumentException) {
|
|
1056
|
-
call.reject("Invalid service UUID.")
|
|
1057
|
-
return null
|
|
1058
|
-
}
|
|
1059
|
-
if (serviceUUID == null) {
|
|
1060
|
-
call.reject("Service UUID required.")
|
|
1061
|
-
return null
|
|
1062
|
-
}
|
|
1063
|
-
val characteristicString = call.getString("characteristic", null)
|
|
1064
|
-
val characteristicUUID: UUID?
|
|
1065
|
-
try {
|
|
1066
|
-
characteristicUUID = UUID.fromString(characteristicString)
|
|
1067
|
-
} catch (e: IllegalArgumentException) {
|
|
1068
|
-
call.reject("Invalid characteristic UUID.")
|
|
1069
|
-
return null
|
|
1070
|
-
}
|
|
1071
|
-
if (characteristicUUID == null) {
|
|
1072
|
-
call.reject("Characteristic UUID required.")
|
|
1073
|
-
return null
|
|
1074
|
-
}
|
|
1075
|
-
return Pair(serviceUUID, characteristicUUID)
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
private fun getDescriptor(call: PluginCall): Triple<UUID, UUID, UUID>? {
|
|
1079
|
-
val characteristic = getCharacteristic(call) ?: return null
|
|
1080
|
-
val descriptorString = call.getString("descriptor", null)
|
|
1081
|
-
val descriptorUUID: UUID?
|
|
1082
|
-
try {
|
|
1083
|
-
descriptorUUID = UUID.fromString(descriptorString)
|
|
1084
|
-
} catch (e: IllegalAccessException) {
|
|
1085
|
-
call.reject("Invalid descriptor UUID.")
|
|
1086
|
-
return null
|
|
1087
|
-
}
|
|
1088
|
-
if (descriptorUUID == null) {
|
|
1089
|
-
call.reject("Descriptor UUID required.")
|
|
1090
|
-
return null
|
|
1091
|
-
}
|
|
1092
|
-
return Triple(characteristic.first, characteristic.second, descriptorUUID)
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1
|
+
package com.capacitorjs.community.plugins.bluetoothle
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.annotation.SuppressLint
|
|
5
|
+
import android.app.Activity
|
|
6
|
+
import android.bluetooth.BluetoothAdapter
|
|
7
|
+
import android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE
|
|
8
|
+
import android.bluetooth.BluetoothDevice
|
|
9
|
+
import android.bluetooth.BluetoothGatt
|
|
10
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
11
|
+
import android.bluetooth.BluetoothManager
|
|
12
|
+
import android.bluetooth.BluetoothProfile
|
|
13
|
+
import android.bluetooth.le.ScanFilter
|
|
14
|
+
import android.bluetooth.le.ScanResult
|
|
15
|
+
import android.bluetooth.le.ScanSettings
|
|
16
|
+
import android.content.BroadcastReceiver
|
|
17
|
+
import android.content.Context
|
|
18
|
+
import android.content.Intent
|
|
19
|
+
import android.content.IntentFilter
|
|
20
|
+
import android.content.pm.PackageManager
|
|
21
|
+
import android.location.LocationManager
|
|
22
|
+
import android.net.Uri
|
|
23
|
+
import android.os.Build
|
|
24
|
+
import android.os.ParcelUuid
|
|
25
|
+
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
|
26
|
+
import android.provider.Settings.ACTION_BLUETOOTH_SETTINGS
|
|
27
|
+
import android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS
|
|
28
|
+
import androidx.activity.result.ActivityResult
|
|
29
|
+
import androidx.core.location.LocationManagerCompat
|
|
30
|
+
import com.getcapacitor.JSArray
|
|
31
|
+
import com.getcapacitor.JSObject
|
|
32
|
+
import com.getcapacitor.Logger
|
|
33
|
+
import com.getcapacitor.PermissionState
|
|
34
|
+
import com.getcapacitor.Plugin
|
|
35
|
+
import com.getcapacitor.PluginCall
|
|
36
|
+
import com.getcapacitor.PluginMethod
|
|
37
|
+
import com.getcapacitor.annotation.ActivityCallback
|
|
38
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
39
|
+
import com.getcapacitor.annotation.Permission
|
|
40
|
+
import com.getcapacitor.annotation.PermissionCallback
|
|
41
|
+
import java.util.UUID
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@SuppressLint("MissingPermission")
|
|
45
|
+
@CapacitorPlugin(
|
|
46
|
+
name = "BluetoothLe",
|
|
47
|
+
permissions = [
|
|
48
|
+
Permission(
|
|
49
|
+
strings = [
|
|
50
|
+
Manifest.permission.ACCESS_COARSE_LOCATION,
|
|
51
|
+
], alias = "ACCESS_COARSE_LOCATION"
|
|
52
|
+
),
|
|
53
|
+
Permission(
|
|
54
|
+
strings = [
|
|
55
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
56
|
+
], alias = "ACCESS_FINE_LOCATION"
|
|
57
|
+
),
|
|
58
|
+
Permission(
|
|
59
|
+
strings = [
|
|
60
|
+
Manifest.permission.BLUETOOTH,
|
|
61
|
+
], alias = "BLUETOOTH"
|
|
62
|
+
),
|
|
63
|
+
Permission(
|
|
64
|
+
strings = [
|
|
65
|
+
Manifest.permission.BLUETOOTH_ADMIN,
|
|
66
|
+
], alias = "BLUETOOTH_ADMIN"
|
|
67
|
+
),
|
|
68
|
+
Permission(
|
|
69
|
+
strings = [
|
|
70
|
+
// Manifest.permission.BLUETOOTH_SCAN
|
|
71
|
+
"android.permission.BLUETOOTH_SCAN",
|
|
72
|
+
], alias = "BLUETOOTH_SCAN"
|
|
73
|
+
),
|
|
74
|
+
Permission(
|
|
75
|
+
strings = [
|
|
76
|
+
// Manifest.permission.BLUETOOTH_ADMIN
|
|
77
|
+
"android.permission.BLUETOOTH_CONNECT",
|
|
78
|
+
], alias = "BLUETOOTH_CONNECT"
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
class BluetoothLe : Plugin() {
|
|
83
|
+
companion object {
|
|
84
|
+
private val TAG = BluetoothLe::class.java.simpleName
|
|
85
|
+
|
|
86
|
+
// maximal scan duration for requestDevice
|
|
87
|
+
private const val MAX_SCAN_DURATION: Long = 30000
|
|
88
|
+
private const val CONNECTION_TIMEOUT: Float = 10000.0F
|
|
89
|
+
private const val DEFAULT_TIMEOUT: Float = 5000.0F
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
93
|
+
private var stateReceiver: BroadcastReceiver? = null
|
|
94
|
+
private var deviceMap = HashMap<String, Device>()
|
|
95
|
+
private var deviceScanner: DeviceScanner? = null
|
|
96
|
+
private var displayStrings: DisplayStrings? = null
|
|
97
|
+
private var aliases: Array<String> = arrayOf()
|
|
98
|
+
|
|
99
|
+
override fun load() {
|
|
100
|
+
displayStrings = getDisplayStrings()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@PluginMethod
|
|
104
|
+
fun initialize(call: PluginCall) {
|
|
105
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
106
|
+
val neverForLocation = call.getBoolean("androidNeverForLocation", false) as Boolean
|
|
107
|
+
aliases = if (neverForLocation) {
|
|
108
|
+
arrayOf(
|
|
109
|
+
"BLUETOOTH_SCAN",
|
|
110
|
+
"BLUETOOTH_CONNECT",
|
|
111
|
+
)
|
|
112
|
+
} else {
|
|
113
|
+
arrayOf(
|
|
114
|
+
"BLUETOOTH_SCAN",
|
|
115
|
+
"BLUETOOTH_CONNECT",
|
|
116
|
+
"ACCESS_FINE_LOCATION",
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
aliases = arrayOf(
|
|
121
|
+
"ACCESS_COARSE_LOCATION",
|
|
122
|
+
"ACCESS_FINE_LOCATION",
|
|
123
|
+
"BLUETOOTH",
|
|
124
|
+
"BLUETOOTH_ADMIN",
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
requestPermissionForAliases(aliases, call, "checkPermission")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@PermissionCallback
|
|
131
|
+
private fun checkPermission(call: PluginCall) {
|
|
132
|
+
val granted: List<Boolean> = aliases.map { alias ->
|
|
133
|
+
getPermissionState(alias) == PermissionState.GRANTED
|
|
134
|
+
}
|
|
135
|
+
// all have to be true
|
|
136
|
+
if (granted.all { it }) {
|
|
137
|
+
runInitialization(call)
|
|
138
|
+
} else {
|
|
139
|
+
call.reject("Permission denied.")
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun runInitialization(call: PluginCall) {
|
|
144
|
+
if (!activity.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
|
|
145
|
+
call.reject("BLE is not supported.")
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
bluetoothAdapter =
|
|
150
|
+
(activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
|
|
151
|
+
|
|
152
|
+
if (bluetoothAdapter == null) {
|
|
153
|
+
call.reject("BLE is not available.")
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
call.resolve()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@PluginMethod
|
|
160
|
+
fun isEnabled(call: PluginCall) {
|
|
161
|
+
assertBluetoothAdapter(call) ?: return
|
|
162
|
+
val enabled = bluetoothAdapter?.isEnabled == true
|
|
163
|
+
val result = JSObject()
|
|
164
|
+
result.put("value", enabled)
|
|
165
|
+
call.resolve(result)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@PluginMethod
|
|
169
|
+
fun requestEnable(call: PluginCall) {
|
|
170
|
+
assertBluetoothAdapter(call) ?: return
|
|
171
|
+
val intent = Intent(ACTION_REQUEST_ENABLE)
|
|
172
|
+
startActivityForResult(call, intent, "handleRequestEnableResult")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@ActivityCallback
|
|
176
|
+
private fun handleRequestEnableResult(call: PluginCall, result: ActivityResult) {
|
|
177
|
+
if (result.resultCode == Activity.RESULT_OK) {
|
|
178
|
+
call.resolve()
|
|
179
|
+
} else {
|
|
180
|
+
call.reject("requestEnable failed.")
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@PluginMethod
|
|
185
|
+
fun enable(call: PluginCall) {
|
|
186
|
+
assertBluetoothAdapter(call) ?: return
|
|
187
|
+
val result = bluetoothAdapter?.enable()
|
|
188
|
+
if (result != true) {
|
|
189
|
+
call.reject("Enable failed.")
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
call.resolve()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@PluginMethod
|
|
196
|
+
fun disable(call: PluginCall) {
|
|
197
|
+
assertBluetoothAdapter(call) ?: return
|
|
198
|
+
val result = bluetoothAdapter?.disable()
|
|
199
|
+
if (result != true) {
|
|
200
|
+
call.reject("Disable failed.")
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
call.resolve()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@PluginMethod
|
|
207
|
+
fun startEnabledNotifications(call: PluginCall) {
|
|
208
|
+
assertBluetoothAdapter(call) ?: return
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
createStateReceiver()
|
|
212
|
+
} catch (e: Error) {
|
|
213
|
+
Logger.error(
|
|
214
|
+
TAG, "Error while registering enabled state receiver: ${e.localizedMessage}", e
|
|
215
|
+
)
|
|
216
|
+
call.reject("startEnabledNotifications failed.")
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
call.resolve()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private fun createStateReceiver() {
|
|
223
|
+
if (stateReceiver == null) {
|
|
224
|
+
stateReceiver = object : BroadcastReceiver() {
|
|
225
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
226
|
+
val action = intent.action
|
|
227
|
+
if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
|
|
228
|
+
val state = intent.getIntExtra(
|
|
229
|
+
BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR
|
|
230
|
+
)
|
|
231
|
+
val enabled = state == BluetoothAdapter.STATE_ON
|
|
232
|
+
val result = JSObject()
|
|
233
|
+
result.put("value", enabled)
|
|
234
|
+
try {
|
|
235
|
+
notifyListeners("onEnabledChanged", result)
|
|
236
|
+
} catch (e: ConcurrentModificationException) {
|
|
237
|
+
Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
|
243
|
+
context.registerReceiver(stateReceiver, intentFilter)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@PluginMethod
|
|
248
|
+
fun stopEnabledNotifications(call: PluginCall) {
|
|
249
|
+
if (stateReceiver != null) {
|
|
250
|
+
context.unregisterReceiver(stateReceiver)
|
|
251
|
+
}
|
|
252
|
+
stateReceiver = null
|
|
253
|
+
call.resolve()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@PluginMethod
|
|
257
|
+
fun isLocationEnabled(call: PluginCall) {
|
|
258
|
+
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
259
|
+
val enabled = LocationManagerCompat.isLocationEnabled(lm)
|
|
260
|
+
Logger.debug(TAG, "location $enabled")
|
|
261
|
+
val result = JSObject()
|
|
262
|
+
result.put("value", enabled)
|
|
263
|
+
call.resolve(result)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@PluginMethod
|
|
267
|
+
fun openLocationSettings(call: PluginCall) {
|
|
268
|
+
val intent = Intent(ACTION_LOCATION_SOURCE_SETTINGS)
|
|
269
|
+
activity.startActivity(intent)
|
|
270
|
+
call.resolve()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@PluginMethod
|
|
274
|
+
fun openBluetoothSettings(call: PluginCall) {
|
|
275
|
+
val intent = Intent(ACTION_BLUETOOTH_SETTINGS)
|
|
276
|
+
activity.startActivity(intent)
|
|
277
|
+
call.resolve()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@PluginMethod
|
|
281
|
+
fun openAppSettings(call: PluginCall) {
|
|
282
|
+
val intent = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
283
|
+
intent.data = Uri.parse("package:" + activity.packageName)
|
|
284
|
+
activity.startActivity(intent)
|
|
285
|
+
call.resolve()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@PluginMethod
|
|
289
|
+
fun setDisplayStrings(call: PluginCall) {
|
|
290
|
+
displayStrings = DisplayStrings(
|
|
291
|
+
call.getString(
|
|
292
|
+
"scanning", displayStrings!!.scanning
|
|
293
|
+
) as String,
|
|
294
|
+
call.getString(
|
|
295
|
+
"cancel", displayStrings!!.cancel
|
|
296
|
+
) as String,
|
|
297
|
+
call.getString(
|
|
298
|
+
"availableDevices", displayStrings!!.availableDevices
|
|
299
|
+
) as String,
|
|
300
|
+
call.getString(
|
|
301
|
+
"noDeviceFound", displayStrings!!.noDeviceFound
|
|
302
|
+
) as String,
|
|
303
|
+
)
|
|
304
|
+
call.resolve()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@PluginMethod
|
|
308
|
+
fun requestDevice(call: PluginCall) {
|
|
309
|
+
assertBluetoothAdapter(call) ?: return
|
|
310
|
+
val scanFilters = getScanFilters(call) ?: return
|
|
311
|
+
val scanSettings = getScanSettings(call) ?: return
|
|
312
|
+
val namePrefix = call.getString("namePrefix", "") as String
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
deviceScanner?.stopScanning()
|
|
316
|
+
} catch (e: IllegalStateException) {
|
|
317
|
+
Logger.error(TAG, "Error in requestDevice: ${e.localizedMessage}", e)
|
|
318
|
+
call.reject(e.localizedMessage)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
deviceScanner = DeviceScanner(
|
|
323
|
+
context,
|
|
324
|
+
bluetoothAdapter!!,
|
|
325
|
+
scanDuration = MAX_SCAN_DURATION,
|
|
326
|
+
displayStrings = displayStrings!!,
|
|
327
|
+
showDialog = true,
|
|
328
|
+
)
|
|
329
|
+
deviceScanner?.startScanning(
|
|
330
|
+
scanFilters, scanSettings, false, namePrefix, { scanResponse ->
|
|
331
|
+
run {
|
|
332
|
+
if (scanResponse.success) {
|
|
333
|
+
if (scanResponse.device == null) {
|
|
334
|
+
call.reject("No device found.")
|
|
335
|
+
} else {
|
|
336
|
+
val bleDevice = getBleDevice(scanResponse.device)
|
|
337
|
+
call.resolve(bleDevice)
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
call.reject(scanResponse.message)
|
|
341
|
+
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}, null
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@PluginMethod
|
|
349
|
+
fun requestLEScan(call: PluginCall) {
|
|
350
|
+
assertBluetoothAdapter(call) ?: return
|
|
351
|
+
val scanFilters = getScanFilters(call) ?: return
|
|
352
|
+
val scanSettings = getScanSettings(call) ?: return
|
|
353
|
+
val namePrefix = call.getString("namePrefix", "") as String
|
|
354
|
+
val allowDuplicates = call.getBoolean("allowDuplicates", false) as Boolean
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
deviceScanner?.stopScanning()
|
|
358
|
+
} catch (e: IllegalStateException) {
|
|
359
|
+
Logger.error(TAG, "Error in requestLEScan: ${e.localizedMessage}", e)
|
|
360
|
+
call.reject(e.localizedMessage)
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
deviceScanner = DeviceScanner(
|
|
365
|
+
context,
|
|
366
|
+
bluetoothAdapter!!,
|
|
367
|
+
scanDuration = null,
|
|
368
|
+
displayStrings = displayStrings!!,
|
|
369
|
+
showDialog = false,
|
|
370
|
+
)
|
|
371
|
+
deviceScanner?.startScanning(
|
|
372
|
+
scanFilters,
|
|
373
|
+
scanSettings,
|
|
374
|
+
allowDuplicates,
|
|
375
|
+
namePrefix,
|
|
376
|
+
{ scanResponse ->
|
|
377
|
+
run {
|
|
378
|
+
if (scanResponse.success) {
|
|
379
|
+
call.resolve()
|
|
380
|
+
} else {
|
|
381
|
+
call.reject(scanResponse.message)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
{ result ->
|
|
386
|
+
run {
|
|
387
|
+
val scanResult = getScanResult(result)
|
|
388
|
+
try {
|
|
389
|
+
notifyListeners("onScanResult", scanResult)
|
|
390
|
+
} catch (e: ConcurrentModificationException) {
|
|
391
|
+
Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@PluginMethod
|
|
398
|
+
fun stopLEScan(call: PluginCall) {
|
|
399
|
+
assertBluetoothAdapter(call) ?: return
|
|
400
|
+
try {
|
|
401
|
+
deviceScanner?.stopScanning()
|
|
402
|
+
} catch (e: IllegalStateException) {
|
|
403
|
+
Logger.error(TAG, "Error in stopLEScan: ${e.localizedMessage}", e)
|
|
404
|
+
}
|
|
405
|
+
call.resolve()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
@PluginMethod
|
|
409
|
+
fun getDevices(call: PluginCall) {
|
|
410
|
+
assertBluetoothAdapter(call) ?: return
|
|
411
|
+
val deviceIds = (call.getArray("deviceIds", JSArray()) as JSArray).toList<String>()
|
|
412
|
+
val bleDevices = JSArray()
|
|
413
|
+
deviceIds.forEach { deviceId ->
|
|
414
|
+
val bleDevice = JSObject()
|
|
415
|
+
bleDevice.put("deviceId", deviceId)
|
|
416
|
+
bleDevices.put(bleDevice)
|
|
417
|
+
}
|
|
418
|
+
val result = JSObject()
|
|
419
|
+
result.put("devices", bleDevices)
|
|
420
|
+
call.resolve(result)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
@PluginMethod
|
|
424
|
+
fun getConnectedDevices(call: PluginCall) {
|
|
425
|
+
assertBluetoothAdapter(call) ?: return
|
|
426
|
+
val bluetoothManager =
|
|
427
|
+
(activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager)
|
|
428
|
+
val devices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
|
|
429
|
+
val bleDevices = JSArray()
|
|
430
|
+
devices.forEach { device ->
|
|
431
|
+
bleDevices.put(getBleDevice(device))
|
|
432
|
+
}
|
|
433
|
+
val result = JSObject()
|
|
434
|
+
result.put("devices", bleDevices)
|
|
435
|
+
call.resolve(result)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@PluginMethod
|
|
439
|
+
fun getBondedDevices(call: PluginCall) {
|
|
440
|
+
assertBluetoothAdapter(call) ?: return
|
|
441
|
+
|
|
442
|
+
val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
|
443
|
+
val bluetoothAdapter = bluetoothManager.adapter
|
|
444
|
+
|
|
445
|
+
if (bluetoothAdapter == null) {
|
|
446
|
+
call.reject("Bluetooth is not supported on this device")
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
val bondedDevices = bluetoothAdapter.bondedDevices
|
|
451
|
+
val bleDevices = JSArray()
|
|
452
|
+
|
|
453
|
+
bondedDevices.forEach { device ->
|
|
454
|
+
bleDevices.put(getBleDevice(device))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
val result = JSObject()
|
|
458
|
+
result.put("devices", bleDevices)
|
|
459
|
+
call.resolve(result)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
@PluginMethod
|
|
463
|
+
fun connect(call: PluginCall) {
|
|
464
|
+
val device = getOrCreateDevice(call) ?: return
|
|
465
|
+
val timeout = call.getFloat("timeout", CONNECTION_TIMEOUT)!!.toLong()
|
|
466
|
+
device.connect(timeout) { response ->
|
|
467
|
+
run {
|
|
468
|
+
if (response.success) {
|
|
469
|
+
call.resolve()
|
|
470
|
+
} else {
|
|
471
|
+
call.reject(response.value)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private fun onDisconnect(deviceId: String) {
|
|
478
|
+
try {
|
|
479
|
+
notifyListeners("disconnected|${deviceId}", null)
|
|
480
|
+
} catch (e: ConcurrentModificationException) {
|
|
481
|
+
Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
@PluginMethod
|
|
486
|
+
fun createBond(call: PluginCall) {
|
|
487
|
+
val device = getOrCreateDevice(call) ?: return
|
|
488
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
489
|
+
device.createBond(timeout) { response ->
|
|
490
|
+
run {
|
|
491
|
+
if (response.success) {
|
|
492
|
+
call.resolve()
|
|
493
|
+
} else {
|
|
494
|
+
call.reject(response.value)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
@PluginMethod
|
|
501
|
+
fun isBonded(call: PluginCall) {
|
|
502
|
+
val device = getOrCreateDevice(call) ?: return
|
|
503
|
+
val isBonded = device.isBonded()
|
|
504
|
+
val result = JSObject()
|
|
505
|
+
result.put("value", isBonded)
|
|
506
|
+
call.resolve(result)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@PluginMethod
|
|
510
|
+
fun disconnect(call: PluginCall) {
|
|
511
|
+
val device = getOrCreateDevice(call) ?: return
|
|
512
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
513
|
+
device.disconnect(timeout) { response ->
|
|
514
|
+
run {
|
|
515
|
+
if (response.success) {
|
|
516
|
+
device.cleanup()
|
|
517
|
+
deviceMap.remove(device.getId())
|
|
518
|
+
call.resolve()
|
|
519
|
+
} else {
|
|
520
|
+
call.reject(response.value)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
@PluginMethod
|
|
527
|
+
fun getServices(call: PluginCall) {
|
|
528
|
+
val device = getDevice(call) ?: return
|
|
529
|
+
val services = device.getServices()
|
|
530
|
+
val bleServices = JSArray()
|
|
531
|
+
services.forEach { service ->
|
|
532
|
+
val bleCharacteristics = JSArray()
|
|
533
|
+
service.characteristics.forEach { characteristic ->
|
|
534
|
+
val bleCharacteristic = JSObject()
|
|
535
|
+
bleCharacteristic.put("uuid", characteristic.uuid)
|
|
536
|
+
bleCharacteristic.put("properties", getProperties(characteristic))
|
|
537
|
+
val bleDescriptors = JSArray()
|
|
538
|
+
characteristic.descriptors.forEach { descriptor ->
|
|
539
|
+
val bleDescriptor = JSObject()
|
|
540
|
+
bleDescriptor.put("uuid", descriptor.uuid)
|
|
541
|
+
bleDescriptors.put(bleDescriptor)
|
|
542
|
+
}
|
|
543
|
+
bleCharacteristic.put("descriptors", bleDescriptors)
|
|
544
|
+
bleCharacteristics.put(bleCharacteristic)
|
|
545
|
+
}
|
|
546
|
+
val bleService = JSObject()
|
|
547
|
+
bleService.put("uuid", service.uuid)
|
|
548
|
+
bleService.put("characteristics", bleCharacteristics)
|
|
549
|
+
bleServices.put(bleService)
|
|
550
|
+
}
|
|
551
|
+
val ret = JSObject()
|
|
552
|
+
ret.put("services", bleServices)
|
|
553
|
+
call.resolve(ret)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private fun getProperties(characteristic: BluetoothGattCharacteristic): JSObject {
|
|
557
|
+
val properties = JSObject()
|
|
558
|
+
properties.put(
|
|
559
|
+
"broadcast",
|
|
560
|
+
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_BROADCAST > 0
|
|
561
|
+
)
|
|
562
|
+
properties.put(
|
|
563
|
+
"read", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ > 0
|
|
564
|
+
)
|
|
565
|
+
properties.put(
|
|
566
|
+
"writeWithoutResponse",
|
|
567
|
+
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0
|
|
568
|
+
)
|
|
569
|
+
properties.put(
|
|
570
|
+
"write", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0
|
|
571
|
+
)
|
|
572
|
+
properties.put(
|
|
573
|
+
"notify", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
|
|
574
|
+
)
|
|
575
|
+
properties.put(
|
|
576
|
+
"indicate",
|
|
577
|
+
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0
|
|
578
|
+
)
|
|
579
|
+
properties.put(
|
|
580
|
+
"authenticatedSignedWrites",
|
|
581
|
+
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE > 0
|
|
582
|
+
)
|
|
583
|
+
properties.put(
|
|
584
|
+
"extendedProperties",
|
|
585
|
+
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS > 0
|
|
586
|
+
)
|
|
587
|
+
return properties
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
@PluginMethod
|
|
591
|
+
fun discoverServices(call: PluginCall) {
|
|
592
|
+
val device = getDevice(call) ?: return
|
|
593
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
594
|
+
device.discoverServices(timeout) { response ->
|
|
595
|
+
run {
|
|
596
|
+
if (response.success) {
|
|
597
|
+
call.resolve()
|
|
598
|
+
} else {
|
|
599
|
+
call.reject(response.value)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
@PluginMethod
|
|
606
|
+
fun getMtu(call: PluginCall) {
|
|
607
|
+
val device = getDevice(call) ?: return
|
|
608
|
+
val mtu = device.getMtu()
|
|
609
|
+
val ret = JSObject()
|
|
610
|
+
ret.put("value", mtu)
|
|
611
|
+
call.resolve(ret)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
@PluginMethod
|
|
615
|
+
fun requestConnectionPriority(call: PluginCall) {
|
|
616
|
+
val device = getDevice(call) ?: return
|
|
617
|
+
val connectionPriority = call.getInt("connectionPriority", -1) as Int
|
|
618
|
+
if (connectionPriority < BluetoothGatt.CONNECTION_PRIORITY_BALANCED || connectionPriority > BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
|
|
619
|
+
call.reject("Invalid connectionPriority.")
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
val result = device.requestConnectionPriority(connectionPriority)
|
|
624
|
+
if (result) {
|
|
625
|
+
call.resolve()
|
|
626
|
+
} else {
|
|
627
|
+
call.reject("requestConnectionPriority failed.")
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
@PluginMethod
|
|
632
|
+
fun readRssi(call: PluginCall) {
|
|
633
|
+
val device = getDevice(call) ?: return
|
|
634
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
635
|
+
device.readRssi(timeout) { response ->
|
|
636
|
+
run {
|
|
637
|
+
if (response.success) {
|
|
638
|
+
val ret = JSObject()
|
|
639
|
+
ret.put("value", response.value)
|
|
640
|
+
call.resolve(ret)
|
|
641
|
+
} else {
|
|
642
|
+
call.reject(response.value)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
@PluginMethod
|
|
649
|
+
fun read(call: PluginCall) {
|
|
650
|
+
val device = getDevice(call) ?: return
|
|
651
|
+
val characteristic = getCharacteristic(call) ?: return
|
|
652
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
653
|
+
device.read(characteristic.first, characteristic.second, timeout) { response ->
|
|
654
|
+
run {
|
|
655
|
+
if (response.success) {
|
|
656
|
+
val ret = JSObject()
|
|
657
|
+
ret.put("value", response.value)
|
|
658
|
+
call.resolve(ret)
|
|
659
|
+
} else {
|
|
660
|
+
call.reject(response.value)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
@PluginMethod
|
|
667
|
+
fun write(call: PluginCall) {
|
|
668
|
+
val device = getDevice(call) ?: return
|
|
669
|
+
val characteristic = getCharacteristic(call) ?: return
|
|
670
|
+
val value = call.getString("value", null)
|
|
671
|
+
if (value == null) {
|
|
672
|
+
call.reject("Value required.")
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
val writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
676
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
677
|
+
device.write(
|
|
678
|
+
characteristic.first, characteristic.second, value, writeType, timeout
|
|
679
|
+
) { response ->
|
|
680
|
+
run {
|
|
681
|
+
if (response.success) {
|
|
682
|
+
call.resolve()
|
|
683
|
+
} else {
|
|
684
|
+
call.reject(response.value)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
@PluginMethod
|
|
691
|
+
fun writeWithoutResponse(call: PluginCall) {
|
|
692
|
+
val device = getDevice(call) ?: return
|
|
693
|
+
val characteristic = getCharacteristic(call) ?: return
|
|
694
|
+
val value = call.getString("value", null)
|
|
695
|
+
if (value == null) {
|
|
696
|
+
call.reject("Value required.")
|
|
697
|
+
return
|
|
698
|
+
}
|
|
699
|
+
val writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
700
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
701
|
+
device.write(
|
|
702
|
+
characteristic.first, characteristic.second, value, writeType, timeout
|
|
703
|
+
) { response ->
|
|
704
|
+
run {
|
|
705
|
+
if (response.success) {
|
|
706
|
+
call.resolve()
|
|
707
|
+
} else {
|
|
708
|
+
call.reject(response.value)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
@PluginMethod
|
|
715
|
+
fun readDescriptor(call: PluginCall) {
|
|
716
|
+
val device = getDevice(call) ?: return
|
|
717
|
+
val descriptor = getDescriptor(call) ?: return
|
|
718
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
719
|
+
device.readDescriptor(
|
|
720
|
+
descriptor.first, descriptor.second, descriptor.third, timeout
|
|
721
|
+
) { response ->
|
|
722
|
+
run {
|
|
723
|
+
if (response.success) {
|
|
724
|
+
val ret = JSObject()
|
|
725
|
+
ret.put("value", response.value)
|
|
726
|
+
call.resolve(ret)
|
|
727
|
+
} else {
|
|
728
|
+
call.reject(response.value)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
@PluginMethod
|
|
735
|
+
fun writeDescriptor(call: PluginCall) {
|
|
736
|
+
val device = getDevice(call) ?: return
|
|
737
|
+
val descriptor = getDescriptor(call) ?: return
|
|
738
|
+
val value = call.getString("value", null)
|
|
739
|
+
if (value == null) {
|
|
740
|
+
call.reject("Value required.")
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
744
|
+
device.writeDescriptor(
|
|
745
|
+
descriptor.first, descriptor.second, descriptor.third, value, timeout
|
|
746
|
+
) { response ->
|
|
747
|
+
run {
|
|
748
|
+
if (response.success) {
|
|
749
|
+
call.resolve()
|
|
750
|
+
} else {
|
|
751
|
+
call.reject(response.value)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
@PluginMethod
|
|
758
|
+
fun startNotifications(call: PluginCall) {
|
|
759
|
+
val device = getDevice(call) ?: return
|
|
760
|
+
val characteristic = getCharacteristic(call) ?: return
|
|
761
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
762
|
+
device.setNotifications(characteristic.first, characteristic.second, true, { response ->
|
|
763
|
+
run {
|
|
764
|
+
val key =
|
|
765
|
+
"notification|${device.getId()}|${(characteristic.first)}|${(characteristic.second)}"
|
|
766
|
+
val ret = JSObject()
|
|
767
|
+
ret.put("value", response.value)
|
|
768
|
+
try {
|
|
769
|
+
notifyListeners(key, ret)
|
|
770
|
+
} catch (e: ConcurrentModificationException) {
|
|
771
|
+
Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}, timeout, { response ->
|
|
775
|
+
run {
|
|
776
|
+
if (response.success) {
|
|
777
|
+
call.resolve()
|
|
778
|
+
} else {
|
|
779
|
+
call.reject(response.value)
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
@PluginMethod
|
|
786
|
+
fun stopNotifications(call: PluginCall) {
|
|
787
|
+
val device = getDevice(call) ?: return
|
|
788
|
+
val characteristic = getCharacteristic(call) ?: return
|
|
789
|
+
val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
|
|
790
|
+
device.setNotifications(
|
|
791
|
+
characteristic.first, characteristic.second, false, null, timeout
|
|
792
|
+
) { response ->
|
|
793
|
+
run {
|
|
794
|
+
if (response.success) {
|
|
795
|
+
call.resolve()
|
|
796
|
+
} else {
|
|
797
|
+
call.reject(response.value)
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private fun assertBluetoothAdapter(call: PluginCall): Boolean? {
|
|
804
|
+
if (bluetoothAdapter == null) {
|
|
805
|
+
call.reject("Bluetooth LE not initialized.")
|
|
806
|
+
return null
|
|
807
|
+
}
|
|
808
|
+
return true
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private fun getScanFilters(call: PluginCall): List<ScanFilter>? {
|
|
812
|
+
val filters: ArrayList<ScanFilter> = ArrayList()
|
|
813
|
+
|
|
814
|
+
val services = (call.getArray("services", JSArray()) as JSArray).toList<String>()
|
|
815
|
+
val manufacturerDataArray = call.getArray("manufacturerData", JSArray())
|
|
816
|
+
val serviceDataArray = call.getArray("serviceData", JSArray())
|
|
817
|
+
val name = call.getString("name", null)
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
// Create filters based on services
|
|
821
|
+
for (service in services) {
|
|
822
|
+
val filter = ScanFilter.Builder()
|
|
823
|
+
filter.setServiceUuid(ParcelUuid.fromString(service))
|
|
824
|
+
if (name != null) {
|
|
825
|
+
filter.setDeviceName(name)
|
|
826
|
+
}
|
|
827
|
+
filters.add(filter.build())
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Service Data Handling (for filtering by service data like OpenDroneID)
|
|
831
|
+
serviceDataArray?.let {
|
|
832
|
+
for (i in 0 until it.length()) {
|
|
833
|
+
val serviceDataObject = it.getJSONObject(i)
|
|
834
|
+
|
|
835
|
+
val serviceUuid = serviceDataObject.getString("serviceUuid")
|
|
836
|
+
val servicePuuid = ParcelUuid.fromString(serviceUuid)
|
|
837
|
+
|
|
838
|
+
val dataPrefix = if (serviceDataObject.has("dataPrefix")) {
|
|
839
|
+
val dataPrefixString = serviceDataObject.getString("dataPrefix")
|
|
840
|
+
stringToBytes(dataPrefixString)
|
|
841
|
+
} else null
|
|
842
|
+
|
|
843
|
+
val mask = if (serviceDataObject.has("mask")) {
|
|
844
|
+
val maskString = serviceDataObject.getString("mask")
|
|
845
|
+
stringToBytes(maskString)
|
|
846
|
+
} else null
|
|
847
|
+
|
|
848
|
+
val filterBuilder = ScanFilter.Builder()
|
|
849
|
+
|
|
850
|
+
if (dataPrefix != null && mask != null) {
|
|
851
|
+
filterBuilder.setServiceData(servicePuuid, dataPrefix, mask)
|
|
852
|
+
} else if (dataPrefix != null) {
|
|
853
|
+
filterBuilder.setServiceData(servicePuuid, dataPrefix)
|
|
854
|
+
} else {
|
|
855
|
+
// Set service data filter without data (just match the service UUID)
|
|
856
|
+
filterBuilder.setServiceData(servicePuuid, byteArrayOf())
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (name != null) {
|
|
860
|
+
filterBuilder.setDeviceName(name)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
filters.add(filterBuilder.build())
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Manufacturer Data Handling (with optional parameters)
|
|
868
|
+
manufacturerDataArray?.let {
|
|
869
|
+
for (i in 0 until it.length()) {
|
|
870
|
+
val manufacturerDataObject = it.getJSONObject(i)
|
|
871
|
+
|
|
872
|
+
val companyIdentifier = manufacturerDataObject.getInt("companyIdentifier")
|
|
873
|
+
|
|
874
|
+
val dataPrefix = if (manufacturerDataObject.has("dataPrefix")) {
|
|
875
|
+
val dataPrefixString = manufacturerDataObject.getString("dataPrefix")
|
|
876
|
+
stringToBytes(dataPrefixString)
|
|
877
|
+
} else null
|
|
878
|
+
|
|
879
|
+
val mask = if (manufacturerDataObject.has("mask")) {
|
|
880
|
+
val maskString = manufacturerDataObject.getString("mask")
|
|
881
|
+
stringToBytes(maskString)
|
|
882
|
+
} else null
|
|
883
|
+
|
|
884
|
+
val filterBuilder = ScanFilter.Builder()
|
|
885
|
+
|
|
886
|
+
if (dataPrefix != null && mask != null) {
|
|
887
|
+
filterBuilder.setManufacturerData(companyIdentifier, dataPrefix, mask)
|
|
888
|
+
} else if (dataPrefix != null) {
|
|
889
|
+
filterBuilder.setManufacturerData(companyIdentifier, dataPrefix)
|
|
890
|
+
} else {
|
|
891
|
+
// Android requires at least dataPrefix for manufacturer filters.
|
|
892
|
+
call.reject("dataPrefix is required when specifying manufacturerData.")
|
|
893
|
+
return null
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (name != null) {
|
|
897
|
+
filterBuilder.setDeviceName(name)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
filters.add(filterBuilder.build())
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
// Create filters when providing only name
|
|
904
|
+
if (name != null && filters.isEmpty()) {
|
|
905
|
+
val filterBuilder = ScanFilter.Builder()
|
|
906
|
+
filterBuilder.setDeviceName(name)
|
|
907
|
+
filters.add(filterBuilder.build())
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return filters;
|
|
911
|
+
} catch (e: IllegalArgumentException) {
|
|
912
|
+
call.reject("Invalid UUID or Manufacturer data provided.")
|
|
913
|
+
return null
|
|
914
|
+
} catch (e: Exception) {
|
|
915
|
+
call.reject("Invalid or malformed filter data provided.")
|
|
916
|
+
return null
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private fun getScanSettings(call: PluginCall): ScanSettings? {
|
|
921
|
+
val scanSettings = ScanSettings.Builder()
|
|
922
|
+
val scanMode = call.getInt("scanMode", ScanSettings.SCAN_MODE_BALANCED) as Int
|
|
923
|
+
try {
|
|
924
|
+
scanSettings.setScanMode(scanMode)
|
|
925
|
+
} catch (e: IllegalArgumentException) {
|
|
926
|
+
call.reject("Invalid scan mode.")
|
|
927
|
+
return null
|
|
928
|
+
}
|
|
929
|
+
return scanSettings.build()
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private fun getBleDevice(device: BluetoothDevice): JSObject {
|
|
933
|
+
val bleDevice = JSObject()
|
|
934
|
+
bleDevice.put("deviceId", device.address)
|
|
935
|
+
if (device.name != null) {
|
|
936
|
+
bleDevice.put("name", device.name)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
val uuids = JSArray()
|
|
940
|
+
device.uuids?.forEach { uuid -> uuids.put(uuid.toString()) }
|
|
941
|
+
if (uuids.length() > 0) {
|
|
942
|
+
bleDevice.put("uuids", uuids)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return bleDevice
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private fun getScanResult(result: ScanResult): JSObject {
|
|
949
|
+
val scanResult = JSObject()
|
|
950
|
+
|
|
951
|
+
val bleDevice = getBleDevice(result.device)
|
|
952
|
+
scanResult.put("device", bleDevice)
|
|
953
|
+
|
|
954
|
+
if (result.device.name != null) {
|
|
955
|
+
scanResult.put("localName", result.device.name)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
scanResult.put("rssi", result.rssi)
|
|
959
|
+
|
|
960
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
961
|
+
scanResult.put("txPower", result.txPower)
|
|
962
|
+
} else {
|
|
963
|
+
scanResult.put("txPower", 127)
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
val manufacturerData = JSObject()
|
|
967
|
+
val manufacturerSpecificData = result.scanRecord?.manufacturerSpecificData
|
|
968
|
+
if (manufacturerSpecificData != null) {
|
|
969
|
+
for (i in 0 until manufacturerSpecificData.size()) {
|
|
970
|
+
val key = manufacturerSpecificData.keyAt(i)
|
|
971
|
+
val bytes = manufacturerSpecificData.get(key)
|
|
972
|
+
manufacturerData.put(key.toString(), bytesToString(bytes))
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
scanResult.put("manufacturerData", manufacturerData)
|
|
976
|
+
|
|
977
|
+
val serviceDataObject = JSObject()
|
|
978
|
+
val serviceData = result.scanRecord?.serviceData
|
|
979
|
+
serviceData?.forEach {
|
|
980
|
+
serviceDataObject.put(it.key.toString(), bytesToString(it.value))
|
|
981
|
+
}
|
|
982
|
+
scanResult.put("serviceData", serviceDataObject)
|
|
983
|
+
|
|
984
|
+
val uuids = JSArray()
|
|
985
|
+
result.scanRecord?.serviceUuids?.forEach { uuid -> uuids.put(uuid.toString()) }
|
|
986
|
+
scanResult.put("uuids", uuids)
|
|
987
|
+
|
|
988
|
+
scanResult.put("rawAdvertisement", result.scanRecord?.bytes?.let { bytesToString(it) })
|
|
989
|
+
return scanResult
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private fun getDisplayStrings(): DisplayStrings {
|
|
993
|
+
return DisplayStrings(
|
|
994
|
+
config.getString(
|
|
995
|
+
"displayStrings.scanning", "Scanning..."
|
|
996
|
+
),
|
|
997
|
+
config.getString(
|
|
998
|
+
"displayStrings.cancel", "Cancel"
|
|
999
|
+
),
|
|
1000
|
+
config.getString(
|
|
1001
|
+
"displayStrings.availableDevices", "Available devices"
|
|
1002
|
+
),
|
|
1003
|
+
config.getString(
|
|
1004
|
+
"displayStrings.noDeviceFound", "No device found"
|
|
1005
|
+
),
|
|
1006
|
+
)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private fun getDeviceId(call: PluginCall): String? {
|
|
1010
|
+
val deviceId = call.getString("deviceId", null)
|
|
1011
|
+
if (deviceId == null) {
|
|
1012
|
+
call.reject("deviceId required.")
|
|
1013
|
+
return null
|
|
1014
|
+
}
|
|
1015
|
+
return deviceId
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
private fun getOrCreateDevice(call: PluginCall): Device? {
|
|
1019
|
+
assertBluetoothAdapter(call) ?: return null
|
|
1020
|
+
val deviceId = getDeviceId(call) ?: return null
|
|
1021
|
+
val device = deviceMap[deviceId]
|
|
1022
|
+
if (device != null) {
|
|
1023
|
+
return device
|
|
1024
|
+
}
|
|
1025
|
+
return try {
|
|
1026
|
+
val newDevice = Device(
|
|
1027
|
+
activity.applicationContext, bluetoothAdapter!!, deviceId
|
|
1028
|
+
) {
|
|
1029
|
+
onDisconnect(deviceId)
|
|
1030
|
+
}
|
|
1031
|
+
deviceMap[deviceId] = newDevice
|
|
1032
|
+
newDevice
|
|
1033
|
+
} catch (e: IllegalArgumentException) {
|
|
1034
|
+
call.reject("Invalid deviceId")
|
|
1035
|
+
null
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
private fun getDevice(call: PluginCall): Device? {
|
|
1040
|
+
assertBluetoothAdapter(call) ?: return null
|
|
1041
|
+
val deviceId = getDeviceId(call) ?: return null
|
|
1042
|
+
val device = deviceMap[deviceId]
|
|
1043
|
+
if (device == null || !device.isConnected()) {
|
|
1044
|
+
call.reject("Not connected to device.")
|
|
1045
|
+
return null
|
|
1046
|
+
}
|
|
1047
|
+
return device
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private fun getCharacteristic(call: PluginCall): Pair<UUID, UUID>? {
|
|
1051
|
+
val serviceString = call.getString("service", null)
|
|
1052
|
+
val serviceUUID: UUID?
|
|
1053
|
+
try {
|
|
1054
|
+
serviceUUID = UUID.fromString(serviceString)
|
|
1055
|
+
} catch (e: IllegalArgumentException) {
|
|
1056
|
+
call.reject("Invalid service UUID.")
|
|
1057
|
+
return null
|
|
1058
|
+
}
|
|
1059
|
+
if (serviceUUID == null) {
|
|
1060
|
+
call.reject("Service UUID required.")
|
|
1061
|
+
return null
|
|
1062
|
+
}
|
|
1063
|
+
val characteristicString = call.getString("characteristic", null)
|
|
1064
|
+
val characteristicUUID: UUID?
|
|
1065
|
+
try {
|
|
1066
|
+
characteristicUUID = UUID.fromString(characteristicString)
|
|
1067
|
+
} catch (e: IllegalArgumentException) {
|
|
1068
|
+
call.reject("Invalid characteristic UUID.")
|
|
1069
|
+
return null
|
|
1070
|
+
}
|
|
1071
|
+
if (characteristicUUID == null) {
|
|
1072
|
+
call.reject("Characteristic UUID required.")
|
|
1073
|
+
return null
|
|
1074
|
+
}
|
|
1075
|
+
return Pair(serviceUUID, characteristicUUID)
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private fun getDescriptor(call: PluginCall): Triple<UUID, UUID, UUID>? {
|
|
1079
|
+
val characteristic = getCharacteristic(call) ?: return null
|
|
1080
|
+
val descriptorString = call.getString("descriptor", null)
|
|
1081
|
+
val descriptorUUID: UUID?
|
|
1082
|
+
try {
|
|
1083
|
+
descriptorUUID = UUID.fromString(descriptorString)
|
|
1084
|
+
} catch (e: IllegalAccessException) {
|
|
1085
|
+
call.reject("Invalid descriptor UUID.")
|
|
1086
|
+
return null
|
|
1087
|
+
}
|
|
1088
|
+
if (descriptorUUID == null) {
|
|
1089
|
+
call.reject("Descriptor UUID required.")
|
|
1090
|
+
return null
|
|
1091
|
+
}
|
|
1092
|
+
return Triple(characteristic.first, characteristic.second, descriptorUUID)
|
|
1093
|
+
}
|
|
1094
|
+
}
|