Skip to content

Commit 19f5fc1

Browse files
committed
fix: widget refresh scheduling
1 parent 740e60a commit 19f5fc1

30 files changed

Lines changed: 945 additions & 311 deletions

app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<uses-permission android:name="android.permission.INTERNET" />
2323
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
2424
<uses-permission android:name="android.permission.CAMERA" />
25+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
2526
<uses-permission
2627
android:name="android.permission.FOREGROUND_SERVICE"
2728
tools:ignore="ForegroundServicePermission,ForegroundServicesPolicy" />
@@ -173,8 +174,14 @@
173174
</service>
174175

175176
<receiver
176-
android:name=".appwidget.AppWidgetRefreshAlarmReceiver"
177-
android:exported="false" />
177+
android:name=".appwidget.AppWidgetRefreshReceiver"
178+
android:exported="false">
179+
<intent-filter>
180+
<action android:name="android.intent.action.BOOT_COMPLETED" />
181+
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
182+
<action android:name="to.bitkit.appwidget.REFRESH_ALARM" />
183+
</intent-filter>
184+
</receiver>
178185

179186
<provider
180187
android:name="androidx.startup.InitializationProvider"

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

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,13 @@ import android.annotation.SuppressLint
44
import android.app.Activity
55
import android.app.Application
66
import android.app.Application.ActivityLifecycleCallbacks
7-
import android.content.Intent
8-
import android.content.IntentFilter
97
import android.os.Bundle
10-
import android.os.PowerManager
11-
import androidx.core.content.ContextCompat
128
import androidx.hilt.work.HiltWorkerFactory
139
import androidx.work.Configuration
1410
import coil3.ImageLoader
1511
import coil3.SingletonImageLoader
1612
import dagger.hilt.android.HiltAndroidApp
17-
import to.bitkit.appwidget.AppWidgetRefreshReceiver
18-
import to.bitkit.appwidget.AppWidgetRefreshWorker
13+
import to.bitkit.appwidget.AppWidgetRefreshScheduler
1914
import to.bitkit.env.Env
2015
import to.bitkit.services.BluetoothInit
2116
import javax.inject.Inject
@@ -28,6 +23,9 @@ internal open class App : Application(), Configuration.Provider {
2823
@Inject
2924
lateinit var imageLoader: ImageLoader
3025

26+
@Inject
27+
lateinit var appWidgetRefreshScheduler: AppWidgetRefreshScheduler
28+
3129
override val workManagerConfiguration
3230
get() = Configuration.Builder()
3331
.setWorkerFactory(workerFactory)
@@ -37,26 +35,12 @@ internal open class App : Application(), Configuration.Provider {
3735
super.onCreate()
3836
SingletonImageLoader.setSafe { imageLoader }
3937
currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) }
40-
registerAppWidgetRefreshReceiver()
41-
AppWidgetRefreshWorker.enqueue(this)
38+
appWidgetRefreshScheduler.ensureScheduled("app_start")
4239
Env.initAppStoragePath(filesDir.absolutePath)
4340
// Initialize btleplug for Bluetooth support (required before any BLE usage)
4441
BluetoothInit.ensureInitialized()
4542
}
4643

47-
private fun registerAppWidgetRefreshReceiver() {
48-
val filter = IntentFilter().apply {
49-
addAction(Intent.ACTION_USER_PRESENT)
50-
addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
51-
}
52-
ContextCompat.registerReceiver(
53-
this,
54-
AppWidgetRefreshReceiver(),
55-
filter,
56-
ContextCompat.RECEIVER_NOT_EXPORTED,
57-
)
58-
}
59-
6044
companion object {
6145
@SuppressLint("StaticFieldLeak") // Should be safe given its manual memory management
6246
internal var currentActivity: CurrentActivity? = null

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import kotlinx.coroutines.launch
1717
import org.lightningdevkit.ldknode.Event
1818
import to.bitkit.App
1919
import to.bitkit.R
20-
import to.bitkit.appwidget.AppWidgetRefreshWorker
20+
import to.bitkit.appwidget.AppWidgetRefreshScheduler
2121
import to.bitkit.data.CacheStore
2222
import to.bitkit.di.UiDispatcher
2323
import to.bitkit.domain.commands.NotifyPaymentReceived
@@ -60,6 +60,9 @@ class LightningNodeService : Service() {
6060
@Inject
6161
lateinit var cacheStore: CacheStore
6262

63+
@Inject
64+
lateinit var appWidgetRefreshScheduler: AppWidgetRefreshScheduler
65+
6366
override fun onCreate() {
6467
super.onCreate()
6568
startForeground(ID_NOTIFICATION_NODE, createNotification())
@@ -154,6 +157,8 @@ class LightningNodeService : Service() {
154157
when (intent?.action) {
155158
ACTION_STOP_SERVICE_AND_APP -> {
156159
Logger.debug("ACTION_STOP_SERVICE_AND_APP detected", context = TAG)
160+
appWidgetRefreshScheduler.ensureScheduled("service_stop_action")
161+
appWidgetRefreshScheduler.requestCatchUp("service_stop_action")
157162
serviceScope.launch {
158163
lightningRepo.stop()
159164
activityManager.appTasks.forEach { it.finishAndRemoveTask() }
@@ -167,8 +172,6 @@ class LightningNodeService : Service() {
167172

168173
override fun onDestroy() {
169174
Logger.debug("onDestroy", context = TAG)
170-
AppWidgetRefreshWorker.enqueue(this)
171-
AppWidgetRefreshWorker.enqueueCatchUp(this)
172175
// Safe to call even if already stopped — guarded by lifecycleMutex + isStoppedOrStopping()
173176
serviceScope.launch { lightningRepo.stop() }
174177
super.onDestroy()

app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import androidx.datastore.core.DataStore
55
import androidx.datastore.dataStore
66
import dagger.hilt.EntryPoint
77
import dagger.hilt.InstallIn
8+
import dagger.hilt.android.EntryPointAccessors
89
import dagger.hilt.android.qualifiers.ApplicationContext
910
import dagger.hilt.components.SingletonComponent
1011
import kotlinx.coroutines.flow.Flow
1112
import kotlinx.coroutines.flow.first
1213
import kotlinx.coroutines.flow.map
1314
import to.bitkit.appwidget.model.AppWidgetData
1415
import to.bitkit.appwidget.model.AppWidgetEntry
16+
import to.bitkit.appwidget.model.AppWidgetRefreshMetadata
1517
import to.bitkit.appwidget.model.AppWidgetType
1618
import to.bitkit.data.dto.ArticleDTO
1719
import to.bitkit.data.dto.BlockDTO
@@ -33,9 +35,15 @@ private val Context.appWidgetDataStore: DataStore<AppWidgetData> by dataStore(
3335
interface AppWidgetEntryPoint {
3436
fun appWidgetPreferencesStore(): AppWidgetPreferencesStore
3537
fun appWidgetDataRepository(): AppWidgetDataRepository
38+
fun appWidgetRefreshScheduler(): AppWidgetRefreshScheduler
3639
fun currencyRepo(): CurrencyRepo
3740
}
3841

42+
val Context.appWidgetRefreshScheduler: AppWidgetRefreshScheduler
43+
get() = EntryPointAccessors
44+
.fromApplication(applicationContext, AppWidgetEntryPoint::class.java)
45+
.appWidgetRefreshScheduler()
46+
3947
@Singleton
4048
@Suppress("TooManyFunctions")
4149
class AppWidgetPreferencesStore @Inject constructor(
@@ -80,6 +88,17 @@ class AppWidgetPreferencesStore @Inject constructor(
8088
.map { it.pricePreferences.period }
8189
.toSet()
8290

91+
suspend fun getRefreshMetadata(type: AppWidgetType): AppWidgetRefreshMetadata =
92+
store.data.first().refreshMetadata[type] ?: AppWidgetRefreshMetadata()
93+
94+
suspend fun markRefreshAttempt(type: AppWidgetType, timestampMs: Long) {
95+
updateRefreshMetadata(type) { it.copy(lastAttemptAtMs = timestampMs) }
96+
}
97+
98+
suspend fun markRefreshSuccess(type: AppWidgetType, timestampMs: Long) {
99+
updateRefreshMetadata(type) { it.copy(lastSuccessAtMs = timestampMs) }
100+
}
101+
83102
fun hasWidgetsOfType(type: AppWidgetType): Flow<Boolean> =
84103
data.map { it.entries.any { entry -> entry.type == type } }
85104

@@ -112,4 +131,14 @@ class AppWidgetPreferencesStore @Inject constructor(
112131
suspend fun cacheWeather(weather: WeatherDTO) {
113132
store.updateData { it.copy(cachedWeather = weather) }
114133
}
134+
135+
private suspend fun updateRefreshMetadata(
136+
type: AppWidgetType,
137+
transform: (AppWidgetRefreshMetadata) -> AppWidgetRefreshMetadata,
138+
) {
139+
store.updateData {
140+
val current = it.refreshMetadata[type] ?: AppWidgetRefreshMetadata()
141+
it.copy(refreshMetadata = it.refreshMetadata + (type to transform(current)))
142+
}
143+
}
115144
}

app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshAlarmReceiver.kt

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package to.bitkit.appwidget
2+
3+
import to.bitkit.appwidget.model.AppWidgetRefreshMetadata
4+
import to.bitkit.appwidget.model.AppWidgetType
5+
6+
object AppWidgetRefreshPolicy {
7+
fun shouldRefreshRemote(
8+
type: AppWidgetType,
9+
metadata: AppWidgetRefreshMetadata,
10+
nowMs: Long,
11+
): Boolean {
12+
if (!type.isRemoteBacked()) return false
13+
if (metadata.lastSuccessAtMs <= 0L) return true
14+
return nowMs - metadata.lastSuccessAtMs >= AppWidgetRefreshScheduler.REFRESH_INTERVAL.inWholeMilliseconds
15+
}
16+
17+
fun AppWidgetType.isRemoteBacked(): Boolean = this != AppWidgetType.FACTS
18+
}

app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@ package to.bitkit.appwidget
33
import android.content.BroadcastReceiver
44
import android.content.Context
55
import android.content.Intent
6-
import android.os.PowerManager
7-
import to.bitkit.ext.powerManager
86
import to.bitkit.utils.Logger
97

108
class AppWidgetRefreshReceiver : BroadcastReceiver() {
119
override fun onReceive(context: Context, intent: Intent) {
1210
when (intent.action) {
13-
Intent.ACTION_USER_PRESENT -> enqueueCatchUp(context, "user_present")
14-
PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED -> {
15-
if (!context.powerManager.isDeviceIdleMode) enqueueCatchUp(context, "device_idle_exit")
11+
Intent.ACTION_BOOT_COMPLETED -> scheduleAfterSystemEvent(context, "boot_completed")
12+
Intent.ACTION_MY_PACKAGE_REPLACED -> scheduleAfterSystemEvent(context, "package_replaced")
13+
AppWidgetRefreshScheduler.CATCH_UP_ALARM_ACTION -> {
14+
Logger.debug("Received widget refresh alarm", context = TAG)
15+
context.appWidgetRefreshScheduler.handleCatchUpAlarm("catch_up_alarm")
1616
}
1717
}
1818
}
1919

20-
private fun enqueueCatchUp(context: Context, reason: String) {
21-
Logger.debug("Enqueued widget refresh for '$reason'", context = TAG)
22-
AppWidgetRefreshWorker.enqueueCatchUp(context)
20+
private fun scheduleAfterSystemEvent(context: Context, reason: String) {
21+
Logger.debug("Received widget refresh event for '$reason'", context = TAG)
22+
context.appWidgetRefreshScheduler.ensureScheduled(reason)
23+
context.appWidgetRefreshScheduler.requestCatchUp(reason)
2324
}
2425

2526
private companion object {

0 commit comments

Comments
 (0)