Skip to content

fix: refresh widgets after idle#978

Open
ovitrif wants to merge 1 commit into
masterfrom
codex/github-mention-bug]-android-os-widgets-show-stale-data-af
Open

fix: refresh widgets after idle#978
ovitrif wants to merge 1 commit into
masterfrom
codex/github-mention-bug]-android-os-widgets-show-stale-data-af

Conversation

@ovitrif
Copy link
Copy Markdown
Collaborator

@ovitrif ovitrif commented May 31, 2026

Fixes #975

Motivation

  • Android home-screen widgets could show stale data after the device stayed idle because Android defers periodic WorkManager refreshes during idle/Doze and the app did not enqueue a catch-up refresh on unlock or idle exit.

Description

  • Add AppWidgetRefreshReceiver that enqueues a one-shot widget catch-up when Intent.ACTION_USER_PRESENT or PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED idle exit occurs.
  • Add enqueueCatchUp one-time WorkManager path to AppWidgetRefreshWorker and hasActiveWidgets/cancel logic so catch-up runs only when OS widgets are installed and catch-up work is cancelled when no widgets remain.
  • Register the receiver at app startup and trigger the same catch-up when the app returns to foreground.
  • Add Context.powerManager and changelog fragment changelog.d/next/978.fixed.md.

Preview

N/A

QA Notes

Manual Tests

  • 1. Android launcher → add any Bitkit home-screen widget, not only an in-app widget → open Bitkit once → press Home: the widget is visible and the app process has registered the receiver.
  • 2. regression: ADB → force the device into idle/Doze with the commands below → exit idle and unlock: logcat shows Enqueued widget refresh for 'device_idle_exit' or Enqueued widget refresh for 'user_present', then Refreshing data for widget types: ...; the home-screen widget updates without waiting for the 15-minute periodic WorkManager window.
  • 3. No-lock smoke test → apply the temporary debug patch below → rebuild/install dev debug → send the custom broadcast: logcat shows Enqueued widget refresh for 'debug_catch_up', then Refreshing data for widget types: ....

The original stale-widget condition is: an actual Android home-screen widget exists, the app has previously started, Android enters idle/Doze and defers the 15-minute periodic WorkManager refresh, then the user unlocks/exits idle before the periodic job is allowed to run. On master, the same forced-idle/unidle flow has no widget catch-up receiver path, so the widget can stay on cached data until the next periodic refresh is eventually allowed. This PR should enqueue immediate one-time catch-up work on idle exit, unlock, or app foreground.

Force the idle condition without waiting 15+ minutes:

adb logcat -c
adb shell am start -n to.bitkit.dev/to.bitkit.ui.MainActivity
adb shell input keyevent KEYCODE_HOME
adb shell dumpsys battery unplug
adb shell dumpsys deviceidle enable
adb shell dumpsys deviceidle force-idle
adb shell dumpsys deviceidle get deep
adb shell dumpsys deviceidle unforce
adb shell input keyevent KEYCODE_WAKEUP
adb shell input keyevent 82
adb logcat -d -s APP | grep -E "AppWidgetRefreshReceiver|AppWidgetRefreshWorker|Enqueued widget refresh|Refreshing data for widget types"
adb shell dumpsys battery reset

Optional temporary patch for a no-lock, no-wait adb trigger. Apply in one command, rebuild with ./gradlew installDevDebug, open Bitkit once, then run adb shell am broadcast -a to.bitkit.dev.DEBUG_WIDGET_CATCH_UP -p to.bitkit.dev.

git apply <<'PATCH'
diff --git a/app/src/main/java/to/bitkit/App.kt b/app/src/main/java/to/bitkit/App.kt
index d7a412346..6d22fb89b 100644
--- a/app/src/main/java/to/bitkit/App.kt
+++ b/app/src/main/java/to/bitkit/App.kt
@@ -47,10 +47,16 @@ internal open class App : Application(), Configuration.Provider {
             addAction(Intent.ACTION_USER_PRESENT)
             addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
+            if (BuildConfig.DEBUG) addAction("to.bitkit.dev.DEBUG_WIDGET_CATCH_UP")
+        }
+        val receiverFlags = if (BuildConfig.DEBUG) {
+            ContextCompat.RECEIVER_EXPORTED
+        } else {
+            ContextCompat.RECEIVER_NOT_EXPORTED
         }
         ContextCompat.registerReceiver(
             this,
             AppWidgetRefreshReceiver(),
             filter,
-            ContextCompat.RECEIVER_NOT_EXPORTED,
+            receiverFlags,
         )
     }
 
diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt
index 486406d79..78e4cda0b 100644
--- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt
+++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt
@@ -11,6 +11,7 @@ class AppWidgetRefreshReceiver : BroadcastReceiver() {
     override fun onReceive(context: Context, intent: Intent) {
         when (intent.action) {
             Intent.ACTION_USER_PRESENT -> enqueueCatchUp(context, "user_present")
+            "to.bitkit.dev.DEBUG_WIDGET_CATCH_UP" -> enqueueCatchUp(context, "debug_catch_up")
             PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED -> {
                 if (!context.powerManager.isDeviceIdleMode) enqueueCatchUp(context, "device_idle_exit")
             }
PATCH

Clean up the temporary patch after testing:

git restore app/src/main/java/to/bitkit/App.kt app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshReceiver.kt

Automated Checks

  • git diff --check was run and produced no whitespace errors.
  • The temporary debug QA patch in these notes was validated with git apply --check.
  • ./gradlew compileDevDebugKotlin passed locally.
  • ./gradlew testDevDebugUnitTest passed locally.
  • ./gradlew detekt passed locally.

@ovitrif ovitrif self-assigned this May 31, 2026
@ovitrif ovitrif force-pushed the codex/github-mention-bug]-android-os-widgets-show-stale-data-af branch from efea229 to 295079f Compare May 31, 2026 19:18
@ovitrif ovitrif removed the codex label May 31, 2026
@ovitrif ovitrif added this to the 2.3.0 milestone May 31, 2026
@ovitrif ovitrif requested a review from piotr-iohk May 31, 2026 19:50
@ovitrif ovitrif marked this pull request as ready for review May 31, 2026 19:51
@ovitrif ovitrif enabled auto-merge May 31, 2026 19:51
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 295079f67e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +50 to +54
ContextCompat.registerReceiver(
this,
AppWidgetRefreshReceiver(),
filter,
ContextCompat.RECEIVER_NOT_EXPORTED,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid relying on a runtime-only idle receiver

When the device has been idle long enough for Android to reclaim the Bitkit process, this runtime receiver is no longer registered, so unlock or idle-exit broadcasts will not start the app and enqueueCatchUp() is never called. That leaves the stale-widget scenario unchanged until the next periodic WorkManager run; the catch-up path needs to be tied to a trigger that still runs when the app process is not alive, or otherwise account for that case.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Android OS widgets show stale data after idle

1 participant