react-native-flic2 2.0.0-beta.1 → 2.0.0-beta.2

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.
@@ -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 com.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
+
@@ -57,6 +57,8 @@ class Flic2Module(reactContext: ReactApplicationContext) :
57
57
  manager.buttons.forEach { button ->
58
58
  setupButtonListener(button)
59
59
  }
60
+ // Update foreground service state based on button count
61
+ updateForegroundServiceState(manager.buttons.size)
60
62
  }
61
63
 
62
64
  // Resolve the initialize promise if pending
@@ -104,11 +106,11 @@ class Flic2Module(reactContext: ReactApplicationContext) :
104
106
 
105
107
  val intent = Intent(reactApplicationContext, Flic2Service::class.java)
106
108
 
107
- // Start service
108
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
109
- reactApplicationContext.startForegroundService(intent)
110
- } else {
111
- reactApplicationContext.startService(intent)
109
+ // Check if service is already running
110
+ val isRunning = ActivityUtil.isServiceRunning(reactApplicationContext, Flic2Service::class.java)
111
+ if (!isRunning) {
112
+ // Start service
113
+ ActivityUtil.startForegroundService(reactApplicationContext, intent)
112
114
  }
113
115
 
114
116
  // Bind to service - promise will be resolved in onServiceConnected
@@ -188,6 +190,11 @@ class Flic2Module(reactContext: ReactApplicationContext) :
188
190
 
189
191
  setupButtonListener(button)
190
192
 
193
+ // Update foreground service state after adding button
194
+ flic2Service?.getManager()?.let { manager ->
195
+ updateForegroundServiceState(manager.buttons.size)
196
+ }
197
+
191
198
  // Emit discovered event as button event (like iOS)
192
199
  emitOnButtonEvent(Arguments.createMap().apply {
193
200
  putString("uuid", button.uuid)
@@ -259,6 +266,9 @@ class Flic2Module(reactContext: ReactApplicationContext) :
259
266
  // Forget button
260
267
  manager.forgetButton(button)
261
268
 
269
+ // Update foreground service state after removing button
270
+ updateForegroundServiceState(manager.buttons.size)
271
+
262
272
  promise.resolve(Arguments.createMap().apply {
263
273
  putBoolean("success", true)
264
274
  putString("message", "Button forgotten")
@@ -414,6 +424,9 @@ class Flic2Module(reactContext: ReactApplicationContext) :
414
424
  manager.forgetButton(button)
415
425
  }
416
426
 
427
+ // Update foreground service state after removing all buttons
428
+ updateForegroundServiceState(manager.buttons.size)
429
+
417
430
  promise.resolve(Arguments.createMap().apply {
418
431
  putBoolean("success", true)
419
432
  putString("message", "All buttons forgotten")
@@ -462,6 +475,16 @@ class Flic2Module(reactContext: ReactApplicationContext) :
462
475
  buttonListeners[button.uuid] = listener
463
476
  }
464
477
 
478
+ private fun updateForegroundServiceState(buttonCount: Int) {
479
+ if (buttonCount > 0) {
480
+ // Start foreground service when buttons exist
481
+ flic2Service?.startForegroundService()
482
+ } else {
483
+ // Stop foreground service when no buttons
484
+ flic2Service?.stopForegroundService()
485
+ }
486
+ }
487
+
465
488
  private fun mapScanResultToCode(result: Int): Int {
466
489
  // Map Android library's 9 result codes (0-8) to TypeScript enum codes (0-21) matching iOS
467
490
  // Android library only provides these constants, so we map them to the closest equivalent
@@ -5,7 +5,10 @@ import android.app.NotificationChannel
5
5
  import android.app.NotificationManager
6
6
  import android.app.PendingIntent
7
7
  import android.app.Service
8
+ import android.content.BroadcastReceiver
9
+ import android.content.Context
8
10
  import android.content.Intent
11
+ import android.content.pm.PackageManager
9
12
  import android.os.Binder
10
13
  import android.os.Build
11
14
  import android.os.Handler
@@ -19,12 +22,22 @@ class Flic2Service : Service() {
19
22
 
20
23
  private val binder = Flic2ServiceBinder()
21
24
  private var manager: Flic2Manager? = null
25
+ private var isServiceStarted = false
26
+ private var notification: Notification? = null
22
27
 
23
28
  companion object {
24
29
  private const val TAG = "Flic2Service"
25
- private const val NOTIFICATION_ID = 1
26
- private const val CHANNEL_ID = "Flic2ServiceChannel"
27
- private const val CHANNEL_NAME = "Flic2 Service"
30
+ private const val DEFAULT_NOTIFICATION_ID = 123321
31
+ private const val DEFAULT_CHANNEL_ID = "Notification_Channel_Flic2Service"
32
+
33
+ // Metadata keys for notification configuration
34
+ private const val KEY_CHANNEL_NAME = "nl.xguard.flic2.notification_channel_name"
35
+ private const val KEY_CHANNEL_DESCRIPTION = "nl.xguard.flic2.notification_channel_description"
36
+ private const val NOTIFICATION_TITLE_KEY = "nl.xguard.flic2.notification_title"
37
+ private const val NOTIFICATION_TEXT_KEY = "nl.xguard.flic2.notification_text"
38
+ private const val NOTIFICATION_ICON_KEY = "nl.xguard.flic2.notification_icon"
39
+ private const val NOTIFICATION_ID_KEY = "nl.xguard.flic2.notification_id"
40
+ private const val CHANNEL_ID_KEY = "nl.xguard.flic2.notification_channel_id"
28
41
  }
29
42
 
30
43
  inner class Flic2ServiceBinder : Binder() {
@@ -47,31 +60,25 @@ class Flic2Service : Service() {
47
60
  } catch (e: Exception) {
48
61
  Log.e(TAG, "Failed to initialize Flic2Manager", e)
49
62
  }
63
+
64
+ // Create notification channel and notification in onCreate
65
+ createNotificationChannel()
66
+ notification = createNotification()
50
67
  }
51
68
 
52
69
  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
53
70
  Log.d(TAG, "Service onStartCommand")
54
71
 
55
- // Create notification channel for Android O and above
56
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
57
- val channel = NotificationChannel(
58
- CHANNEL_ID,
59
- CHANNEL_NAME,
60
- NotificationManager.IMPORTANCE_LOW
61
- ).apply {
62
- description = "Keeps Flic2 buttons connected in the background"
63
- setShowBadge(false)
72
+ if (intent != null) {
73
+ if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
74
+ Log.d(TAG, "onStartCommand: ACTION_BOOT_COMPLETED")
64
75
  }
65
-
66
- val notificationManager = getSystemService(NotificationManager::class.java)
67
- notificationManager.createNotificationChannel(channel)
68
76
  }
69
77
 
70
- // Create notification
71
- val notification = createNotification()
72
-
73
- // Start as foreground service
74
- startForeground(NOTIFICATION_ID, notification)
78
+ // Start foreground service if notification is ready
79
+ if (notification != null) {
80
+ startForegroundService()
81
+ }
75
82
 
76
83
  return START_STICKY
77
84
  }
@@ -90,6 +97,45 @@ class Flic2Service : Service() {
90
97
 
91
98
  fun isManagerInitialized(): Boolean = manager != null
92
99
 
100
+ fun startForegroundService() {
101
+ if (!isServiceStarted && notification != null) {
102
+ isServiceStarted = true
103
+ try {
104
+ val notificationId = getNotificationId()
105
+ startForeground(notificationId, notification)
106
+ } catch (e: Exception) {
107
+ Log.w(TAG, "startForegroundService() exception", e)
108
+ }
109
+ }
110
+ }
111
+
112
+ fun stopForegroundService() {
113
+ if (isServiceStarted) {
114
+ isServiceStarted = false
115
+ stopForeground(true)
116
+ }
117
+ }
118
+
119
+ private fun createNotificationChannel() {
120
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
121
+ val channelId = getChannelId()
122
+ val channelName = getChannelName()
123
+ val channelDescription = getChannelDescription()
124
+
125
+ val channel = NotificationChannel(
126
+ channelId,
127
+ channelName,
128
+ NotificationManager.IMPORTANCE_LOW
129
+ ).apply {
130
+ description = channelDescription
131
+ setShowBadge(false)
132
+ }
133
+
134
+ val notificationManager = getSystemService(NotificationManager::class.java)
135
+ notificationManager.createNotificationChannel(channel)
136
+ }
137
+ }
138
+
93
139
  private fun createNotification(): Notification {
94
140
  val notificationIntent = Intent(this, Flic2Service::class.java)
95
141
  val pendingIntent = PendingIntent.getActivity(
@@ -99,14 +145,141 @@ class Flic2Service : Service() {
99
145
  PendingIntent.FLAG_IMMUTABLE
100
146
  )
101
147
 
102
- return NotificationCompat.Builder(this, CHANNEL_ID)
103
- .setContentTitle("Flic2 Service")
104
- .setContentText("Flic2 buttons are connected")
105
- .setSmallIcon(android.R.drawable.ic_dialog_info)
148
+ val channelId = getChannelId()
149
+ val title = getNotificationTitle()
150
+ val text = getNotificationText()
151
+ val icon = getNotificationIcon()
152
+
153
+ return NotificationCompat.Builder(this, channelId)
154
+ .setContentTitle(title)
155
+ .setContentText(text)
156
+ .setSmallIcon(icon)
106
157
  .setContentIntent(pendingIntent)
107
158
  .setPriority(NotificationCompat.PRIORITY_LOW)
108
159
  .setOngoing(true)
109
160
  .build()
110
161
  }
162
+
163
+ private fun getNotificationId(): Int {
164
+ return try {
165
+ val metadata = applicationContext.packageManager
166
+ .getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
167
+ .metaData
168
+ metadata?.getInt(NOTIFICATION_ID_KEY, DEFAULT_NOTIFICATION_ID) ?: DEFAULT_NOTIFICATION_ID
169
+ } catch (e: PackageManager.NameNotFoundException) {
170
+ Log.w(TAG, "getNotificationId() NameNotFoundException", e)
171
+ DEFAULT_NOTIFICATION_ID
172
+ } catch (e: Exception) {
173
+ Log.w(TAG, "getNotificationId() exception", e)
174
+ DEFAULT_NOTIFICATION_ID
175
+ }
176
+ }
177
+
178
+ private fun getChannelId(): String {
179
+ return try {
180
+ val metadata = applicationContext.packageManager
181
+ .getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
182
+ .metaData
183
+ metadata?.getString(CHANNEL_ID_KEY) ?: DEFAULT_CHANNEL_ID
184
+ } catch (e: PackageManager.NameNotFoundException) {
185
+ Log.w(TAG, "getChannelId() NameNotFoundException", e)
186
+ DEFAULT_CHANNEL_ID
187
+ } catch (e: Exception) {
188
+ Log.w(TAG, "getChannelId() exception", e)
189
+ DEFAULT_CHANNEL_ID
190
+ }
191
+ }
192
+
193
+ private fun getChannelName(): String {
194
+ return try {
195
+ val metadata = applicationContext.packageManager
196
+ .getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
197
+ .metaData
198
+ metadata?.getString(KEY_CHANNEL_NAME) ?: "Flic2Channel"
199
+ } catch (e: PackageManager.NameNotFoundException) {
200
+ Log.w(TAG, "getChannelName() NameNotFoundException", e)
201
+ "Flic2Channel"
202
+ } catch (e: Exception) {
203
+ Log.w(TAG, "getChannelName() exception", e)
204
+ "Flic2Channel"
205
+ }
206
+ }
207
+
208
+ private fun getChannelDescription(): String {
209
+ return try {
210
+ val metadata = applicationContext.packageManager
211
+ .getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
212
+ .metaData
213
+ metadata?.getString(KEY_CHANNEL_DESCRIPTION) ?: "Flic2Channel"
214
+ } catch (e: PackageManager.NameNotFoundException) {
215
+ Log.w(TAG, "getChannelDescription() NameNotFoundException", e)
216
+ "Flic2Channel"
217
+ } catch (e: Exception) {
218
+ Log.w(TAG, "getChannelDescription() exception", e)
219
+ "Flic2Channel"
220
+ }
221
+ }
222
+
223
+ private fun getNotificationTitle(): String {
224
+ return try {
225
+ val metadata = applicationContext.packageManager
226
+ .getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
227
+ .metaData
228
+ metadata?.getString(NOTIFICATION_TITLE_KEY) ?: "Flic 2"
229
+ } catch (e: PackageManager.NameNotFoundException) {
230
+ Log.w(TAG, "getNotificationTitle() NameNotFoundException", e)
231
+ "Flic 2"
232
+ } catch (e: Exception) {
233
+ Log.w(TAG, "getNotificationTitle() exception", e)
234
+ "Flic 2"
235
+ }
236
+ }
237
+
238
+ private fun getNotificationText(): String {
239
+ return try {
240
+ val metadata = applicationContext.packageManager
241
+ .getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
242
+ .metaData
243
+ metadata?.getString(NOTIFICATION_TEXT_KEY) ?: "Flic 2 service is running"
244
+ } catch (e: PackageManager.NameNotFoundException) {
245
+ Log.w(TAG, "getNotificationText() NameNotFoundException", e)
246
+ "Flic 2 service is running"
247
+ } catch (e: Exception) {
248
+ Log.w(TAG, "getNotificationText() exception", e)
249
+ "Flic 2 service is running"
250
+ }
251
+ }
252
+
253
+ private fun getNotificationIcon(): Int {
254
+ return try {
255
+ val metadata = applicationContext.packageManager
256
+ .getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
257
+ .metaData
258
+ val icon = metadata?.getInt(NOTIFICATION_ICON_KEY, 0) ?: 0
259
+ if (icon != 0) icon else android.R.drawable.ic_dialog_info
260
+ } catch (e: PackageManager.NameNotFoundException) {
261
+ Log.w(TAG, "getNotificationIcon() NameNotFoundException", e)
262
+ android.R.drawable.ic_dialog_info
263
+ } catch (e: Exception) {
264
+ Log.w(TAG, "getNotificationIcon() exception", e)
265
+ android.R.drawable.ic_dialog_info
266
+ }
267
+ }
268
+
269
+ // BootUpReceiver for handling device boot
270
+ class BootUpReceiver : BroadcastReceiver() {
271
+ override fun onReceive(context: Context, intent: Intent) {
272
+ Log.d(TAG, "BootUpReceiver()")
273
+ // The Application class's onCreate has already been called at this point, which is what we want
274
+ }
275
+ }
276
+
277
+ // UpdateReceiver for handling app updates
278
+ class UpdateReceiver : BroadcastReceiver() {
279
+ override fun onReceive(context: Context, intent: Intent) {
280
+ Log.d(TAG, "UpdateReceiver()")
281
+ // The Application class's onCreate has already been called at this point, which is what we want
282
+ }
283
+ }
111
284
  }
112
285
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-flic2",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.2",
4
4
  "description": "React Native Flic plugin made with the Flic2 SDK (unofficial)",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",