Skip to content

Commit 8a6ad11

Browse files
committed
fix: battery saving lifecycle detection
1 parent 31d146e commit 8a6ad11

7 files changed

Lines changed: 89 additions & 57 deletions

File tree

app/src/main/java/to/bitkit/App.kt

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package to.bitkit
22

33
import android.annotation.SuppressLint
4-
import android.app.Activity
54
import android.app.Application
6-
import android.app.Application.ActivityLifecycleCallbacks
7-
import android.os.Bundle
85
import androidx.hilt.work.HiltWorkerFactory
96
import androidx.work.Configuration
107
import dagger.hilt.android.HiltAndroidApp
@@ -23,27 +20,12 @@ internal open class App : Application(), Configuration.Provider {
2320

2421
override fun onCreate() {
2522
super.onCreate()
26-
currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) }
23+
lifecycle = AppLifecycle().also { registerActivityLifecycleCallbacks(it) }
2724
Env.initAppStoragePath(filesDir.absolutePath)
2825
}
2926

3027
companion object {
31-
@SuppressLint("StaticFieldLeak") // Should be safe given its manual memory management
32-
internal var currentActivity: CurrentActivity? = null
28+
@SuppressLint("StaticFieldLeak") // Should be safe given the manual memory management
29+
internal var lifecycle: AppLifecycle? = null
3330
}
3431
}
35-
36-
class CurrentActivity : ActivityLifecycleCallbacks {
37-
var value: Activity? = null
38-
private set
39-
40-
override fun onActivityCreated(activity: Activity, bundle: Bundle?) = Unit
41-
override fun onActivityStarted(activity: Activity) = run { this.value = activity }
42-
override fun onActivityResumed(activity: Activity) = run { this.value = activity }
43-
override fun onActivityPaused(activity: Activity) = clearIfCurrent(activity)
44-
override fun onActivityStopped(activity: Activity) = clearIfCurrent(activity)
45-
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) = Unit
46-
override fun onActivityDestroyed(activity: Activity) = clearIfCurrent(activity)
47-
48-
private fun clearIfCurrent(activity: Activity) = run { if (this.value == activity) this.value = null }
49-
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package to.bitkit
2+
3+
import android.app.Activity
4+
import android.app.Application.ActivityLifecycleCallbacks
5+
import android.os.Bundle
6+
7+
fun interface AppLifecycleListener {
8+
fun onAppLifecycleChanged(isInForeground: Boolean)
9+
}
10+
11+
class AppLifecycle : ActivityLifecycleCallbacks {
12+
var activity: Activity? = null
13+
private set
14+
15+
private val listeners = mutableListOf<AppLifecycleListener>()
16+
17+
val isInForeground: Boolean get() = activity != null
18+
19+
fun addListener(listener: AppLifecycleListener) {
20+
listeners.add(listener)
21+
}
22+
23+
fun removeListener(listener: AppLifecycleListener) {
24+
listeners.remove(listener)
25+
}
26+
27+
override fun onActivityCreated(activity: Activity, bundle: Bundle?) = Unit
28+
29+
override fun onActivityStarted(activity: Activity) {
30+
val wasInBackground = this.activity == null
31+
this.activity = activity
32+
if (wasInBackground) notifyListeners(isInForeground = true)
33+
}
34+
35+
override fun onActivityResumed(activity: Activity) = run { this.activity = activity }
36+
37+
override fun onActivityPaused(activity: Activity) = clearIfCurrent(activity)
38+
39+
override fun onActivityStopped(activity: Activity) {
40+
val wasInForeground = this.activity != null
41+
clearIfCurrent(activity)
42+
if (wasInForeground && this.activity == null) notifyListeners(isInForeground = false)
43+
}
44+
45+
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) = Unit
46+
override fun onActivityDestroyed(activity: Activity) = clearIfCurrent(activity)
47+
48+
private fun clearIfCurrent(activity: Activity) = run { if (this.activity == activity) this.activity = null }
49+
50+
private fun notifyListeners(isInForeground: Boolean) {
51+
listeners.forEach { it.onAppLifecycleChanged(isInForeground) }
52+
}
53+
}

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

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ import android.os.IBinder
99
import androidx.annotation.RequiresApi
1010
import androidx.core.app.NotificationCompat
1111
import androidx.core.content.ContextCompat
12-
import androidx.lifecycle.DefaultLifecycleObserver
13-
import androidx.lifecycle.LifecycleOwner
14-
import androidx.lifecycle.ProcessLifecycleOwner
1512
import dagger.hilt.android.AndroidEntryPoint
1613
import kotlinx.coroutines.CoroutineDispatcher
1714
import kotlinx.coroutines.CoroutineScope
@@ -20,6 +17,7 @@ import kotlinx.coroutines.cancel
2017
import kotlinx.coroutines.launch
2118
import org.lightningdevkit.ldknode.Event
2219
import to.bitkit.App
20+
import to.bitkit.AppLifecycleListener
2321
import to.bitkit.R
2422
import to.bitkit.data.CacheStore
2523
import to.bitkit.di.UiDispatcher
@@ -57,13 +55,30 @@ class LightningNodeService : Service() {
5755
@Inject
5856
lateinit var cacheStore: CacheStore
5957

60-
private var lifecycleObserver: AppLifecycleObserver? = null
58+
private var lifecycleListener: AppLifecycleListener? = null
6159

6260
override fun onCreate() {
6361
super.onCreate()
6462
startForeground(ID_NOTIFICATION_NODE, createNotification())
6563
setupService()
66-
lifecycleObserver = AppLifecycleObserver().also { ProcessLifecycleOwner.get().lifecycle.addObserver(it) }
64+
setupLifecycleListener()
65+
}
66+
67+
private fun setupLifecycleListener() {
68+
lifecycleListener = AppLifecycleListener { isInForeground ->
69+
Logger.debug("App lifecycle changed: isInForeground=$isInForeground", context = TAG)
70+
serviceScope.launch {
71+
if (isInForeground) {
72+
lightningRepo.disableBatterySavingMode()
73+
.onSuccess { Logger.debug("Exited battery saving mode", context = TAG) }
74+
.onFailure { Logger.warn("Error exiting battery saving mode", it, context = TAG) }
75+
} else {
76+
lightningRepo.enableBatterySavingMode()
77+
.onSuccess { Logger.debug("Entered battery saving mode", context = TAG) }
78+
.onFailure { Logger.warn("Error entering battery saving mode", it, context = TAG) }
79+
}
80+
}
81+
}.also { App.lifecycle?.addListener(it) }
6782
}
6883

6984
private fun setupService() {
@@ -96,7 +111,7 @@ class LightningNodeService : Service() {
96111
sheet: NewTransactionSheetDetails,
97112
notification: NotificationDetails,
98113
) {
99-
if (App.currentActivity?.value != null) {
114+
if (App.lifecycle?.activity != null) {
100115
Logger.debug("Skipping payment notification: activity is active", context = TAG)
101116
return
102117
}
@@ -134,7 +149,7 @@ class LightningNodeService : Service() {
134149
ACTION_STOP_SERVICE_AND_APP -> {
135150
Logger.debug("ACTION_STOP_SERVICE_AND_APP detected", context = TAG)
136151
// Close activities gracefully without force-stopping the app
137-
App.currentActivity?.value?.finishAffinity()
152+
App.lifecycle?.activity?.finishAffinity()
138153
// Stop the service
139154
stopSelf()
140155
return START_NOT_STICKY
@@ -145,7 +160,7 @@ class LightningNodeService : Service() {
145160

146161
override fun onDestroy() {
147162
Logger.debug("onDestroy", context = TAG)
148-
lifecycleObserver?.let { ProcessLifecycleOwner.get().lifecycle.removeObserver(it) }
163+
lifecycleListener?.let { App.lifecycle?.removeListener(it) }
149164
serviceScope.launch {
150165
lightningRepo.stop()
151166
serviceScope.cancel()
@@ -165,24 +180,6 @@ class LightningNodeService : Service() {
165180

166181
override fun onBind(intent: Intent?): IBinder? = null
167182

168-
private inner class AppLifecycleObserver : DefaultLifecycleObserver {
169-
override fun onStart(owner: LifecycleOwner) {
170-
serviceScope.launch {
171-
lightningRepo.disableBatterySavingMode()
172-
.onSuccess { Logger.debug("Sync intervals exited battery saving mode", context = TAG) }
173-
.onFailure { Logger.warn("Error setting sync intervals out of battery saving", it, context = TAG) }
174-
}
175-
}
176-
177-
override fun onStop(owner: LifecycleOwner) {
178-
serviceScope.launch {
179-
lightningRepo.enableBatterySavingMode()
180-
.onSuccess { Logger.debug("Sync intervals entered battery saving mode", context = TAG) }
181-
.onFailure { Logger.warn("Error setting sync intervals set to battery saving", it, context = TAG) }
182-
}
183-
}
184-
}
185-
186183
companion object {
187184
const val TAG = "LightningNodeService"
188185
const val CHANNEL_ID_NODE = "bitkit_notification_channel_node"

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ internal object Env {
2626

2727
val ldkLogLevel = LogLevel.TRACE
2828

29-
val syncIntervals = if (network == Network.REGTEST) SyncIntervals.REGTEST else SyncIntervals.DEFAULT
29+
val syncIntervals = if (network == Network.REGTEST) SyncIntervals.FAST else SyncIntervals.DEFAULT
3030

3131
val trustedLnPeers
3232
get() = when (network) {
@@ -229,7 +229,7 @@ private object SyncIntervals {
229229
lightningWalletSyncIntervalSecs = 30_uL,
230230
feeRateCacheUpdateIntervalSecs = 600_uL, // 10 min
231231
)
232-
val REGTEST = RuntimeSyncIntervals(
232+
val FAST = RuntimeSyncIntervals(
233233
onchainWalletSyncIntervalSecs = 10_uL,
234234
lightningWalletSyncIntervalSecs = 10_uL,
235235
feeRateCacheUpdateIntervalSecs = 10_uL,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ class WakeNodeWorker @AssistedInject constructor(
264264

265265
// Only stop node if app is not in foreground
266266
// LightningNodeService will keep node running in background when notifications are enabled
267-
if (App.currentActivity?.value == null) {
267+
if (App.lifecycle?.activity == null) {
268268
Logger.debug("App in background, stopping node after notification delivery", context = TAG)
269269
lightningRepo.stop()
270270
} else {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ import to.bitkit.data.backup.VssStoreIdProvider
4444
import to.bitkit.data.keychain.Keychain
4545
import to.bitkit.di.BgDispatcher
4646
import to.bitkit.env.Env
47+
import to.bitkit.ext.toBackgroundSyncConfig
4748
import to.bitkit.ext.totalNextOutboundHtlcLimitSats
4849
import to.bitkit.ext.uByteList
4950
import to.bitkit.ext.uri
5051
import to.bitkit.models.OpenChannelResult
51-
import to.bitkit.ext.toBackgroundSyncConfig
5252
import to.bitkit.utils.LdkError
5353
import to.bitkit.utils.LdkLogWriter
5454
import to.bitkit.utils.Logger

app/src/test/java/to/bitkit/androidServices/LightningNodeServiceTest.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import org.robolectric.RobolectricTestRunner
3636
import org.robolectric.Shadows
3737
import org.robolectric.annotation.Config
3838
import to.bitkit.App
39-
import to.bitkit.CurrentActivity
39+
import to.bitkit.AppLifecycle
4040
import to.bitkit.R
4141
import to.bitkit.data.AppCacheData
4242
import to.bitkit.data.CacheStore
@@ -115,13 +115,13 @@ class LightningNodeServiceTest : BaseUnitTest() {
115115
val app = context as Application
116116
Shadows.shadowOf(app).grantPermissions(Manifest.permission.POST_NOTIFICATIONS)
117117

118-
// Reset App.currentActivity to simulate background state
119-
App.currentActivity = CurrentActivity()
118+
// Reset App.lifecycle to simulate background state
119+
App.lifecycle = AppLifecycle()
120120
}
121121

122122
@After
123123
fun tearDown() {
124-
App.currentActivity = null
124+
App.lifecycle = null
125125
}
126126

127127
@Test
@@ -162,9 +162,9 @@ class LightningNodeServiceTest : BaseUnitTest() {
162162

163163
@Test
164164
fun `payment received in foreground does nothing`() = test {
165-
// Simulate foreground by setting App.currentActivity.value via lifecycle callback
165+
// Simulate foreground by setting App.lifecycle.value via lifecycle callback
166166
val mockActivity: Activity = mock()
167-
App.currentActivity?.onActivityStarted(mockActivity)
167+
App.lifecycle?.onActivityStarted(mockActivity)
168168

169169
val controller = Robolectric.buildService(LightningNodeService::class.java)
170170
controller.create().startCommand(0, 0)

0 commit comments

Comments
 (0)