android-background-work
Skill2 starsChanged 40 days agoMITView on GitHub
Use when work must run outside the visible UI — sync, uploads, periodic jobs, long-running tasks. Covers WorkManager constraints and expedited work, foreground service types, exact alarms policy, Doze/App Standby, and testing background work deterministically.
In this repository31 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- android-accessibilitySkill
- android-architectureSkill
- android-data-persistenceSkill
- android-device-testingSkill
- android-e2e-verificationSkill
- android-ui-engineeringSkill
- api-and-interface-designSkill
- ci-cd-and-automationSkill
- code-review-and-qualitySkill
- code-simplificationSkill
- constraint-driven-developmentSkill
- context-engineeringSkill
- debugging-and-error-recoverySkill
- deprecation-and-migrationSkill
- documentation-and-adrsSkill
- doubt-driven-developmentSkill
- git-workflow-and-versioningSkill
- idea-refineSkill
- incremental-implementationSkill
- interview-meSkill
- observability-and-instrumentationSkill
- performance-optimizationSkill
- planning-and-task-breakdownSkill
- security-and-hardeningSkill
- shipping-and-launchSkill
- source-driven-developmentSkill
- spec-driven-developmentSkill
- test-driven-developmentSkill
- using-agent-skillsSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 95
Never: ask for REQUEST_IGNORE_BATTERY_OPTIMIZATIONS as a convenience — it's a Play-policy-restricted permission for genuinely exempt app …
- Very specific: line 152
No test using WorkManagerTestInitHelper for critical background flows
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Android Background Work
Overview
Background execution on Android is a negotiation with the OS, not a right. Doze, App Standby buckets, foreground-service type enforcement, and exact-alarm policy all exist to kill battery-hungry work — code that ignores them runs fine on the developer's charging Pixel and silently never runs in the field. This skill picks the correct primitive for each job and makes background work observable and testable.
When to Use
- Syncing data, uploading files, or refreshing content off-screen
- Scheduling periodic or deferred jobs
- Long-running user-initiated tasks (export, media processing)
- Anything currently "solved" with
GlobalScope.launchor a rawService
Skip when: The work only matters while the screen is visible — use viewModelScope/lifecycleScope and let it cancel with the UI.
Core Process
Step 1: Pick the Right Primitive
- Decision table — default to WorkManager:
| Need | Primitive |
| Deferrable, guaranteed (sync, upload) | WorkManager OneTimeWorkRequest |
| Periodic (≥15 min interval) | WorkManager PeriodicWorkRequest |
| User-visible ongoing task (playback, navigation, recording) | Foreground service with a declared type |
| User-facing alarm/reminder at an exact time | AlarmManager.setExactAndAllowWhileIdle + SCHEDULE_EXACT_ALARM/USE_EXACT_ALARM |
| Work only while UI is alive | Coroutine scope tied to the lifecycle — not background work |
Choosing a foreground service for deferrable work, or exact alarms for "roughly hourly", fails Play review and drains battery.
Step 2: WorkManager Done Right
- Constraints and backoff instead of retry loops:
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
// Unique work: a second "sync now" tap must not enqueue a second sync
WorkManager.getInstance(context).enqueueUniqueWork(
"sync", ExistingWorkPolicy.KEEP, syncRequest
)- Worker rules:
CoroutineWorker+Result.retry()on transient failures;Result.failure()only for permanent ones- Expedited work (
setExpedited) for short user-initiated jobs that should dodge Doze — withgetForegroundInfo()implemented for the pre-API-31 fallback - Workers are re-run after process death: make them idempotent (upsert, not insert)
- Inject dependencies via Hilt's
@HiltWorker+HiltWorkerFactory, not service locators
Step 3: Foreground Services Under Type Enforcement
- Since API 34 every foreground service declares a type — in the manifest and at start, with the type's prerequisite permission:
<service
android:name=".playback.PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />ServiceCompat.startForeground(
this, NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
)Starting with a mismatched or undeclared type throws; dataSync type is being phased out — use WorkManager for transfers. The notification requires POST_NOTIFICATIONS runtime permission on API 33+.
Step 4: Respect Doze and Standby
- Design for the powered-off case:
- Doze defers network, jobs, and standard alarms into maintenance windows — WorkManager already cooperates; hand-rolled
Handler.postDelayedloops do not - App Standby buckets throttle how often deferred work runs — heavy users get more budget than dormant installs; never promise sync freshness the bucket can't deliver
setAndAllowWhileIdleis rate-limited (~once/9 min per app) — it is for user-facing alarms, not sync- Never ask for
REQUEST_IGNORE_BATTERY_OPTIMIZATIONSas a convenience — it's a Play-policy-restricted permission for genuinely exempt app categories
- Doze defers network, jobs, and standard alarms into maintenance windows — WorkManager already cooperates; hand-rolled
Step 5: Test It Deterministically
- WorkManager has a real test API — use it instead of sleeping:
@Before
fun setup() {
val config = Configuration.Builder()
.setExecutor(SynchronousExecutor())
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
@Test
fun syncWorker_runs_whenConstraintsMet() {
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(networkConstraints)
.build()
WorkManager.getInstance(context).enqueue(request).result.get()
// Simulate the OS satisfying constraints
WorkManagerTestInitHelper.getTestDriver(context)!!
.setAllConstraintsMet(request.id)
val info = WorkManager.getInstance(context).getWorkInfoById(request.id).get()
assertEquals(WorkInfo.State.SUCCEEDED, info.state)
}- On-device verification (see
android-device-testing):
adb shell dumpsys jobscheduler | grep <package> # scheduled jobs
adb shell am send-trim-memory <package> RUNNING_CRITICAL
adb shell cmd deviceidle force-idle # simulate DozeCommon Rationalizations
| Shortcut | Why It Fails |
| "A foreground service is simpler than WorkManager" | Type enforcement, notification permission, and Play review make it the complicated option — and it dies anyway when the user swipes the app. |
| "GlobalScope.launch works fine" | Until process death. No retry, no constraints, no guarantee — it works on the dev device and drops work in the field. |
| "I need exact timing for sync" | You need eventual sync. Exact alarms are policy-gated for user-facing alarms; sync at :00 sharp is battery abuse with no user benefit. |
| "I'll test background work manually" | Doze and standby buckets don't manifest in a 2-minute manual test. WorkManagerTestInitHelper and force-idle reproduce what the field does. |
| "Battery-optimization exemption fixes it" | It fixes your test device and fails Play review. Design for Doze instead of opting out of it. |
Red Flags
- Raw
ServiceorGlobalScope.launchdoing deferrable work - Foreground service without a manifest
foregroundServiceType dataSyncforeground service where WorkManager belongs- Periodic work under 15 minutes "scheduled" with self-rearming alarms
- Non-idempotent workers (duplicate rows after a retry)
enqueueinstead ofenqueueUniqueWorkfor user-triggerable jobs- No test using
WorkManagerTestInitHelperfor critical background flows
Verification
- Each background job maps to the decision table's primitive (state which and why)
- WorkRequests have constraints and backoff; user-triggerable work is unique work
- Workers are idempotent — a forced re-run produces no duplicates
- Foreground services declare types + prerequisite permissions; start succeeds on API 34+
POST_NOTIFICATIONSflow handled for service notifications (API 33+)- Tests pass with
WorkManagerTestInitHelper(./gradlew testoutput) - Behavior verified under
cmd deviceidle force-idlefor sync-critical flows
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

