diff --git a/app/src/main/java/to/bitkit/App.kt b/app/src/main/java/to/bitkit/App.kt index 1620b722a..d7a412346 100644 --- a/app/src/main/java/to/bitkit/App.kt +++ b/app/src/main/java/to/bitkit/App.kt @@ -4,12 +4,17 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.Application import android.app.Application.ActivityLifecycleCallbacks +import android.content.Intent +import android.content.IntentFilter import android.os.Bundle +import android.os.PowerManager +import androidx.core.content.ContextCompat import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import coil3.ImageLoader import coil3.SingletonImageLoader import dagger.hilt.android.HiltAndroidApp +import to.bitkit.appwidget.AppWidgetRefreshReceiver import to.bitkit.env.Env import to.bitkit.services.BluetoothInit import javax.inject.Inject @@ -31,11 +36,25 @@ internal open class App : Application(), Configuration.Provider { super.onCreate() SingletonImageLoader.setSafe { imageLoader } currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) } + registerAppWidgetRefreshReceiver() Env.initAppStoragePath(filesDir.absolutePath) // Initialize btleplug for Bluetooth support (required before any BLE usage) BluetoothInit.ensureInitialized() } + private fun registerAppWidgetRefreshReceiver() { + val filter = IntentFilter().apply { + addAction(Intent.ACTION_USER_PRESENT) + addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED) + } + ContextCompat.registerReceiver( + this, + AppWidgetRefreshReceiver(), + filter, + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + } + companion object { @SuppressLint("StaticFieldLeak") // Should be safe given its manual memory management internal var currentActivity: CurrentActivity? = null diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt new file mode 100644 index 000000000..486406d79 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt @@ -0,0 +1,28 @@ +package to.bitkit.appwidget + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.PowerManager +import to.bitkit.ext.powerManager +import to.bitkit.utils.Logger + +class AppWidgetRefreshReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_USER_PRESENT -> enqueueCatchUp(context, "user_present") + PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED -> { + if (!context.powerManager.isDeviceIdleMode) enqueueCatchUp(context, "device_idle_exit") + } + } + } + + private fun enqueueCatchUp(context: Context, reason: String) { + Logger.debug("Enqueued widget refresh for '$reason'", context = TAG) + AppWidgetRefreshWorker.enqueueCatchUp(context) + } + + private companion object { + const val TAG = "AppWidgetRefreshReceiver" + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index 20621a0cb..6b186e6b3 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -9,7 +9,9 @@ import androidx.hilt.work.HiltWorker import androidx.work.Constraints import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters @@ -41,6 +43,7 @@ class AppWidgetRefreshWorker @AssistedInject constructor( companion object { private const val TAG = "AppWidgetRefreshWorker" private const val WORK_NAME = "appwidget_refresh" + private const val CATCH_UP_WORK_NAME = "appwidget_refresh_catch_up" fun enqueue(context: Context) { val constraints = Constraints.Builder() @@ -58,14 +61,36 @@ class AppWidgetRefreshWorker @AssistedInject constructor( ) } + fun enqueueCatchUp(context: Context) { + if (!hasActiveWidgets(context)) return + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .build() + + WorkManager.getInstance(context).enqueueUniqueWork( + CATCH_UP_WORK_NAME, + ExistingWorkPolicy.KEEP, + request, + ) + } + fun cancelIfNoWidgets(context: Context) { + if (!hasActiveWidgets(context)) { + WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME) + WorkManager.getInstance(context).cancelUniqueWork(CATCH_UP_WORK_NAME) + } + } + + private fun hasActiveWidgets(context: Context): Boolean { val manager = AppWidgetManager.getInstance(context) - val hasAny = AppWidgetType.entries.any { type -> + return AppWidgetType.entries.any { type -> manager.getAppWidgetIds(ComponentName(context, receiverClassFor(type))).isNotEmpty() } - if (!hasAny) { - WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME) - } } private fun receiverClassFor(type: AppWidgetType): Class = when (type) { diff --git a/app/src/main/java/to/bitkit/ext/Context.kt b/app/src/main/java/to/bitkit/ext/Context.kt index 6720b8366..a23138e6f 100644 --- a/app/src/main/java/to/bitkit/ext/Context.kt +++ b/app/src/main/java/to/bitkit/ext/Context.kt @@ -12,6 +12,7 @@ import android.content.ContextWrapper import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.hardware.usb.UsbManager +import android.os.PowerManager import android.provider.Settings import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -39,6 +40,9 @@ val Context.usbManager: UsbManager val Context.bluetoothManager: BluetoothManager get() = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager +val Context.powerManager: PowerManager + get() = getSystemService(Context.POWER_SERVICE) as PowerManager + // Permissions fun Context.requiresPermission(permission: String): Boolean = diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 167ea4f32..e96a95ed2 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -44,6 +44,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import to.bitkit.appwidget.AppWidgetRefreshWorker import to.bitkit.env.Env import to.bitkit.models.NodeLifecycleState import to.bitkit.models.Toast @@ -257,6 +258,7 @@ fun ContentView( appViewModel.consumePaymentReceivedInBackground() + AppWidgetRefreshWorker.enqueueCatchUp(context) currencyViewModel.triggerRefresh() blocktankViewModel.refreshOrders() appViewModel.refreshPublicPaykitEndpoints() diff --git a/changelog.d/next/os-widget-idle.fixed.md b/changelog.d/next/os-widget-idle.fixed.md new file mode 100644 index 000000000..9130b4cf2 --- /dev/null +++ b/changelog.d/next/os-widget-idle.fixed.md @@ -0,0 +1 @@ +Android home-screen widgets now refresh shortly after unlocking the device so stale data catches up after idle periods.