munim-bluetooth 0.3.26 → 0.3.27

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.
@@ -0,0 +1,40 @@
1
+ package com.munimbluetooth
2
+
3
+ import android.Manifest
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.os.Build
7
+
8
+ internal object BluetoothPermissionUtils {
9
+ fun requiredPermissions(): Array<String> {
10
+ return when {
11
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> arrayOf(
12
+ Manifest.permission.BLUETOOTH_SCAN,
13
+ Manifest.permission.BLUETOOTH_CONNECT,
14
+ Manifest.permission.BLUETOOTH_ADVERTISE
15
+ )
16
+
17
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> arrayOf(
18
+ Manifest.permission.ACCESS_FINE_LOCATION
19
+ )
20
+
21
+ else -> emptyArray()
22
+ }
23
+ }
24
+
25
+ fun missingPermissions(context: Context): Array<String> {
26
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
27
+ return emptyArray()
28
+ }
29
+
30
+ return requiredPermissions()
31
+ .filter { permission ->
32
+ context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED
33
+ }
34
+ .toTypedArray()
35
+ }
36
+
37
+ fun hasRequiredPermissions(context: Context): Boolean {
38
+ return missingPermissions(context).isEmpty()
39
+ }
40
+ }
@@ -23,6 +23,7 @@ import android.bluetooth.le.ScanResult
23
23
  import android.bluetooth.le.ScanSettings
24
24
  import android.content.Context
25
25
  import android.content.Intent
26
+ import android.content.pm.PackageManager
26
27
  import android.os.Build
27
28
  import android.os.ParcelUuid
28
29
  import android.util.Log
@@ -30,6 +31,8 @@ import com.facebook.react.bridge.Arguments
30
31
  import com.facebook.react.bridge.WritableArray
31
32
  import com.facebook.react.bridge.WritableMap
32
33
  import com.facebook.react.modules.core.DeviceEventManagerModule
34
+ import com.facebook.react.modules.core.PermissionAwareActivity
35
+ import com.facebook.react.modules.core.PermissionListener
33
36
  import com.margelo.nitro.NitroModules
34
37
  import com.margelo.nitro.core.Promise
35
38
  import com.margelo.nitro.munimbluetooth.AdvertisingDataTypes
@@ -80,6 +83,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
80
83
  private val lastCharacteristicValues = mutableMapOf<String, CharacteristicValue>()
81
84
  private val lastRssiValues = mutableMapOf<String, Double>()
82
85
  private val eventEmitter = NitroEventEmitter(TAG)
86
+ private var nextPermissionRequestCode = BLUETOOTH_PERMISSION_REQUEST_CODE
83
87
 
84
88
  private fun getBluetoothManager(): BluetoothManager? {
85
89
  val context = NitroModules.applicationContext ?: return null
@@ -93,7 +97,35 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
93
97
  }
94
98
  }
95
99
 
100
+ private fun hasRequiredBluetoothPermissions(): Boolean {
101
+ val context = NitroModules.applicationContext ?: return false
102
+ return BluetoothPermissionUtils.hasRequiredPermissions(context)
103
+ }
104
+
105
+ private fun ensureBluetoothPermissions(operationName: String): Boolean {
106
+ val context = NitroModules.applicationContext
107
+ if (context == null) {
108
+ Log.w(TAG, "Unable to $operationName: React context unavailable")
109
+ return false
110
+ }
111
+
112
+ val missingPermissions = BluetoothPermissionUtils.missingPermissions(context)
113
+ if (missingPermissions.isNotEmpty()) {
114
+ Log.w(
115
+ TAG,
116
+ "Unable to $operationName: missing Bluetooth permissions (${missingPermissions.joinToString()})"
117
+ )
118
+ return false
119
+ }
120
+
121
+ return true
122
+ }
123
+
96
124
  override fun startAdvertising(options: AdvertisingOptions) {
125
+ if (!ensureBluetoothPermissions("start advertising")) {
126
+ return
127
+ }
128
+
97
129
  ensureBluetoothManager()
98
130
  val adapter = bluetoothAdapter
99
131
  if (adapter == null || !adapter.isEnabled) {
@@ -115,7 +147,12 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
115
147
  )
116
148
 
117
149
  if (!currentLocalName.isNullOrBlank() && previousAdapterName == null) {
118
- previousAdapterName = adapter.name
150
+ previousAdapterName = try {
151
+ adapter.name
152
+ } catch (error: SecurityException) {
153
+ Log.w(TAG, "Unable to read Bluetooth adapter name", error)
154
+ null
155
+ }
119
156
  }
120
157
  if (!currentLocalName.isNullOrBlank()) {
121
158
  try {
@@ -161,6 +198,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
161
198
  }
162
199
 
163
200
  override fun setServices(services: Array<GATTService>) {
201
+ if (!ensureBluetoothPermissions("set GATT services")) {
202
+ return
203
+ }
204
+
164
205
  ensureBluetoothManager()
165
206
  gattServerReady = false
166
207
 
@@ -197,15 +238,63 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
197
238
  }
198
239
 
199
240
  override fun isBluetoothEnabled(): Promise<Boolean> {
241
+ if (!hasRequiredBluetoothPermissions()) {
242
+ return Promise.resolved(false)
243
+ }
244
+
200
245
  ensureBluetoothManager()
201
246
  return Promise.resolved(bluetoothAdapter?.isEnabled == true)
202
247
  }
203
248
 
204
249
  override fun requestBluetoothPermission(): Promise<Boolean> {
205
- return Promise.resolved(true)
250
+ val context = NitroModules.applicationContext ?: run {
251
+ Log.w(TAG, "Unable to request Bluetooth permissions: React context unavailable")
252
+ return Promise.resolved(false)
253
+ }
254
+
255
+ val missingPermissions = BluetoothPermissionUtils.missingPermissions(context)
256
+ if (missingPermissions.isEmpty()) {
257
+ return Promise.resolved(true)
258
+ }
259
+
260
+ val activity = context.currentActivity as? PermissionAwareActivity
261
+ if (activity == null) {
262
+ Log.w(TAG, "Unable to request Bluetooth permissions: current activity unavailable")
263
+ return Promise.resolved(false)
264
+ }
265
+
266
+ val requestCode = nextPermissionRequestCode++
267
+ val promise = Promise<Boolean>()
268
+
269
+ try {
270
+ activity.requestPermissions(
271
+ missingPermissions,
272
+ requestCode,
273
+ PermissionListener { callbackRequestCode, _, grantResults ->
274
+ if (callbackRequestCode != requestCode) {
275
+ return@PermissionListener false
276
+ }
277
+
278
+ val isGranted =
279
+ grantResults.isNotEmpty() &&
280
+ grantResults.all { it == PackageManager.PERMISSION_GRANTED }
281
+ promise.resolve(isGranted)
282
+ true
283
+ }
284
+ )
285
+ } catch (error: IllegalStateException) {
286
+ Log.w(TAG, "Unable to request Bluetooth permissions", error)
287
+ promise.resolve(false)
288
+ }
289
+
290
+ return promise
206
291
  }
207
292
 
208
293
  override fun startScan(options: ScanOptions?) {
294
+ if (!ensureBluetoothPermissions("start scanning")) {
295
+ return
296
+ }
297
+
209
298
  ensureBluetoothManager()
210
299
  val adapter = bluetoothAdapter
211
300
  if (adapter == null || !adapter.isEnabled) {
@@ -278,6 +367,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
278
367
  }
279
368
 
280
369
  override fun connect(deviceId: String): Promise<Unit> {
370
+ if (!ensureBluetoothPermissions("connect to BLE device")) {
371
+ return Promise.rejected(IllegalStateException("Bluetooth permissions not granted"))
372
+ }
373
+
281
374
  ensureBluetoothManager()
282
375
  connectedDevices[deviceId]?.let { existingGatt ->
283
376
  if (existingGatt.services != null) {
@@ -453,6 +546,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
453
546
  return
454
547
  }
455
548
 
549
+ if (!ensureBluetoothPermissions("start background BLE session")) {
550
+ return
551
+ }
552
+
456
553
  val intent = Intent(context, MunimBluetoothBackgroundService::class.java).apply {
457
554
  action = MunimBluetoothBackgroundService.ACTION_START
458
555
  putExtra(
@@ -517,6 +614,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
517
614
  }
518
615
 
519
616
  private fun restartAdvertising(delayMs: Long) {
617
+ if (!ensureBluetoothPermissions("restart advertising")) {
618
+ return
619
+ }
620
+
520
621
  ensureBluetoothManager()
521
622
  val adapter = bluetoothAdapter
522
623
  if (adapter == null || !adapter.isEnabled) {
@@ -1027,6 +1128,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
1027
1128
 
1028
1129
  companion object {
1029
1130
  private const val TAG = "HybridMunimBluetooth"
1131
+ private const val BLUETOOTH_PERMISSION_REQUEST_CODE = 9137
1030
1132
  private val CLIENT_CHARACTERISTIC_CONFIG_UUID =
1031
1133
  UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
1032
1134
  }
@@ -100,19 +100,38 @@ class MunimBluetoothBackgroundService : Service() {
100
100
  allowDuplicates: Boolean,
101
101
  scanMode: ScanMode
102
102
  ) {
103
+ if (!BluetoothPermissionUtils.hasRequiredPermissions(applicationContext)) {
104
+ Log.w(TAG, "Unable to start background BLE session: missing runtime permissions")
105
+ stopSelf()
106
+ return
107
+ }
108
+
103
109
  bluetoothManager =
104
110
  applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
105
111
  bluetoothAdapter = bluetoothManager?.adapter
106
112
 
107
113
  val adapter = bluetoothAdapter
108
- if (adapter == null || !adapter.isEnabled) {
114
+ val isEnabled = try {
115
+ adapter?.isEnabled == true
116
+ } catch (error: SecurityException) {
117
+ Log.w(TAG, "Unable to inspect Bluetooth adapter state for background session", error)
118
+ false
119
+ }
120
+
121
+ if (adapter == null || !isEnabled) {
109
122
  Log.w(TAG, "Unable to start background BLE session: Bluetooth unavailable")
110
123
  stopSelf()
111
124
  return
112
125
  }
113
126
 
114
127
  if (!localName.isNullOrBlank() && previousAdapterName == null) {
115
- previousAdapterName = adapter.name
128
+ previousAdapterName = try {
129
+ adapter.name
130
+ } catch (error: SecurityException) {
131
+ Log.w(TAG, "Unable to read adapter name for background advertising", error)
132
+ null
133
+ }
134
+
116
135
  try {
117
136
  adapter.name = localName
118
137
  } catch (error: SecurityException) {
@@ -193,7 +212,11 @@ class MunimBluetoothBackgroundService : Service() {
193
212
  }
194
213
  }
195
214
 
196
- activeAdvertiser.startAdvertising(settings, data.build(), advertiseCallback)
215
+ try {
216
+ activeAdvertiser.startAdvertising(settings, data.build(), advertiseCallback)
217
+ } catch (error: SecurityException) {
218
+ Log.w(TAG, "Unable to start background advertising", error)
219
+ }
197
220
  }
198
221
 
199
222
  private fun startScan(
@@ -242,7 +265,11 @@ class MunimBluetoothBackgroundService : Service() {
242
265
  }
243
266
  }
244
267
 
245
- activeScanner.startScan(filters, settingsBuilder.build(), scanCallback)
268
+ try {
269
+ activeScanner.startScan(filters, settingsBuilder.build(), scanCallback)
270
+ } catch (error: SecurityException) {
271
+ Log.w(TAG, "Unable to start background scan", error)
272
+ }
246
273
  }
247
274
 
248
275
  private fun handleScanResult(result: ScanResult, allowDuplicates: Boolean) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "munim-bluetooth",
3
- "version": "0.3.26",
3
+ "version": "0.3.27",
4
4
  "description": "A comprehensive React Native library for all your Bluetooth Low Energy (BLE) needs, supporting both peripheral and central roles with Expo support",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",