Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE"
tools:ignore="ForegroundServicePermission,ForegroundServicesPolicy" />
Expand Down Expand Up @@ -172,6 +173,16 @@
</intent-filter>
</service>

<receiver
android:name=".appwidget.AppWidgetRefreshReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="to.bitkit.appwidget.REFRESH_ALARM" />
</intent-filter>
</receiver>

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/to/bitkit/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import androidx.work.Configuration
import coil3.ImageLoader
import coil3.SingletonImageLoader
import dagger.hilt.android.HiltAndroidApp
import to.bitkit.appwidget.AppWidgetRefreshReason
import to.bitkit.appwidget.AppWidgetRefreshScheduler
import to.bitkit.env.Env
import to.bitkit.services.BluetoothInit
import javax.inject.Inject
Expand All @@ -22,6 +24,9 @@ internal open class App : Application(), Configuration.Provider {
@Inject
lateinit var imageLoader: ImageLoader

@Inject
lateinit var appWidgetRefreshScheduler: AppWidgetRefreshScheduler

override val workManagerConfiguration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
Expand All @@ -31,6 +36,7 @@ internal open class App : Application(), Configuration.Provider {
super.onCreate()
SingletonImageLoader.setSafe { imageLoader }
currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) }
appWidgetRefreshScheduler.ensureScheduled(AppWidgetRefreshReason.APP_START)
Env.initAppStoragePath(filesDir.absolutePath)
// Initialize btleplug for Bluetooth support (required before any BLE usage)
BluetoothInit.ensureInitialized()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import kotlinx.coroutines.launch
import org.lightningdevkit.ldknode.Event
import to.bitkit.App
import to.bitkit.R
import to.bitkit.appwidget.AppWidgetRefreshReason
import to.bitkit.appwidget.AppWidgetRefreshScheduler
import to.bitkit.data.CacheStore
import to.bitkit.di.UiDispatcher
import to.bitkit.domain.commands.NotifyPaymentReceived
Expand Down Expand Up @@ -59,6 +61,9 @@ class LightningNodeService : Service() {
@Inject
lateinit var cacheStore: CacheStore

@Inject
lateinit var appWidgetRefreshScheduler: AppWidgetRefreshScheduler

override fun onCreate() {
super.onCreate()
startForeground(ID_NOTIFICATION_NODE, createNotification())
Expand Down Expand Up @@ -153,6 +158,8 @@ class LightningNodeService : Service() {
when (intent?.action) {
ACTION_STOP_SERVICE_AND_APP -> {
Logger.debug("ACTION_STOP_SERVICE_AND_APP detected", context = TAG)
appWidgetRefreshScheduler.ensureScheduled(AppWidgetRefreshReason.SERVICE_STOP_ACTION)
appWidgetRefreshScheduler.requestCatchUp(AppWidgetRefreshReason.SERVICE_STOP_ACTION)
serviceScope.launch {
lightningRepo.stop()
activityManager.appTasks.forEach { it.finishAndRemoveTask() }
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import to.bitkit.appwidget.model.AppWidgetData
import to.bitkit.appwidget.model.AppWidgetEntry
import to.bitkit.appwidget.model.AppWidgetRefreshMetadata
import to.bitkit.appwidget.model.AppWidgetType
import to.bitkit.data.dto.ArticleDTO
import to.bitkit.data.dto.BlockDTO
Expand All @@ -33,9 +35,15 @@ private val Context.appWidgetDataStore: DataStore<AppWidgetData> by dataStore(
interface AppWidgetEntryPoint {
fun appWidgetPreferencesStore(): AppWidgetPreferencesStore
fun appWidgetDataRepository(): AppWidgetDataRepository
fun appWidgetRefreshScheduler(): AppWidgetRefreshScheduler
fun currencyRepo(): CurrencyRepo
}

val Context.appWidgetRefreshScheduler: AppWidgetRefreshScheduler
get() = EntryPointAccessors
.fromApplication(applicationContext, AppWidgetEntryPoint::class.java)
.appWidgetRefreshScheduler()

@Singleton
@Suppress("TooManyFunctions")
class AppWidgetPreferencesStore @Inject constructor(
Expand Down Expand Up @@ -80,6 +88,17 @@ class AppWidgetPreferencesStore @Inject constructor(
.map { it.pricePreferences.period }
.toSet()

suspend fun getRefreshMetadata(type: AppWidgetType): AppWidgetRefreshMetadata =
store.data.first().refreshMetadata[type] ?: AppWidgetRefreshMetadata()

suspend fun markRefreshAttempt(type: AppWidgetType, timestampMs: Long) {
updateRefreshMetadata(type) { it.copy(lastAttemptAtMs = timestampMs) }
}

suspend fun markRefreshSuccess(type: AppWidgetType, timestampMs: Long) {
updateRefreshMetadata(type) { it.copy(lastSuccessAtMs = timestampMs) }
}

fun hasWidgetsOfType(type: AppWidgetType): Flow<Boolean> =
data.map { it.entries.any { entry -> entry.type == type } }

Expand Down Expand Up @@ -112,4 +131,14 @@ class AppWidgetPreferencesStore @Inject constructor(
suspend fun cacheWeather(weather: WeatherDTO) {
store.updateData { it.copy(cachedWeather = weather) }
}

private suspend fun updateRefreshMetadata(
type: AppWidgetType,
transform: (AppWidgetRefreshMetadata) -> AppWidgetRefreshMetadata,
) {
store.updateData {
val current = it.refreshMetadata[type] ?: AppWidgetRefreshMetadata()
it.copy(refreshMetadata = it.refreshMetadata + (type to transform(current)))
}
}
}
18 changes: 18 additions & 0 deletions app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshPolicy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package to.bitkit.appwidget

import to.bitkit.appwidget.model.AppWidgetRefreshMetadata
import to.bitkit.appwidget.model.AppWidgetType

object AppWidgetRefreshPolicy {
fun shouldRefreshRemote(
type: AppWidgetType,
metadata: AppWidgetRefreshMetadata,
nowMs: Long,
): Boolean {
if (!type.isRemoteBacked()) return false
if (metadata.lastSuccessAtMs <= 0L) return true
return nowMs - metadata.lastSuccessAtMs >= AppWidgetRefreshScheduler.REFRESH_INTERVAL.inWholeMilliseconds
}

fun AppWidgetType.isRemoteBacked(): Boolean = this != AppWidgetType.FACTS
}
37 changes: 37 additions & 0 deletions app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package to.bitkit.appwidget

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import to.bitkit.utils.Logger

class AppWidgetRefreshReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED -> scheduleAfterSystemEvent(
context,
AppWidgetRefreshReason.BOOT_COMPLETED,
)

Intent.ACTION_MY_PACKAGE_REPLACED -> scheduleAfterSystemEvent(
context,
AppWidgetRefreshReason.PACKAGE_REPLACED,
)

AppWidgetRefreshScheduler.CATCH_UP_ALARM_ACTION -> {
Logger.debug("Received widget refresh alarm", context = TAG)
context.appWidgetRefreshScheduler.handleCatchUpAlarm(AppWidgetRefreshReason.CATCH_UP_ALARM)
}
}
}

private fun scheduleAfterSystemEvent(context: Context, reason: AppWidgetRefreshReason) {
Logger.debug("Received widget refresh event for '${reason.name}'", context = TAG)
context.appWidgetRefreshScheduler.ensureScheduled(reason)
context.appWidgetRefreshScheduler.requestCatchUp(reason)
}

private companion object {
const val TAG = "AppWidgetRefreshReceiver"
}
}
Loading
Loading