react-native-flic2 2.0.0-alpha.39 → 2.0.0-beta.10

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.
@@ -26,7 +26,7 @@ def getExtOrIntegerDefault(name) {
26
26
  }
27
27
 
28
28
  android {
29
- namespace "com.flic2"
29
+ namespace "nl.xguard.flic2"
30
30
 
31
31
  compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
32
32
 
@@ -15,6 +15,7 @@
15
15
  <!-- Foreground service permission -->
16
16
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
17
17
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
18
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
18
19
 
19
20
  <application>
20
21
  <service
@@ -22,6 +23,28 @@
22
23
  android:enabled="true"
23
24
  android:exported="false"
24
25
  android:foregroundServiceType="connectedDevice" />
26
+
27
+ <receiver
28
+ android:name=".Flic2Service$BootUpReceiver"
29
+ android:enabled="true"
30
+ android:permission="android.permission.RECEIVE_BOOT_COMPLETED"
31
+ android:exported="false">
32
+ <intent-filter>
33
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
34
+ <category android:name="android.intent.category.DEFAULT" />
35
+ </intent-filter>
36
+ </receiver>
37
+
38
+ <receiver
39
+ android:name=".Flic2Service$UpdateReceiver"
40
+ android:enabled="true"
41
+ android:exported="false">
42
+ <intent-filter>
43
+ <action android:name="android.intent.action.PACKAGE_REPLACED" />
44
+ <data
45
+ android:scheme="package" />
46
+ </intent-filter>
47
+ </receiver>
25
48
  </application>
26
49
 
27
50
  </manifest>
@@ -0,0 +1,29 @@
1
+ package nl.xguard.flic2
2
+
3
+ import android.app.ActivityManager
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.os.Build
7
+
8
+ object ActivityUtil {
9
+ private const val TAG = "ActivityUtil"
10
+
11
+ fun isServiceRunning(context: Context, serviceClass: Class<*>): Boolean {
12
+ val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
13
+ for (service in manager.getRunningServices(Integer.MAX_VALUE)) {
14
+ if (serviceClass.name == service.service.className) {
15
+ return true
16
+ }
17
+ }
18
+ return false
19
+ }
20
+
21
+ fun startForegroundService(context: Context, intent: Intent) {
22
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
23
+ context.startForegroundService(intent)
24
+ } else {
25
+ context.startService(intent)
26
+ }
27
+ }
28
+ }
29
+
@@ -1,4 +1,4 @@
1
- package com.flic2
1
+ package nl.xguard.flic2
2
2
 
3
3
  import com.facebook.react.bridge.Arguments
4
4
  import com.facebook.react.bridge.WritableMap
@@ -47,10 +47,20 @@ class Flic2ButtonEventListener(
47
47
  val event = if (isDown) "buttonDown" else "buttonUp"
48
48
  emitEvent(createButtonEvent(button, event).apply {
49
49
  putBoolean("queued", wasQueued)
50
- putDouble("age", System.currentTimeMillis() - timestamp.toDouble())
50
+ // Match old Android implementation and iOS:
51
+ // - Age is in seconds
52
+ // - Only meaningful for queued events; 0 for real-time events
53
+ val ageSeconds = if (wasQueued) {
54
+ (button.readyTimestamp - timestamp) / 1000.0
55
+ } else {
56
+ 0.0
57
+ }
58
+ putDouble("age", ageSeconds)
51
59
  })
52
60
  }
53
61
 
62
+ // Android library calls ALL applicable callback methods, causing duplicate events.
63
+ // Only onButtonSingleOrDoubleClickOrHold should emit events to match iOS behavior.
54
64
  override fun onButtonClickOrHold(
55
65
  button: Flic2Button,
56
66
  wasQueued: Boolean,
@@ -59,13 +69,11 @@ class Flic2ButtonEventListener(
59
69
  isClick: Boolean,
60
70
  isHold: Boolean
61
71
  ) {
62
- val event = if (isClick) "click" else "hold"
63
- emitEvent(createButtonEvent(button, event).apply {
64
- putBoolean("queued", wasQueued)
65
- putDouble("age", System.currentTimeMillis() - timestamp.toDouble())
66
- })
72
+ // Intentionally empty - events are handled by onButtonSingleOrDoubleClickOrHold
67
73
  }
68
74
 
75
+ // Android library calls ALL applicable callback methods, causing duplicate events.
76
+ // Only onButtonSingleOrDoubleClickOrHold should emit events to match iOS behavior.
69
77
  override fun onButtonSingleOrDoubleClick(
70
78
  button: Flic2Button,
71
79
  wasQueued: Boolean,
@@ -74,11 +82,7 @@ class Flic2ButtonEventListener(
74
82
  isSingleClick: Boolean,
75
83
  isDoubleClick: Boolean
76
84
  ) {
77
- val event = if (isSingleClick) "click" else "doubleClick"
78
- emitEvent(createButtonEvent(button, event).apply {
79
- putBoolean("queued", wasQueued)
80
- putDouble("age", System.currentTimeMillis() - timestamp.toDouble())
81
- })
85
+ // Intentionally empty - events are handled by onButtonSingleOrDoubleClickOrHold
82
86
  }
83
87
 
84
88
  override fun onButtonSingleOrDoubleClickOrHold(
@@ -98,7 +102,15 @@ class Flic2ButtonEventListener(
98
102
  }
99
103
  emitEvent(createButtonEvent(button, event).apply {
100
104
  putBoolean("queued", wasQueued)
101
- putDouble("age", System.currentTimeMillis() - timestamp.toDouble())
105
+ // Match old Android implementation and iOS:
106
+ // - Age is in seconds
107
+ // - Only meaningful for queued events; 0 for real-time events
108
+ val ageSeconds = if (wasQueued) {
109
+ (button.readyTimestamp - timestamp) / 1000.0
110
+ } else {
111
+ 0.0
112
+ }
113
+ putDouble("age", ageSeconds)
102
114
  })
103
115
  }
104
116
 
@@ -1,4 +1,4 @@
1
- package com.flic2
1
+ package nl.xguard.flic2
2
2
 
3
3
  import com.facebook.react.bridge.Arguments
4
4
  import com.facebook.react.bridge.WritableArray
@@ -31,8 +31,7 @@ object Flic2Converter {
31
31
  putInt("firmwareRevision", button.getFirmwareVersion())
32
32
 
33
33
  // Check if ready by comparing connection state
34
- val isReady = connState == Flic2Button.CONNECTION_STATE_CONNECTED_READY
35
- putBoolean("isReady", isReady)
34
+ putBoolean("isReady", connState == Flic2Button.CONNECTION_STATE_CONNECTED_READY)
36
35
 
37
36
  // Get battery level from BatteryLevel object
38
37
  val batteryLevel = button.getLastKnownBatteryLevel()
@@ -1,4 +1,4 @@
1
- package com.flic2
1
+ package nl.xguard.flic2
2
2
 
3
3
  import android.content.ComponentName
4
4
  import android.content.Context
@@ -48,8 +48,7 @@ class Flic2Module(reactContext: ReactApplicationContext) :
48
48
  private val serviceConnection = object : ServiceConnection {
49
49
  override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
50
50
  Log.d(TAG, "Service connected")
51
- val binder = service as Flic2Service.Flic2ServiceBinder
52
- flic2Service = binder.getService()
51
+ flic2Service = (service as Flic2Service.Flic2ServiceBinder).getService()
53
52
  serviceBound = true
54
53
 
55
54
  // Set up listeners for existing buttons
@@ -57,14 +56,13 @@ class Flic2Module(reactContext: ReactApplicationContext) :
57
56
  manager.buttons.forEach { button ->
58
57
  setupButtonListener(button)
59
58
  }
59
+ // Update foreground service state based on button count
60
+ updateForegroundServiceState(manager.buttons.size)
60
61
  }
61
62
 
62
63
  // Resolve the initialize promise if pending
63
64
  initializePromise?.let { promise ->
64
- promise.resolve(Arguments.createMap().apply {
65
- putBoolean("success", true)
66
- putString("message", "Manager initialized successfully")
67
- })
65
+ promise.resolve(null)
68
66
  initializePromise = null
69
67
  }
70
68
  }
@@ -88,6 +86,31 @@ class Flic2Module(reactContext: ReactApplicationContext) :
88
86
 
89
87
  override fun invalidate() {
90
88
  super.invalidate()
89
+
90
+ // Remove all button listeners before cleanup to prevent callbacks after teardown
91
+ try {
92
+ val manager = flic2Service?.getManager()
93
+ if (manager != null) {
94
+ buttonListeners.forEach { (uuid, listener) ->
95
+ try {
96
+ // Find the button and remove the listener
97
+ val button = manager.buttons.find { it.uuid == uuid }
98
+ if (button != null) {
99
+ button.removeListener(listener)
100
+ Log.d(TAG, "Removed listener for button during invalidate: $uuid")
101
+ }
102
+ } catch (e: Exception) {
103
+ Log.w(TAG, "Failed to remove listener for button during invalidate: $uuid", e)
104
+ }
105
+ }
106
+ }
107
+ } catch (e: Exception) {
108
+ Log.w(TAG, "Error during listener cleanup in invalidate", e)
109
+ }
110
+
111
+ // Clear listeners map
112
+ buttonListeners.clear()
113
+
91
114
  moduleScope.cancel()
92
115
  if (serviceBound) {
93
116
  reactApplicationContext.unbindService(serviceConnection)
@@ -95,20 +118,6 @@ class Flic2Module(reactContext: ReactApplicationContext) :
95
118
  }
96
119
  }
97
120
 
98
- // Example method - keep for reference
99
- override fun multiply(a: Double, b: Double): Double {
100
- val result = a * b
101
-
102
- val eventData = Arguments.createMap().apply {
103
- putDouble("a", a)
104
- putDouble("b", b)
105
- putDouble("result", result)
106
- }
107
- emitOnMultiply(eventData)
108
-
109
- return result
110
- }
111
-
112
121
  // MARK: - Manager Methods
113
122
 
114
123
  override fun initialize(background: Boolean, promise: Promise) {
@@ -118,11 +127,10 @@ class Flic2Module(reactContext: ReactApplicationContext) :
118
127
 
119
128
  val intent = Intent(reactApplicationContext, Flic2Service::class.java)
120
129
 
121
- // Start service
122
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
123
- reactApplicationContext.startForegroundService(intent)
124
- } else {
125
- reactApplicationContext.startService(intent)
130
+ // Check if service is already running
131
+ if (!ActivityUtil.isServiceRunning(reactApplicationContext, Flic2Service::class.java)) {
132
+ // Start service
133
+ ActivityUtil.startForegroundService(reactApplicationContext, intent)
126
134
  }
127
135
 
128
136
  // Bind to service - promise will be resolved in onServiceConnected
@@ -172,63 +180,60 @@ class Flic2Module(reactContext: ReactApplicationContext) :
172
180
 
173
181
  Log.d(TAG, "Starting scan")
174
182
 
183
+ // Emit started event (matches iOS)
184
+ emitOnScanStatusChange(Arguments.createMap().apply {
185
+ putString("event", "started")
186
+ putString("eventName", "started")
187
+ })
188
+
175
189
  manager.startScan(object : Flic2ScanCallback {
176
190
  override fun onDiscoveredAlreadyPairedButton(button: Flic2Button) {
177
191
  Log.d(TAG, "Discovered already paired button")
178
- emitOnScanStatusChange(Arguments.createMap().apply {
179
- putInt("event", 0)
180
- putString("eventName", "discovered")
181
- })
182
192
  }
183
193
 
184
194
  override fun onDiscovered(bdAddr: String) {
185
195
  Log.d(TAG, "Discovered button: $bdAddr")
186
- emitOnScanStatusChange(Arguments.createMap().apply {
187
- putInt("event", 0)
188
- putString("eventName", "discovered")
189
- })
190
196
  }
191
197
 
192
198
  override fun onConnected() {
193
199
  Log.d(TAG, "Button connected during scan")
194
- emitOnScanStatusChange(Arguments.createMap().apply {
195
- putInt("event", 1)
196
- putString("eventName", "connected")
197
- })
198
200
  }
199
201
 
200
202
  override fun onComplete(result: Int, subCode: Int, button: Flic2Button?) {
201
203
  Log.d(TAG, "Scan complete: result=$result, button=${button?.uuid}")
202
204
 
203
205
  if (result == Flic2ScanCallback.RESULT_SUCCESS && button != null) {
204
- emitOnScanStatusChange(Arguments.createMap().apply {
205
- putInt("event", 2)
206
- putString("eventName", "verified")
207
- })
208
-
209
206
  // Auto-connect (trigger mode not available in Android v1.1.0+)
210
207
  button.connect()
211
208
 
212
209
  setupButtonListener(button)
213
210
 
214
- // Emit discovered event
211
+ // Update foreground service state after adding button
212
+ flic2Service?.getManager()?.let { manager ->
213
+ updateForegroundServiceState(manager.buttons.size)
214
+ }
215
+
216
+ // Emit discovered event as button event (like iOS)
215
217
  emitOnButtonEvent(Arguments.createMap().apply {
216
218
  putString("uuid", button.uuid)
217
219
  putString("event", "discovered")
218
220
  putMap("button", Flic2Converter.buttonToMap(button))
219
221
  })
220
222
  } else {
221
- val errorCode = Flic2Converter.scanResultToString(result)
222
- Log.e(TAG, "Scan failed with error code: $errorCode")
223
+ Log.e(TAG, "Scan failed with error code: ${Flic2Converter.scanResultToString(result)}")
223
224
  }
225
+
226
+ // Emit scan completion with result code
227
+ emitOnScanStatusChange(Arguments.createMap().apply {
228
+ putString("event", "completion")
229
+ putString("eventName", "completion")
230
+ putInt("result", mapScanResultToCode(result))
231
+ })
224
232
  }
225
233
  })
226
234
 
227
235
  // Return immediately - scan results will come through events
228
- promise.resolve(Arguments.createMap().apply {
229
- putBoolean("success", true)
230
- putString("message", "Scan started")
231
- })
236
+ promise.resolve(null)
232
237
  }
233
238
 
234
239
  override fun stopScan(promise: Promise) {
@@ -242,10 +247,7 @@ class Flic2Module(reactContext: ReactApplicationContext) :
242
247
  scanJob?.cancel()
243
248
  manager.stopScan()
244
249
 
245
- promise.resolve(Arguments.createMap().apply {
246
- putBoolean("success", true)
247
- putString("message", "Scan stopped")
248
- })
250
+ promise.resolve(null)
249
251
  } catch (e: Exception) {
250
252
  Log.e(TAG, "Failed to stop scan", e)
251
253
  promise.reject("STOP_SCAN_ERROR", "Failed to stop scan: ${e.message}", e)
@@ -269,16 +271,27 @@ class Flic2Module(reactContext: ReactApplicationContext) :
269
271
  // Disconnect before forgetting like iOS
270
272
  button.disconnectOrAbortPendingConnection()
271
273
 
272
- // Remove listener
274
+ // Explicitly remove listener from button before forgetting (matches old implementation)
275
+ val listener = buttonListeners[uuid]
276
+ if (listener != null) {
277
+ try {
278
+ button.removeListener(listener)
279
+ Log.d(TAG, "Removed listener for button: $uuid")
280
+ } catch (e: Exception) {
281
+ Log.w(TAG, "Failed to remove listener for button: $uuid", e)
282
+ }
283
+ }
284
+
285
+ // Remove listener from map
273
286
  buttonListeners.remove(uuid)
274
287
 
275
288
  // Forget button
276
289
  manager.forgetButton(button)
277
290
 
278
- promise.resolve(Arguments.createMap().apply {
279
- putBoolean("success", true)
280
- putString("message", "Button forgotten")
281
- })
291
+ // Update foreground service state after removing button
292
+ updateForegroundServiceState(manager.buttons.size)
293
+
294
+ promise.resolve(null)
282
295
  } catch (e: Exception) {
283
296
  Log.e(TAG, "Failed to forget button", e)
284
297
  promise.reject("FORGET_ERROR", "Failed to forget button: ${e.message}", e)
@@ -297,10 +310,7 @@ class Flic2Module(reactContext: ReactApplicationContext) :
297
310
 
298
311
  button.connect()
299
312
 
300
- promise.resolve(Arguments.createMap().apply {
301
- putBoolean("success", true)
302
- putString("message", "Connection initiated")
303
- })
313
+ promise.resolve(Flic2Converter.buttonToMap(button))
304
314
  } catch (e: Exception) {
305
315
  Log.e(TAG, "Failed to connect button", e)
306
316
  promise.reject("CONNECT_ERROR", "Failed to connect: ${e.message}", e)
@@ -317,10 +327,7 @@ class Flic2Module(reactContext: ReactApplicationContext) :
317
327
 
318
328
  button.disconnectOrAbortPendingConnection()
319
329
 
320
- promise.resolve(Arguments.createMap().apply {
321
- putBoolean("success", true)
322
- putString("message", "Disconnection initiated")
323
- })
330
+ promise.resolve(Flic2Converter.buttonToMap(button))
324
331
  } catch (e: Exception) {
325
332
  Log.e(TAG, "Failed to disconnect button", e)
326
333
  promise.reject("DISCONNECT_ERROR", "Failed to disconnect: ${e.message}", e)
@@ -352,10 +359,7 @@ class Flic2Module(reactContext: ReactApplicationContext) :
352
359
  // v1.1.0 uses setName() method instead of property
353
360
  button.setName(nickname)
354
361
 
355
- promise.resolve(Arguments.createMap().apply {
356
- putBoolean("success", true)
357
- putString("message", "Nickname set")
358
- })
362
+ promise.resolve(Flic2Converter.buttonToMap(button))
359
363
  } catch (e: Exception) {
360
364
  Log.e(TAG, "Failed to set nickname", e)
361
365
  promise.reject("SET_NICKNAME_ERROR", "Failed to set nickname: ${e.message}", e)
@@ -378,10 +382,7 @@ class Flic2Module(reactContext: ReactApplicationContext) :
378
382
  button.connect()
379
383
  }
380
384
 
381
- promise.resolve(Arguments.createMap().apply {
382
- putBoolean("success", true)
383
- putString("message", "All buttons connection initiated")
384
- })
385
+ promise.resolve(null)
385
386
  } catch (e: Exception) {
386
387
  Log.e(TAG, "Failed to connect all buttons", e)
387
388
  promise.reject("CONNECT_ALL_ERROR", "Failed to connect all buttons: ${e.message}", e)
@@ -403,10 +404,7 @@ class Flic2Module(reactContext: ReactApplicationContext) :
403
404
  button.disconnectOrAbortPendingConnection()
404
405
  }
405
406
 
406
- promise.resolve(Arguments.createMap().apply {
407
- putBoolean("success", true)
408
- putString("message", "All buttons disconnection initiated")
409
- })
407
+ promise.resolve(null)
410
408
  } catch (e: Exception) {
411
409
  Log.e(TAG, "Failed to disconnect all buttons", e)
412
410
  promise.reject("DISCONNECT_ALL_ERROR", "Failed to disconnect all buttons: ${e.message}", e)
@@ -425,15 +423,31 @@ class Flic2Module(reactContext: ReactApplicationContext) :
425
423
  val buttons = manager.buttons.toList()
426
424
 
427
425
  buttons.forEach { button ->
426
+ // Explicitly remove listener from button before forgetting (matches old implementation)
427
+ val listener = buttonListeners[button.uuid]
428
+ if (listener != null) {
429
+ try {
430
+ button.removeListener(listener)
431
+ Log.d(TAG, "Removed listener for button: ${button.uuid}")
432
+ } catch (e: Exception) {
433
+ Log.w(TAG, "Failed to remove listener for button: ${button.uuid}", e)
434
+ }
435
+ }
436
+
437
+ // Remove listener from map
428
438
  buttonListeners.remove(button.uuid)
439
+
440
+ // Disconnect before forgetting
429
441
  button.disconnectOrAbortPendingConnection()
442
+
443
+ // Forget button
430
444
  manager.forgetButton(button)
431
445
  }
432
446
 
433
- promise.resolve(Arguments.createMap().apply {
434
- putBoolean("success", true)
435
- putString("message", "All buttons forgotten")
436
- })
447
+ // Update foreground service state after removing all buttons
448
+ updateForegroundServiceState(manager.buttons.size)
449
+
450
+ promise.resolve(null)
437
451
  } catch (e: Exception) {
438
452
  Log.e(TAG, "Failed to forget all buttons", e)
439
453
  promise.reject("FORGET_ALL_ERROR", "Failed to forget all buttons: ${e.message}", e)
@@ -448,8 +462,7 @@ class Flic2Module(reactContext: ReactApplicationContext) :
448
462
  return
449
463
  }
450
464
 
451
- val scanning = (scanJob != null && scanJob?.isActive == true)
452
- promise.resolve(scanning)
465
+ promise.resolve(scanJob != null && scanJob?.isActive == true)
453
466
  } catch (e: Exception) {
454
467
  Log.e(TAG, "Failed to check scanning status", e)
455
468
  promise.reject("IS_SCANNING_ERROR", "Failed to check scanning status: ${e.message}", e)
@@ -477,4 +490,32 @@ class Flic2Module(reactContext: ReactApplicationContext) :
477
490
  // Store listener reference
478
491
  buttonListeners[button.uuid] = listener
479
492
  }
493
+
494
+ private fun updateForegroundServiceState(buttonCount: Int) {
495
+ if (buttonCount > 0) {
496
+ // Start foreground service when buttons exist
497
+ flic2Service?.startForegroundService()
498
+ } else {
499
+ // Stop foreground service when no buttons
500
+ flic2Service?.stopForegroundService()
501
+ }
502
+ }
503
+
504
+ private fun mapScanResultToCode(result: Int): Int {
505
+ // Map Android library's 9 result codes (0-8) to TypeScript enum codes (0-21) matching iOS
506
+ // Android library only provides these constants, so we map them to the closest equivalent
507
+ return when (result) {
508
+ Flic2ScanCallback.RESULT_SUCCESS -> 0 // SUCCESS
509
+ Flic2ScanCallback.RESULT_FAILED_ALREADY_RUNNING -> 1 // ALREADY_RUNNING
510
+ Flic2ScanCallback.RESULT_FAILED_BLUETOOTH_OFF -> 2 // BLUETOOTH_NOT_ACTIVATED
511
+ Flic2ScanCallback.RESULT_FAILED_SCAN_ERROR -> 3 // UNKNOWN
512
+ Flic2ScanCallback.RESULT_FAILED_NO_NEW_BUTTONS_FOUND -> 4 // NO_PUBLIC_BUTTON_DISCOVERED
513
+ Flic2ScanCallback.RESULT_FAILED_BUTTON_ALREADY_CONNECTED_TO_OTHER_DEVICE -> 5 // ALREADY_CONNECTED_TO_ANOTHER_DEVICE
514
+ Flic2ScanCallback.RESULT_FAILED_CONNECT_TIMED_OUT -> 6 // CONNECTION_TIMEOUT
515
+ Flic2ScanCallback.RESULT_FAILED_VERIFY_TIMED_OUT -> 7 // INVALID_VERIFIER
516
+ Flic2ScanCallback.RESULT_SYSTEM_PAIRING_DIALOG_NOT_ACCEPTED -> 9 // BLE_PAIRING_FAILED_USER_CANCELED
517
+ else -> 3 // UNKNOWN (for any unexpected codes)
518
+ }
519
+ }
480
520
  }
521
+
@@ -1,4 +1,4 @@
1
- package com.flic2
1
+ package nl.xguard.flic2
2
2
 
3
3
  import com.facebook.react.BaseReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
@@ -31,3 +31,4 @@ class Flic2Package : BaseReactPackage() {
31
31
  }
32
32
  }
33
33
  }
34
+