Skip to content

Commit 0c22b7e

Browse files
authored
Merge pull request #526 from synonymdev/fix/wake-node-polish
fix: wake node polish
2 parents 4883620 + 1f35397 commit 0c22b7e

File tree

7 files changed

+74
-53
lines changed

7 files changed

+74
-53
lines changed

app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ class LightningNodeService : Service() {
131131
when (intent?.action) {
132132
ACTION_STOP_SERVICE_AND_APP -> {
133133
Logger.debug("ACTION_STOP_SERVICE_AND_APP detected", context = TAG)
134-
// Close all activities
135-
App.currentActivity?.value?.finishAndRemoveTask()
134+
// Close activities gracefully without force-stopping the app
135+
App.currentActivity?.value?.finishAffinity()
136136
// Stop the service
137137
stopSelf()
138138
return START_NOT_STICKY

app/src/main/java/to/bitkit/fcm/FcmService.kt

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package to.bitkit.fcm
33
import android.os.Bundle
44
import androidx.core.os.toPersistableBundle
55
import androidx.work.OneTimeWorkRequestBuilder
6+
import androidx.work.OutOfQuotaPolicy
67
import androidx.work.WorkManager
78
import androidx.work.workDataOf
89
import com.google.firebase.messaging.FirebaseMessagingService
@@ -46,24 +47,24 @@ class FcmService : FirebaseMessagingService() {
4647
* Act on received messages. [Debug](https://goo.gl/39bRNJ)
4748
*/
4849
override fun onMessageReceived(message: RemoteMessage) {
49-
Logger.debug("New FCM at: ${Date(message.sentTime)}")
50+
Logger.debug("New FCM at: ${Date(message.sentTime)}", context = TAG)
5051

5152
message.notification?.run {
52-
Logger.debug("FCM title: $title")
53-
Logger.debug("FCM body: $body")
53+
Logger.debug("FCM title: $title", context = TAG)
54+
Logger.debug("FCM body: $body", context = TAG)
5455
sendNotification(title, body, Bundle(message.data.toPersistableBundle()))
5556
}
5657

5758
if (message.data.isNotEmpty()) {
58-
Logger.debug("FCM data: ${message.data}")
59+
Logger.debug("FCM data: ${message.data}", context = TAG)
5960

6061
val shouldSchedule = runCatching {
6162
val isEncryptedNotification = message.data.tryAs<EncryptedNotification> {
6263
decryptPayload(it)
6364
}
6465
isEncryptedNotification
6566
}.getOrElse {
66-
Logger.error("Failed to read encrypted notification payload", it)
67+
Logger.error("Failed to read encrypted notification payload", it, context = TAG)
6768
// Let the node to spin up and handle incoming events
6869
true
6970
}
@@ -83,28 +84,29 @@ class FcmService : FirebaseMessagingService() {
8384
"payload" to notificationPayload?.toString(),
8485
)
8586
)
87+
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
8688
.build()
8789
WorkManager.getInstance(this)
8890
.beginWith(work)
8991
.enqueue()
9092
}
9193

9294
private fun handleNow(data: Map<String, String>) {
93-
Logger.warn("FCM handler not implemented for: $data")
95+
Logger.warn("FCM handler not implemented for: $data", context = TAG)
9496
}
9597

9698
private fun decryptPayload(response: EncryptedNotification) {
9799
val ciphertext = runCatching { response.cipher.fromBase64() }.getOrElse {
98-
Logger.error("Failed to decode cipher", it)
100+
Logger.error("Failed to decode cipher", it, context = TAG)
99101
return
100102
}
101103
val privateKey = runCatching { keychain.load(Keychain.Key.PUSH_NOTIFICATION_PRIVATE_KEY.name)!! }.getOrElse {
102-
Logger.error("Missing PUSH_NOTIFICATION_PRIVATE_KEY", it)
104+
Logger.error("Missing PUSH_NOTIFICATION_PRIVATE_KEY", it, context = TAG)
103105
return
104106
}
105107
val password =
106108
runCatching { crypto.generateSharedSecret(privateKey, response.publicKey, DERIVATION_NAME) }.getOrElse {
107-
Logger.error("Failed to generate shared secret", it)
109+
Logger.error("Failed to generate shared secret", it, context = TAG)
108110
return
109111
}
110112

@@ -114,20 +116,20 @@ class FcmService : FirebaseMessagingService() {
114116
)
115117

116118
val decoded = decrypted.decodeToString()
117-
Logger.debug("Decrypted payload: $decoded")
119+
Logger.debug("Decrypted payload: $decoded", context = TAG)
118120

119121
val (payload, type) = runCatching { json.decodeFromString<DecryptedNotification>(decoded) }.getOrElse {
120-
Logger.error("Failed to decode decrypted data", it)
122+
Logger.error("Failed to decode decrypted data", it, context = TAG)
121123
return
122124
}
123125

124126
if (payload == null) {
125-
Logger.error("Missing payload")
127+
Logger.error("Missing payload", context = TAG)
126128
return
127129
}
128130

129131
if (type == null) {
130-
Logger.error("Missing type")
132+
Logger.error("Missing type", context = TAG)
131133
return
132134
}
133135

app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import androidx.work.workDataOf
88
import dagger.assisted.Assisted
99
import dagger.assisted.AssistedInject
1010
import kotlinx.coroutines.CompletableDeferred
11+
import kotlinx.coroutines.delay
1112
import kotlinx.coroutines.flow.first
1213
import kotlinx.coroutines.withTimeout
1314
import kotlinx.serialization.json.JsonObject
1415
import kotlinx.serialization.json.JsonPrimitive
1516
import kotlinx.serialization.json.contentOrNull
1617
import kotlinx.serialization.json.jsonObject
1718
import org.lightningdevkit.ldknode.Event
19+
import to.bitkit.App
1820
import to.bitkit.R
1921
import to.bitkit.data.CacheStore
2022
import to.bitkit.data.SettingsStore
@@ -34,26 +36,23 @@ import to.bitkit.models.NotificationDetails
3436
import to.bitkit.repositories.ActivityRepo
3537
import to.bitkit.repositories.BlocktankRepo
3638
import to.bitkit.repositories.LightningRepo
37-
import to.bitkit.services.CoreService
3839
import to.bitkit.ui.pushNotification
3940
import to.bitkit.utils.Logger
4041
import to.bitkit.utils.measured
4142
import kotlin.time.Duration.Companion.minutes
43+
import kotlin.time.Duration.Companion.seconds
4244

4345
@Suppress("LongParameterList")
4446
@HiltWorker
4547
class WakeNodeWorker @AssistedInject constructor(
4648
@Assisted private val appContext: Context,
4749
@Assisted private val workerParams: WorkerParameters,
48-
private val coreService: CoreService,
4950
private val lightningRepo: LightningRepo,
5051
private val blocktankRepo: BlocktankRepo,
5152
private val activityRepo: ActivityRepo,
5253
private val settingsStore: SettingsStore,
5354
private val cacheStore: CacheStore,
5455
) : CoroutineWorker(appContext, workerParams) {
55-
private val self = this
56-
5756
private var bestAttemptContent: NotificationDetails? = null
5857

5958
private var notificationType: BlocktankNotificationType? = null
@@ -63,15 +62,19 @@ class WakeNodeWorker @AssistedInject constructor(
6362
private val deliverSignal = CompletableDeferred<Unit>()
6463

6564
override suspend fun doWork(): Result {
66-
Logger.debug("Node wakeup from notification…")
65+
Logger.debug("Node wakeup from notification…", context = TAG)
6766

6867
notificationType = workerParams.inputData.getString("type")?.let { BlocktankNotificationType.valueOf(it) }
6968
notificationPayload = workerParams.inputData.getString("payload")?.let {
7069
runCatching { json.parseToJsonElement(it).jsonObject }.getOrNull()
7170
}
7271

73-
Logger.debug("${this::class.simpleName} notification type: $notificationType")
74-
Logger.debug("${this::class.simpleName} notification payload: $notificationPayload")
72+
Logger.debug("notification type: $notificationType", context = TAG)
73+
Logger.debug("notification payload: $notificationPayload", context = TAG)
74+
75+
if (notificationType == null) {
76+
Logger.warn("Notification type is null, proceeding with node wake", context = TAG)
77+
}
7578

7679
try {
7780
measured(TAG) {
@@ -80,25 +83,22 @@ class WakeNodeWorker @AssistedInject constructor(
8083
timeout = timeout,
8184
eventHandler = { event -> handleLdkEvent(event) }
8285
)
83-
lightningRepo.connectToTrustedPeers()
8486

8587
// Once node is started, handle the manual channel opening if needed
86-
if (self.notificationType == orderPaymentConfirmed) {
88+
if (notificationType == orderPaymentConfirmed) {
8789
val orderId = (notificationPayload?.get("orderId") as? JsonPrimitive)?.contentOrNull
8890

8991
if (orderId == null) {
90-
Logger.error("Missing orderId")
92+
Logger.error("Missing orderId", context = TAG)
9193
} else {
92-
try {
93-
Logger.info("Open channel request for order $orderId")
94-
coreService.blocktank.open(orderId = orderId)
95-
} catch (e: Exception) {
96-
Logger.error("failed to open channel", e)
97-
self.bestAttemptContent = NotificationDetails(
94+
Logger.info("Open channel request for order $orderId", context = TAG)
95+
blocktankRepo.openChannel(orderId).onFailure { e ->
96+
Logger.error("Failed to open channel", e, context = TAG)
97+
bestAttemptContent = NotificationDetails(
9898
title = appContext.getString(R.string.notification_channel_open_failed_title),
9999
body = e.message ?: appContext.getString(R.string.notification_unknown_error),
100100
)
101-
self.deliver()
101+
deliver()
102102
}
103103
}
104104
}
@@ -108,12 +108,12 @@ class WakeNodeWorker @AssistedInject constructor(
108108
} catch (e: Exception) {
109109
val reason = e.message ?: appContext.getString(R.string.notification_unknown_error)
110110

111-
self.bestAttemptContent = NotificationDetails(
111+
bestAttemptContent = NotificationDetails(
112112
title = appContext.getString(R.string.notification_lightning_error_title),
113113
body = reason,
114114
)
115-
Logger.error("Lightning error", e)
116-
self.deliver()
115+
Logger.error("Lightning error", e, context = TAG)
116+
deliver()
117117

118118
return Result.failure(workDataOf("Reason" to reason))
119119
}
@@ -130,7 +130,7 @@ class WakeNodeWorker @AssistedInject constructor(
130130
is Event.PaymentReceived -> onPaymentReceived(event, showDetails, hiddenBody)
131131

132132
is Event.ChannelPending -> {
133-
self.bestAttemptContent = NotificationDetails(
133+
bestAttemptContent = NotificationDetails(
134134
title = appContext.getString(R.string.notification_channel_opened_title),
135135
body = appContext.getString(R.string.notification_channel_pending_body),
136136
)
@@ -141,13 +141,13 @@ class WakeNodeWorker @AssistedInject constructor(
141141
is Event.ChannelClosed -> onChannelClosed(event)
142142

143143
is Event.PaymentFailed -> {
144-
self.bestAttemptContent = NotificationDetails(
144+
bestAttemptContent = NotificationDetails(
145145
title = appContext.getString(R.string.notification_payment_failed_title),
146146
body = "${event.reason}",
147147
)
148148

149-
if (self.notificationType == wakeToTimeout) {
150-
self.deliver()
149+
if (notificationType == wakeToTimeout) {
150+
deliver()
151151
}
152152
}
153153

@@ -156,7 +156,7 @@ class WakeNodeWorker @AssistedInject constructor(
156156
}
157157

158158
private suspend fun onChannelClosed(event: Event.ChannelClosed) {
159-
self.bestAttemptContent = when (self.notificationType) {
159+
bestAttemptContent = when (notificationType) {
160160
mutualClose -> NotificationDetails(
161161
title = appContext.getString(R.string.notification_channel_closed_title),
162162
body = appContext.getString(R.string.notification_channel_closed_mutual_body),
@@ -173,7 +173,7 @@ class WakeNodeWorker @AssistedInject constructor(
173173
)
174174
}
175175

176-
self.deliver()
176+
deliver()
177177
}
178178

179179
private suspend fun onPaymentReceived(
@@ -196,8 +196,8 @@ class WakeNodeWorker @AssistedInject constructor(
196196
title = appContext.getString(R.string.notification_received_title),
197197
body = content,
198198
)
199-
if (self.notificationType == incomingHtlc) {
200-
self.deliver()
199+
if (notificationType == incomingHtlc) {
200+
deliver()
201201
}
202202
}
203203

@@ -207,16 +207,16 @@ class WakeNodeWorker @AssistedInject constructor(
207207
hiddenBody: String,
208208
) {
209209
val viaNewChannel = appContext.getString(R.string.notification_via_new_channel_body)
210-
if (self.notificationType == cjitPaymentArrived) {
211-
self.bestAttemptContent = NotificationDetails(
210+
if (notificationType == cjitPaymentArrived) {
211+
bestAttemptContent = NotificationDetails(
212212
title = appContext.getString(R.string.notification_received_title),
213213
body = viaNewChannel,
214214
)
215215

216216
lightningRepo.getChannels()?.find { it.channelId == event.channelId }?.let { channel ->
217217
val sats = channel.amountOnClose
218218
val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else hiddenBody
219-
self.bestAttemptContent = NotificationDetails(
219+
bestAttemptContent = NotificationDetails(
220220
title = content,
221221
body = viaNewChannel,
222222
)
@@ -233,21 +233,32 @@ class WakeNodeWorker @AssistedInject constructor(
233233
activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel)
234234
}
235235
}
236-
} else if (self.notificationType == orderPaymentConfirmed) {
237-
self.bestAttemptContent = NotificationDetails(
236+
} else if (notificationType == orderPaymentConfirmed) {
237+
bestAttemptContent = NotificationDetails(
238238
title = appContext.getString(R.string.notification_channel_opened_title),
239239
body = appContext.getString(R.string.notification_channel_ready_body),
240240
)
241241
}
242-
self.deliver()
242+
deliver()
243243
}
244244

245245
private suspend fun deliver() {
246-
lightningRepo.stop()
247-
246+
// Send notification first
248247
bestAttemptContent?.run {
249248
appContext.pushNotification(title, body)
250-
Logger.info("Delivered notification")
249+
Logger.info("Delivered notification", context = TAG)
250+
}
251+
252+
// Delay briefly to allow app to come to foreground if user clicked notification
253+
delay(1.seconds)
254+
255+
// Only stop node if app is not in foreground
256+
// LightningNodeService will keep node running in background when notifications are enabled
257+
if (App.currentActivity?.value == null) {
258+
Logger.debug("App in background, stopping node after notification delivery", context = TAG)
259+
lightningRepo.stop()
260+
} else {
261+
Logger.debug("App in foreground, keeping node running", context = TAG)
251262
}
252263

253264
deliverSignal.complete(Unit)

app/src/main/java/to/bitkit/repositories/LightningRepo.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,8 @@ class LightningRepo @Inject constructor(
869869
val token = token ?: firebaseMessaging.token.await()
870870
val cachedToken = keychain.loadString(Keychain.Key.PUSH_NOTIFICATION_TOKEN.name)
871871

872+
require(token.isNotEmpty()) { "FCM token is empty or null" }
873+
872874
if (cachedToken == token) {
873875
Logger.debug("Skipped registering for notifications, current device token already registered")
874876
return@executeWhenNodeRunning Result.success(Unit)

app/src/main/java/to/bitkit/services/LspNotificationsService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class LspNotificationsService @Inject constructor(
5252
isoTimestamp = "$timestamp",
5353
signature = signature,
5454
customUrl = Env.blocktankNotificationApiUrl,
55-
isProduction = null,
55+
isProduction = !Env.isDebug,
5656
)
5757
}
5858

app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package to.bitkit.usecases
22

3+
import com.google.firebase.messaging.FirebaseMessaging
34
import to.bitkit.data.AppDb
45
import to.bitkit.data.CacheStore
56
import to.bitkit.data.SettingsStore
@@ -27,6 +28,7 @@ class WipeWalletUseCase @Inject constructor(
2728
private val blocktankRepo: BlocktankRepo,
2829
private val activityRepo: ActivityRepo,
2930
private val lightningRepo: LightningRepo,
31+
private val firebaseMessaging: FirebaseMessaging,
3032
) {
3133
@Suppress("TooGenericExceptionCaught")
3234
suspend operator fun invoke(
@@ -39,6 +41,7 @@ class WipeWalletUseCase @Inject constructor(
3941
backupRepo.reset()
4042

4143
keychain.wipe()
44+
firebaseMessaging.deleteToken()
4245

4346
coreService.wipeData()
4447
db.clearAllTables()

0 commit comments

Comments
 (0)