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
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
26
|
-
private const val
|
|
27
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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