Skip to content

feat(android): Opt-in expedited WorkManager execution for background callbacks - #435

Open
galacticgibbon wants to merge 2 commits into
ABausG:mainfrom
galacticgibbon:feat/expedited-background-work
Open

feat(android): Opt-in expedited WorkManager execution for background callbacks#435
galacticgibbon wants to merge 2 commits into
ABausG:mainfrom
galacticgibbon:feat/expedited-background-work

Conversation

@galacticgibbon

Copy link
Copy Markdown

Description

Follow-up to #377, where you asked for the Android changes to be split out and motivated separately — this PR contains only that change.

Problem

HomeWidgetBackgroundWorker.enqueueWork() schedules background callbacks as regular OneTimeWorkRequests. WorkManager treats these as deferrable: under Doze/battery-saver/App Standby buckets, execution of a widget interaction (e.g. tapping a checkbox on a home-screen widget) can be delayed by seconds or longer. For interactive widgets the user is watching the widget while the tap is processed, so the delay reads as the widget being broken.

Change

Adds an opt-in EXTRA_IS_EXPEDITED boolean on HomeWidgetBackgroundIntent. When set, the work request is built with setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST), so taps run promptly while the app has expedited quota and degrade gracefully to normal scheduling when quota is exhausted. Default is false — existing behavior is unchanged unless a caller opts in.

We've been running this in production in a task-journal app (Bullet) for widget check-off interactions since late 2025.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 62193881-e1ac-4365-aa90-7e3991448af4

📥 Commits

Reviewing files that changed from the base of the PR and between 808dafe and 873482d.

📒 Files selected for processing (2)
  • packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt
  • packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt
  • packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt

Walkthrough

Changes

The Android background-work flow now carries an optional expedited flag from broadcast creation through the receiver to WorkManager, which configures expedited execution with a non-expedited quota fallback.

Expedited background work

Layer / File(s) Summary
Intent flag propagation
packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt, packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt
Adds the expedited intent extra, writes it when creating broadcasts, and passes it from the receiver to the worker.
Worker expedited configuration
packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt
Extends enqueueWork with an optional expedited flag and configures WorkManager with RUN_AS_NON_EXPEDITED_WORK_REQUEST when enabled.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BroadcastCreator
  participant HomeWidgetBackgroundReceiver
  participant HomeWidgetBackgroundWorker
  participant WorkManager
  BroadcastCreator->>HomeWidgetBackgroundReceiver: Send broadcast with EXTRA_IS_EXPEDITED
  HomeWidgetBackgroundReceiver->>HomeWidgetBackgroundWorker: enqueueWork(expedited)
  HomeWidgetBackgroundWorker->>WorkManager: Enqueue expedited or regular work
  WorkManager-->>HomeWidgetBackgroundWorker: Fall back when expedited quota is unavailable
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change and uses a conventional commit prefix.
Description check ✅ Passed The description covers the existing behavior, the change, and the motivation, which matches the template well.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@galacticgibbon galacticgibbon changed the title feat(android): opt-in expedited WorkManager execution for background callbacks feat(android): Opt-in expedited WorkManager execution for background callbacks Jul 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt`:
- Around line 121-123: Add foreground-info support to HomeWidgetBackgroundWorker
by implementing getForegroundInfo() with a valid notification and foreground
service type, and ensure the expedited enqueue path using setExpedited remains
compatible with the minSdkVersion 16 worker. Reuse the worker’s existing
context/configuration and notification setup where available.

In
`@packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt`:
- Around line 58-62: Preserve the existing JVM entry points by adding explicit
old-arity overloads: in
packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt:58-62,
add getBroadcast(Context, Uri) delegating to the defaulted implementation; in
packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt:115-125,
add enqueueWork(Context, Intent) delegating to the new implementation. Keep the
current default-parameter APIs unchanged.
- Around line 58-62: Update getBroadcast so the expedited value contributes to
the PendingIntent identity, using a stable distinct request code or another
identity field for expedited versus non-expedited scheduling. Preserve
EXTRA_IS_EXPEDITED and the existing intent behavior, while ensuring
FLAG_UPDATE_CURRENT cannot reuse one mode’s PendingIntent for the other.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ac30c74e-7979-4ef5-bd75-4ff61e21cd91

📥 Commits

Reviewing files that changed from the base of the PR and between 0b42c89 and 808dafe.

📒 Files selected for processing (3)
  • packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt
  • packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt
  • packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt

Comment on lines +121 to +123
if (expedited) {
workRequestBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file and relevant symbols =="
git ls-files | rg 'HomeWidgetBackgroundWorker\.kt$|manifest|build\.gradle|gradle' || true

echo
echo "== file outline =="
ast-grep outline packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt --view expanded || true

echo
echo "== relevant lines =="
nl -ba packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt | sed -n '1,220p'

echo
echo "== manifest minSdk variants =="
fd -a 'AndroidManifest.xml|build.gradle|build.gradle.kts' packages/home_widget/android -t f | sort | while read -r f; do
  echo "--- $f"
  rg -n 'minSdk|targetSdk|compileSdk|<uses-sdk|manifest' "$f" || true
done

echo
echo "== WorkManager dependency versions =="
rg -n 'work-manager|androidx.work|workRuntime|workRuntimeKtx|workRuntime' packages/home_widget -S || true

echo
echo "== getForegroundInfo usages =="
rg -n 'fun getForegroundInfo|getForegroundInfo\(|setExpedited|OutOfQuotaPolicy|foreground' packages/home_widget/android/src/main/kotlin packages/home_widget/android -S || true

Repository: ABausG/home_widget

Length of output: 2372


🌐 Web query:

Android WorkManager CoroutineWorker getForegroundInfo expedited without foreground notification older Android crash documentation

💡 Result:

When using WorkManager's expedited jobs, you must implement the getForegroundInfo method in your CoroutineWorker [1][2]. Failure to do so will result in an IllegalStateException and a runtime crash when the WorkRequest is executed on Android versions prior to Android 12 (API level 31), where WorkManager falls back to using a foreground service to maintain compatibility [1][3][4]. The crash occurs because the WorkManager framework calls getForegroundInfo to obtain the necessary Notification to start the foreground service, and the default implementation in CoroutineWorker throws an exception [3][2][5]. Key Technical Requirements and Best Practices: 1. Mandatory Implementation: You must override getForegroundInfo in your CoroutineWorker if you use setExpedited [1][2]. Even if you do not explicitly start a foreground service yourself, WorkManager requires this method to manage the fallback foreground service automatically on older devices [1][3]. 2. Error Handling: Always wrap calls to setForeground in a try-catch block [1]. This is critical because foreground service execution is subject to strict OS background-start restrictions (especially on Android 12 and higher), which can lead to ForegroundServiceStartNotAllowedException [1][4][6]. 3. Foreground Service Types: If your app targets Android 14 (API level 34) or higher, you must specify the appropriate foreground service type in your manifest and when calling setForeground [7]. 4. Versioning: Update to the latest version of WorkManager to benefit from improved stability, as specific fixes for foreground service-related crashes are regularly released (e.g., version 2.10.5 addressed specific overlapping foreground worker issues) [8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== HomeWidgetBackgroundWorker source =="
python3 - <<'PY'
from pathlib import Path
p = Path("packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt")
for i, line in enumerate(p.read_text().splitlines(), 1):
    print(f"{i:4d}: {line}")
PY

echo
echo "== Android build/manifest sdk info =="
for f in packages/home_widget/android/build.gradle packages/home_widget/android/src/main/AndroidManifest.xml packages/home_widget/example/android/app/build.gradle packages/home_widget/example/android/src/main/AndroidManifest.xml; do
  if [ -f "$f" ]; then
    echo "--- $f"
    while IFS= read -r line; do
      case "$line" in
        *minSdk*|*targetSdk*|*compileSdk*|*applicationId*|*<uses-sdk*|*<service*|*foregroundServiceType*)
          printf '%s\n' "$line" ;;
      esac
    done < "$f"
  fi
done

echo
echo "== searches for getForegroundInfo, setForeground, expedited, OutOfQuotaPolicy, notification =="
rg -n 'getForegroundInfo|setForeground|setExpedited|OutOfQuotaPolicy|foregroundServiceType|<service|Notification|ForegroundService' packages/home_widget/android packages/home_widget/example/android -S || true

echo
echo "== Kotlin source structural search for CoroutineWorker overrides =="
python3 - <<'PY'
from pathlib import Path
import re
p=Path("packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt")
s=p.read_text()
print("extends:", bool(re.search(r'CoroutineWorker', s)))
for name in ['doWork', 'getForegroundInfo', 'onStopped', 'onStart', 'setForeground']:
    print(f"{name}: {bool(re.search(rf'override\s+(?:suspend\s+)?fun\s+{name}\\s*\\(', s))}")
PY

Repository: ABausG/home_widget

Length of output: 5312


🏁 Script executed:

#!/bin/bash
set -u

echo "== SDK and manifest configuration files =="
for f in packages/home_widget/android/build.gradle packages/home_widget/android/AndroidManifest.xml packages/home_widget/example/android/app/build.gradle packages/home_widget/example/android/AndroidManifest.xml; do
  if [ -f "$f" ]; then
    echo "--- $f"
    grep -nE 'minSdk|targetSdk|compileSdk|applicationId|<uses-sdk|<service|foregroundServiceType|service|permission' "$f" || true
  fi
done 2>/dev/null || true

echo
echo "== Android directory manifests/build files =="
find packages/home_widget packages/home_widget/example -maxdepth 4 -type f \( -name 'AndroidManifest.xml' -o -name 'build.gradle' -o -name 'build.gradle.kts' \) | sort | while IFS= read -r f; do
  has_match=0
  while IFS= read -r line; do
    case "$line" in
      *minSdk*|*targetSdk*|*compileSdk*|*applicationId*|*<uses-sdk*|*foregroundServiceType*|*<service*) has_match=1; printf '%s\n' "$line" ;;
    esac
  done < "$f"
  [ "$has_match" -eq 0 ] || true
done 2>/dev/null || true

echo
echo "== worker override check =="
python3 - <<'PY'
from pathlib import Path
import re
p=Path("packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt")
s=p.read_text()
print("extends CoroutineWorker:", bool(re.search(r'CoroutineWorker', s)))
for name in ['doWork', 'getForegroundInfo', 'onStopped', 'onStart', 'setForeground']:
    print(f"{name}: {bool(re.search(rf'override\s+(?:suspend\s+)?fun\s+{name}\s*\(', s))}")

start=s.find("fun enqueueWork")
print("\nexpedited section:")
print("\n".join(f"{i:4d}: {line}" for i,line in enumerate(s.splitlines(), 1) if start <= (i-1) <= start+20))
PY

Repository: ABausG/home_widget

Length of output: 816


Add foreground-info support before enabling expedited workers.

HomeWidgetBackgroundWorker is a CoroutineWorker with minSdkVersion 16, but enqueueWork(..., expedited = true) only calls setExpedited(...) and does not override getForegroundInfo(). Coroutines workers in WorkManager need a valid foreground notification path on pre-API-31 devices when used as expedited work, unless the worker is refactored/scheduled outside of Android.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundWorker.kt`
around lines 121 - 123, Add foreground-info support to
HomeWidgetBackgroundWorker by implementing getForegroundInfo() with a valid
notification and foreground service type, and ensure the expedited enqueue path
using setExpedited remains compatible with the minSdkVersion 16 worker. Reuse
the worker’s existing context/configuration and notification setup where
available.

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.

1 participant