From 046305de5baca4d21125c04bce3fa3cd45ac9cab Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 25 Jan 2026 23:32:21 +0100 Subject: [PATCH 1/4] chore: update ldk-node - Update `ldk-node-android` dependency to version `v0.7.0-rc.17` - Update `ChannelDetails` extensions and UI mock data to include the new `claimableOnCloseSats` property required by the SDK update --- app/src/main/java/to/bitkit/ext/ChannelDetails.kt | 1 + .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 2 ++ gradle/libs.versions.toml | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ext/ChannelDetails.kt b/app/src/main/java/to/bitkit/ext/ChannelDetails.kt index 00441336b..5a5a39cf5 100644 --- a/app/src/main/java/to/bitkit/ext/ChannelDetails.kt +++ b/app/src/main/java/to/bitkit/ext/ChannelDetails.kt @@ -78,6 +78,7 @@ fun createChannelDetails(): ChannelDetails { forceCloseSpendDelay = null, inboundHtlcMinimumMsat = 0u, inboundHtlcMaximumMsat = null, + claimableOnCloseSats = 0u, config = ChannelConfig( forwardingFeeProportionalMillionths = 0u, forwardingFeeBaseMsat = 0u, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index d4101feb0..59bbaa6c0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -694,6 +694,7 @@ private fun PreviewAutoMode() { forceCloseSpendDelay = null, inboundHtlcMinimumMsat = 0uL, inboundHtlcMaximumMsat = null, + claimableOnCloseSats = 0uL, config = org.lightningdevkit.ldknode.ChannelConfig( forwardingFeeProportionalMillionths = 0u, forwardingFeeBaseMsat = 0u, @@ -764,6 +765,7 @@ private fun PreviewSpendingMode() { forceCloseSpendDelay = null, inboundHtlcMinimumMsat = 0uL, inboundHtlcMaximumMsat = null, + claimableOnCloseSats = 0uL, config = org.lightningdevkit.ldknode.ChannelConfig( forwardingFeeProportionalMillionths = 0u, forwardingFeeBaseMsat = 0u, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd7612931..acf4aa9ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.7.0-rc.8" } # fork | local: remove `v` +ldk-node-android = { module = "com.github.synonymdev.ldk-node:ldk-node-android", version = "v0.7.0-rc.17" } lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } From 16cf43830e18683a28341d4bba8385b1dafba4d9 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 25 Jan 2026 23:32:32 +0100 Subject: [PATCH 2/4] feat: use battery saving sync intervals --- .../androidServices/LightningNodeService.kt | 27 +++++++++++++++++++ app/src/main/java/to/bitkit/env/Env.kt | 8 +++++- .../to/bitkit/repositories/LightningRepo.kt | 25 +++++++++++++++++ .../to/bitkit/services/LightningService.kt | 20 +++++++++++--- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt index 084093db6..43a10b1e1 100644 --- a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt +++ b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt @@ -9,6 +9,9 @@ import android.os.IBinder import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -54,10 +57,15 @@ class LightningNodeService : Service() { @Inject lateinit var cacheStore: CacheStore + private var lifecycleObserver: AppLifecycleObserver? = null + override fun onCreate() { super.onCreate() startForeground(ID_NOTIFICATION_NODE, createNotification()) setupService() + lifecycleObserver = AppLifecycleObserver().also { + ProcessLifecycleOwner.get().lifecycle.addObserver(it) + } } private fun setupService() { @@ -145,6 +153,7 @@ class LightningNodeService : Service() { override fun onDestroy() { Logger.debug("onDestroy", context = TAG) + lifecycleObserver?.let { ProcessLifecycleOwner.get().lifecycle.removeObserver(it) } serviceScope.launch { lightningRepo.stop() serviceScope.cancel() @@ -164,6 +173,24 @@ class LightningNodeService : Service() { override fun onBind(intent: Intent?): IBinder? = null + private inner class AppLifecycleObserver : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + serviceScope.launch { + lightningRepo.disableBatterySavingMode() + .onSuccess { Logger.debug("Sync intervals exited battery saving mode", context = TAG) } + .onFailure { Logger.warn("Error setting sync intervals out of battery saving", it, context = TAG) } + } + } + + override fun onStop(owner: LifecycleOwner) { + serviceScope.launch { + lightningRepo.enableBatterySavingMode() + .onSuccess { Logger.debug("Sync intervals entered battery saving mode", context = TAG) } + .onFailure { Logger.warn("Error setting sync intervals set to battery saving", it, context = TAG) } + } + } + } + companion object { const val CHANNEL_ID_NODE = "bitkit_notification_channel_node" const val TAG = "LightningNodeService" diff --git a/app/src/main/java/to/bitkit/env/Env.kt b/app/src/main/java/to/bitkit/env/Env.kt index 1bd38d2c5..60479776b 100644 --- a/app/src/main/java/to/bitkit/env/Env.kt +++ b/app/src/main/java/to/bitkit/env/Env.kt @@ -4,6 +4,7 @@ import android.os.Build import org.lightningdevkit.ldknode.LogLevel import org.lightningdevkit.ldknode.Network import org.lightningdevkit.ldknode.PeerDetails +import org.lightningdevkit.ldknode.RuntimeSyncIntervals import to.bitkit.BuildConfig import to.bitkit.ext.ensureDir import to.bitkit.ext.of @@ -20,12 +21,17 @@ internal object Env { val e2eBackend = BuildConfig.E2E_BACKEND.lowercase() val network = Network.valueOf(BuildConfig.NETWORK) val locales = BuildConfig.LOCALES.split(",") - const val walletSyncIntervalSecs = 10_uL val platform = "Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})" const val version = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" val ldkLogLevel = LogLevel.TRACE + val syncIntervals = RuntimeSyncIntervals( + onchainWalletSyncIntervalSecs = 80_uL, // ldk-node default + lightningWalletSyncIntervalSecs = 30_uL, // ldk-node default + feeRateCacheUpdateIntervalSecs = 600_uL, // ldk-node default (10 min) + ) + val trustedLnPeers get() = when (network) { Network.BITCOIN -> listOf(Peers.lnd1, Peers.lnd3, Peers.lnd4) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index aacbd5c29..e6c90a342 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -36,6 +36,7 @@ import org.lightningdevkit.ldknode.ChannelDataMigration import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.ClosureReason import org.lightningdevkit.ldknode.Event +import org.lightningdevkit.ldknode.NodeException import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails import org.lightningdevkit.ldknode.PaymentId @@ -439,6 +440,30 @@ class LightningRepo @Inject constructor( /** Clear pending sync flag. Called when manual pull-to-refresh takes priority. */ fun clearPendingSync() = syncPending.set(false) + suspend fun enableBatterySavingMode(): Result = executeWhenNodeRunning("enableBatterySavingMode") { + runCatching { + lightningService.updateSyncIntervals(lightningService.batterySavingIntervals()) + }.recoverCatching { e -> + if (e is NodeException.BackgroundSyncNotEnabled) { + Logger.warn("Background sync not enabled, skipping sync intervals update", context = TAG) + } else { + throw e + } + } + } + + suspend fun disableBatterySavingMode(): Result = executeWhenNodeRunning("disableBatterySavingMode") { + runCatching { + lightningService.updateSyncIntervals(lightningService.defaultSyncIntervals()) + }.recoverCatching { e -> + if (e is NodeException.BackgroundSyncNotEnabled) { + Logger.warn("Background sync not enabled, skipping sync intervals update", context = TAG) + } else { + throw e + } + } + } + private suspend fun refreshChannelCache() = withContext(bgDispatcher) { lightningService.channels?.forEach { channelCache[it.channelId] = it diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 84fe835cb..3a337b662 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -34,6 +34,7 @@ import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails import org.lightningdevkit.ldknode.PaymentId import org.lightningdevkit.ldknode.PeerDetails +import org.lightningdevkit.ldknode.RuntimeSyncIntervals import org.lightningdevkit.ldknode.SpendableUtxo import org.lightningdevkit.ldknode.Txid import org.lightningdevkit.ldknode.defaultConfig @@ -193,9 +194,9 @@ class LightningService @Inject constructor( serverUrl = serverUrl, config = ElectrumSyncConfig( BackgroundSyncConfig( - onchainWalletSyncIntervalSecs = Env.walletSyncIntervalSecs, - lightningWalletSyncIntervalSecs = Env.walletSyncIntervalSecs, - feeRateCacheUpdateIntervalSecs = Env.walletSyncIntervalSecs, + onchainWalletSyncIntervalSecs = Env.syncIntervals.onchainWalletSyncIntervalSecs, + lightningWalletSyncIntervalSecs = Env.syncIntervals.lightningWalletSyncIntervalSecs, + feeRateCacheUpdateIntervalSecs = Env.syncIntervals.feeRateCacheUpdateIntervalSecs, ), ), ) @@ -275,6 +276,19 @@ class LightningService @Inject constructor( Logger.debug("LDK synced", context = TAG) } + suspend fun updateSyncIntervals(intervals: RuntimeSyncIntervals) = withContext(bgDispatcher) { + val node = this@LightningService.node ?: throw ServiceError.NodeNotSetup() + ServiceQueue.LDK.background { + node.updateSyncIntervals(intervals) + } + Logger.info("Sync intervals updated", context = TAG) + } + + fun defaultSyncIntervals(): RuntimeSyncIntervals = Env.syncIntervals + + fun batterySavingIntervals(): RuntimeSyncIntervals = + org.lightningdevkit.ldknode.batterySavingSyncIntervals() + suspend fun sign(message: String): String { val node = this.node ?: throw ServiceError.NodeNotSetup() val msg = runCatching { message.uByteList }.getOrNull() ?: throw ServiceError.InvalidNodeSigningMessage() From 31d146ecb944fc23bb0713e37c0ffef5869ef053 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 26 Jan 2026 14:18:00 +0100 Subject: [PATCH 3/4] refactor: restore 10s sync intervals for regtest --- .../androidServices/LightningNodeService.kt | 16 ++++------------ app/src/main/java/to/bitkit/env/Env.kt | 19 ++++++++++++++----- .../to/bitkit/ext/RuntimeSyncIntervals.kt | 10 ++++++++++ .../to/bitkit/services/LightningService.kt | 8 ++------ 4 files changed, 30 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ext/RuntimeSyncIntervals.kt diff --git a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt index 43a10b1e1..b682c2748 100644 --- a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt +++ b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt @@ -63,9 +63,7 @@ class LightningNodeService : Service() { super.onCreate() startForeground(ID_NOTIFICATION_NODE, createNotification()) setupService() - lifecycleObserver = AppLifecycleObserver().also { - ProcessLifecycleOwner.get().lifecycle.addObserver(it) - } + lifecycleObserver = AppLifecycleObserver().also { ProcessLifecycleOwner.get().lifecycle.addObserver(it) } } private fun setupService() { @@ -116,9 +114,7 @@ class LightningNodeService : Service() { val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) // Create stop action that will close both service and app - val stopIntent = Intent(this, LightningNodeService::class.java).apply { - action = ACTION_STOP_SERVICE_AND_APP - } + val stopIntent = Intent(this, LightningNodeService::class.java).apply { action = ACTION_STOP_SERVICE_AND_APP } val stopPendingIntent = PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE) return NotificationCompat.Builder(this, CHANNEL_ID_NODE) @@ -128,11 +124,7 @@ class LightningNodeService : Service() { .setColor(ContextCompat.getColor(this, R.color.brand)) .setContentIntent(pendingIntent) .setOngoing(true) - .addAction( - R.drawable.ic_x, - getString(R.string.notification__service__stop), - stopPendingIntent - ) + .addAction(R.drawable.ic_x, getString(R.string.notification__service__stop), stopPendingIntent) .build() } @@ -192,8 +184,8 @@ class LightningNodeService : Service() { } companion object { - const val CHANNEL_ID_NODE = "bitkit_notification_channel_node" const val TAG = "LightningNodeService" + const val CHANNEL_ID_NODE = "bitkit_notification_channel_node" const val ACTION_STOP_SERVICE_AND_APP = "to.bitkit.androidServices.action.STOP_SERVICE_AND_APP" } } diff --git a/app/src/main/java/to/bitkit/env/Env.kt b/app/src/main/java/to/bitkit/env/Env.kt index 60479776b..e225f84de 100644 --- a/app/src/main/java/to/bitkit/env/Env.kt +++ b/app/src/main/java/to/bitkit/env/Env.kt @@ -26,11 +26,7 @@ internal object Env { val ldkLogLevel = LogLevel.TRACE - val syncIntervals = RuntimeSyncIntervals( - onchainWalletSyncIntervalSecs = 80_uL, // ldk-node default - lightningWalletSyncIntervalSecs = 30_uL, // ldk-node default - feeRateCacheUpdateIntervalSecs = 600_uL, // ldk-node default (10 min) - ) + val syncIntervals = if (network == Network.REGTEST) SyncIntervals.REGTEST else SyncIntervals.DEFAULT val trustedLnPeers get() = when (network) { @@ -226,3 +222,16 @@ private object ElectrumServers { const val TESTNET = "ssl://electrum.blockstream.info:60002" } + +private object SyncIntervals { + val DEFAULT = RuntimeSyncIntervals( + onchainWalletSyncIntervalSecs = 80_uL, + lightningWalletSyncIntervalSecs = 30_uL, + feeRateCacheUpdateIntervalSecs = 600_uL, // 10 min + ) + val REGTEST = RuntimeSyncIntervals( + onchainWalletSyncIntervalSecs = 10_uL, + lightningWalletSyncIntervalSecs = 10_uL, + feeRateCacheUpdateIntervalSecs = 10_uL, + ) +} diff --git a/app/src/main/java/to/bitkit/ext/RuntimeSyncIntervals.kt b/app/src/main/java/to/bitkit/ext/RuntimeSyncIntervals.kt new file mode 100644 index 000000000..68d932940 --- /dev/null +++ b/app/src/main/java/to/bitkit/ext/RuntimeSyncIntervals.kt @@ -0,0 +1,10 @@ +package to.bitkit.ext + +import org.lightningdevkit.ldknode.BackgroundSyncConfig +import org.lightningdevkit.ldknode.RuntimeSyncIntervals + +fun RuntimeSyncIntervals.toBackgroundSyncConfig(): BackgroundSyncConfig = BackgroundSyncConfig( + onchainWalletSyncIntervalSecs = onchainWalletSyncIntervalSecs, + lightningWalletSyncIntervalSecs = lightningWalletSyncIntervalSecs, + feeRateCacheUpdateIntervalSecs = feeRateCacheUpdateIntervalSecs, +) diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 3a337b662..dde0a53a8 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.withTimeout import kotlinx.serialization.Serializable import org.lightningdevkit.ldknode.Address import org.lightningdevkit.ldknode.AnchorChannelsConfig -import org.lightningdevkit.ldknode.BackgroundSyncConfig import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.Bolt11Invoice import org.lightningdevkit.ldknode.Bolt11InvoiceDescription @@ -49,6 +48,7 @@ import to.bitkit.ext.totalNextOutboundHtlcLimitSats import to.bitkit.ext.uByteList import to.bitkit.ext.uri import to.bitkit.models.OpenChannelResult +import to.bitkit.ext.toBackgroundSyncConfig import to.bitkit.utils.LdkError import to.bitkit.utils.LdkLogWriter import to.bitkit.utils.Logger @@ -193,11 +193,7 @@ class LightningService @Inject constructor( setChainSourceElectrum( serverUrl = serverUrl, config = ElectrumSyncConfig( - BackgroundSyncConfig( - onchainWalletSyncIntervalSecs = Env.syncIntervals.onchainWalletSyncIntervalSecs, - lightningWalletSyncIntervalSecs = Env.syncIntervals.lightningWalletSyncIntervalSecs, - feeRateCacheUpdateIntervalSecs = Env.syncIntervals.feeRateCacheUpdateIntervalSecs, - ), + backgroundSyncConfig = Env.syncIntervals.toBackgroundSyncConfig(), ), ) } From 8a6ad11332b419d9400e27f8045b6b950ea4c3b8 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 26 Jan 2026 15:40:26 +0100 Subject: [PATCH 4/4] fix: battery saving lifecycle detection --- app/src/main/java/to/bitkit/App.kt | 24 ++------- app/src/main/java/to/bitkit/AppLifecycle.kt | 53 +++++++++++++++++++ .../androidServices/LightningNodeService.kt | 49 ++++++++--------- app/src/main/java/to/bitkit/env/Env.kt | 4 +- .../main/java/to/bitkit/fcm/WakeNodeWorker.kt | 2 +- .../to/bitkit/services/LightningService.kt | 2 +- .../LightningNodeServiceTest.kt | 12 ++--- 7 files changed, 89 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/to/bitkit/AppLifecycle.kt diff --git a/app/src/main/java/to/bitkit/App.kt b/app/src/main/java/to/bitkit/App.kt index 27b3a7c17..187b6296a 100644 --- a/app/src/main/java/to/bitkit/App.kt +++ b/app/src/main/java/to/bitkit/App.kt @@ -1,10 +1,7 @@ package to.bitkit import android.annotation.SuppressLint -import android.app.Activity import android.app.Application -import android.app.Application.ActivityLifecycleCallbacks -import android.os.Bundle import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import dagger.hilt.android.HiltAndroidApp @@ -23,27 +20,12 @@ internal open class App : Application(), Configuration.Provider { override fun onCreate() { super.onCreate() - currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) } + lifecycle = AppLifecycle().also { registerActivityLifecycleCallbacks(it) } Env.initAppStoragePath(filesDir.absolutePath) } companion object { - @SuppressLint("StaticFieldLeak") // Should be safe given its manual memory management - internal var currentActivity: CurrentActivity? = null + @SuppressLint("StaticFieldLeak") // Should be safe given the manual memory management + internal var lifecycle: AppLifecycle? = null } } - -class CurrentActivity : ActivityLifecycleCallbacks { - var value: Activity? = null - private set - - override fun onActivityCreated(activity: Activity, bundle: Bundle?) = Unit - override fun onActivityStarted(activity: Activity) = run { this.value = activity } - override fun onActivityResumed(activity: Activity) = run { this.value = activity } - override fun onActivityPaused(activity: Activity) = clearIfCurrent(activity) - override fun onActivityStopped(activity: Activity) = clearIfCurrent(activity) - override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) = Unit - override fun onActivityDestroyed(activity: Activity) = clearIfCurrent(activity) - - private fun clearIfCurrent(activity: Activity) = run { if (this.value == activity) this.value = null } -} diff --git a/app/src/main/java/to/bitkit/AppLifecycle.kt b/app/src/main/java/to/bitkit/AppLifecycle.kt new file mode 100644 index 000000000..560e46d4f --- /dev/null +++ b/app/src/main/java/to/bitkit/AppLifecycle.kt @@ -0,0 +1,53 @@ +package to.bitkit + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle + +fun interface AppLifecycleListener { + fun onAppLifecycleChanged(isInForeground: Boolean) +} + +class AppLifecycle : ActivityLifecycleCallbacks { + var activity: Activity? = null + private set + + private val listeners = mutableListOf() + + val isInForeground: Boolean get() = activity != null + + fun addListener(listener: AppLifecycleListener) { + listeners.add(listener) + } + + fun removeListener(listener: AppLifecycleListener) { + listeners.remove(listener) + } + + override fun onActivityCreated(activity: Activity, bundle: Bundle?) = Unit + + override fun onActivityStarted(activity: Activity) { + val wasInBackground = this.activity == null + this.activity = activity + if (wasInBackground) notifyListeners(isInForeground = true) + } + + override fun onActivityResumed(activity: Activity) = run { this.activity = activity } + + override fun onActivityPaused(activity: Activity) = clearIfCurrent(activity) + + override fun onActivityStopped(activity: Activity) { + val wasInForeground = this.activity != null + clearIfCurrent(activity) + if (wasInForeground && this.activity == null) notifyListeners(isInForeground = false) + } + + override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) = Unit + override fun onActivityDestroyed(activity: Activity) = clearIfCurrent(activity) + + private fun clearIfCurrent(activity: Activity) = run { if (this.activity == activity) this.activity = null } + + private fun notifyListeners(isInForeground: Boolean) { + listeners.forEach { it.onAppLifecycleChanged(isInForeground) } + } +} diff --git a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt index b682c2748..ab282af87 100644 --- a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt +++ b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt @@ -9,9 +9,6 @@ import android.os.IBinder import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ProcessLifecycleOwner import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -20,6 +17,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.Event import to.bitkit.App +import to.bitkit.AppLifecycleListener import to.bitkit.R import to.bitkit.data.CacheStore import to.bitkit.di.UiDispatcher @@ -57,13 +55,30 @@ class LightningNodeService : Service() { @Inject lateinit var cacheStore: CacheStore - private var lifecycleObserver: AppLifecycleObserver? = null + private var lifecycleListener: AppLifecycleListener? = null override fun onCreate() { super.onCreate() startForeground(ID_NOTIFICATION_NODE, createNotification()) setupService() - lifecycleObserver = AppLifecycleObserver().also { ProcessLifecycleOwner.get().lifecycle.addObserver(it) } + setupLifecycleListener() + } + + private fun setupLifecycleListener() { + lifecycleListener = AppLifecycleListener { isInForeground -> + Logger.debug("App lifecycle changed: isInForeground=$isInForeground", context = TAG) + serviceScope.launch { + if (isInForeground) { + lightningRepo.disableBatterySavingMode() + .onSuccess { Logger.debug("Exited battery saving mode", context = TAG) } + .onFailure { Logger.warn("Error exiting battery saving mode", it, context = TAG) } + } else { + lightningRepo.enableBatterySavingMode() + .onSuccess { Logger.debug("Entered battery saving mode", context = TAG) } + .onFailure { Logger.warn("Error entering battery saving mode", it, context = TAG) } + } + } + }.also { App.lifecycle?.addListener(it) } } private fun setupService() { @@ -96,7 +111,7 @@ class LightningNodeService : Service() { sheet: NewTransactionSheetDetails, notification: NotificationDetails, ) { - if (App.currentActivity?.value != null) { + if (App.lifecycle?.activity != null) { Logger.debug("Skipping payment notification: activity is active", context = TAG) return } @@ -134,7 +149,7 @@ class LightningNodeService : Service() { ACTION_STOP_SERVICE_AND_APP -> { Logger.debug("ACTION_STOP_SERVICE_AND_APP detected", context = TAG) // Close activities gracefully without force-stopping the app - App.currentActivity?.value?.finishAffinity() + App.lifecycle?.activity?.finishAffinity() // Stop the service stopSelf() return START_NOT_STICKY @@ -145,7 +160,7 @@ class LightningNodeService : Service() { override fun onDestroy() { Logger.debug("onDestroy", context = TAG) - lifecycleObserver?.let { ProcessLifecycleOwner.get().lifecycle.removeObserver(it) } + lifecycleListener?.let { App.lifecycle?.removeListener(it) } serviceScope.launch { lightningRepo.stop() serviceScope.cancel() @@ -165,24 +180,6 @@ class LightningNodeService : Service() { override fun onBind(intent: Intent?): IBinder? = null - private inner class AppLifecycleObserver : DefaultLifecycleObserver { - override fun onStart(owner: LifecycleOwner) { - serviceScope.launch { - lightningRepo.disableBatterySavingMode() - .onSuccess { Logger.debug("Sync intervals exited battery saving mode", context = TAG) } - .onFailure { Logger.warn("Error setting sync intervals out of battery saving", it, context = TAG) } - } - } - - override fun onStop(owner: LifecycleOwner) { - serviceScope.launch { - lightningRepo.enableBatterySavingMode() - .onSuccess { Logger.debug("Sync intervals entered battery saving mode", context = TAG) } - .onFailure { Logger.warn("Error setting sync intervals set to battery saving", it, context = TAG) } - } - } - } - companion object { const val TAG = "LightningNodeService" const val CHANNEL_ID_NODE = "bitkit_notification_channel_node" diff --git a/app/src/main/java/to/bitkit/env/Env.kt b/app/src/main/java/to/bitkit/env/Env.kt index e225f84de..147faf472 100644 --- a/app/src/main/java/to/bitkit/env/Env.kt +++ b/app/src/main/java/to/bitkit/env/Env.kt @@ -26,7 +26,7 @@ internal object Env { val ldkLogLevel = LogLevel.TRACE - val syncIntervals = if (network == Network.REGTEST) SyncIntervals.REGTEST else SyncIntervals.DEFAULT + val syncIntervals = if (network == Network.REGTEST) SyncIntervals.FAST else SyncIntervals.DEFAULT val trustedLnPeers get() = when (network) { @@ -229,7 +229,7 @@ private object SyncIntervals { lightningWalletSyncIntervalSecs = 30_uL, feeRateCacheUpdateIntervalSecs = 600_uL, // 10 min ) - val REGTEST = RuntimeSyncIntervals( + val FAST = RuntimeSyncIntervals( onchainWalletSyncIntervalSecs = 10_uL, lightningWalletSyncIntervalSecs = 10_uL, feeRateCacheUpdateIntervalSecs = 10_uL, diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index 7702122f1..2cda1dd4c 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -264,7 +264,7 @@ class WakeNodeWorker @AssistedInject constructor( // Only stop node if app is not in foreground // LightningNodeService will keep node running in background when notifications are enabled - if (App.currentActivity?.value == null) { + if (App.lifecycle?.activity == null) { Logger.debug("App in background, stopping node after notification delivery", context = TAG) lightningRepo.stop() } else { diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index dde0a53a8..32c45eecc 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -44,11 +44,11 @@ import to.bitkit.data.backup.VssStoreIdProvider import to.bitkit.data.keychain.Keychain import to.bitkit.di.BgDispatcher import to.bitkit.env.Env +import to.bitkit.ext.toBackgroundSyncConfig import to.bitkit.ext.totalNextOutboundHtlcLimitSats import to.bitkit.ext.uByteList import to.bitkit.ext.uri import to.bitkit.models.OpenChannelResult -import to.bitkit.ext.toBackgroundSyncConfig import to.bitkit.utils.LdkError import to.bitkit.utils.LdkLogWriter import to.bitkit.utils.Logger diff --git a/app/src/test/java/to/bitkit/androidServices/LightningNodeServiceTest.kt b/app/src/test/java/to/bitkit/androidServices/LightningNodeServiceTest.kt index 200705b97..ad51794f7 100644 --- a/app/src/test/java/to/bitkit/androidServices/LightningNodeServiceTest.kt +++ b/app/src/test/java/to/bitkit/androidServices/LightningNodeServiceTest.kt @@ -36,7 +36,7 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows import org.robolectric.annotation.Config import to.bitkit.App -import to.bitkit.CurrentActivity +import to.bitkit.AppLifecycle import to.bitkit.R import to.bitkit.data.AppCacheData import to.bitkit.data.CacheStore @@ -115,13 +115,13 @@ class LightningNodeServiceTest : BaseUnitTest() { val app = context as Application Shadows.shadowOf(app).grantPermissions(Manifest.permission.POST_NOTIFICATIONS) - // Reset App.currentActivity to simulate background state - App.currentActivity = CurrentActivity() + // Reset App.lifecycle to simulate background state + App.lifecycle = AppLifecycle() } @After fun tearDown() { - App.currentActivity = null + App.lifecycle = null } @Test @@ -162,9 +162,9 @@ class LightningNodeServiceTest : BaseUnitTest() { @Test fun `payment received in foreground does nothing`() = test { - // Simulate foreground by setting App.currentActivity.value via lifecycle callback + // Simulate foreground by setting App.lifecycle.value via lifecycle callback val mockActivity: Activity = mock() - App.currentActivity?.onActivityStarted(mockActivity) + App.lifecycle?.onActivityStarted(mockActivity) val controller = Robolectric.buildService(LightningNodeService::class.java) controller.create().startCommand(0, 0)