Lesson 32 of 83 advanced

WorkManager, Background Tasks, Foreground Services & Alarm Limitations

Reliable background execution in the age of battery optimization

Open interactive version (quiz + challenge)

Real-world analogy

WorkManager is like a reliable postal service — you hand it a package (task) with delivery requirements (constraints: needs WiFi, needs charging) and it guarantees delivery even if you go to sleep, restart your phone, or the postman's car breaks down. Foreground Services are like a courier who rings your doorbell and stays visible until they finish. AlarmManager after Android 12 is like the post office that now charges extra for guaranteed exact-time delivery.

What is it?

WorkManager is Jetpack's library for deferrable, guaranteed background work. It wraps JobScheduler, AlarmManager, and BroadcastReceiver into a single API that survives app kills and device restarts. Foreground Services handle user-visible ongoing tasks. AlarmManager handles exact-time alarms but faces increasing OS restrictions from Android 12 onward.

Real-world relevance

In BRAC's field ops app, WorkManager handled offline data sync: a PeriodicWorkRequest every 15 minutes with CONNECTED constraint, chained with an upload task that had exponential backoff. When FCM delivered a data message (new assignment), it enqueued a OneTimeWorkRequest immediately. The foreground service ran during large file uploads so the system wouldn't kill the transfer mid-stream.

Key points

Code example

// 1. CoroutineWorker — preferred for Kotlin
class SyncWorker(context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return try {
            val repo = ServiceLocator.getRepository(applicationContext)
            repo.syncFromServer()
            Result.success()
        } catch (e: IOException) {
            if (runAttemptCount < 3) Result.retry()
            else Result.failure(workDataOf("error" to e.message))
        }
    }
}

// 2. Enqueue with constraints and chaining
fun scheduleSyncWork(context: Context) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .setRequiresBatteryNotLow(true)
        .build()

    val periodicSync = PeriodicWorkRequestBuilder<SyncWorker>(
        15, TimeUnit.MINUTES
    )
        .setConstraints(constraints)
        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
        .addTag("sync")
        .build()

    // KEEP_EXISTING prevents duplicate periodic work
    WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        "periodic_sync",
        ExistingPeriodicWorkPolicy.KEEP,
        periodicSync
    )
}

// 3. Foreground service (Android 8+ requirement)
class UploadService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("Uploading data...")
            .setSmallIcon(R.drawable.ic_upload)
            .setOngoing(true)
            .build()
        // Must call within 5 seconds — or system kills the service
        startForeground(NOTIF_ID, notification)
        CoroutineScope(Dispatchers.IO).launch {
            runUploadPipeline()
            stopSelf()
        }
        return START_NOT_STICKY
    }
    override fun onBind(intent: Intent?) = null
}

// 4. AlarmManager exact alarm (Android 12+)
fun scheduleExactAlarm(context: Context, triggerAtMillis: Long) {
    val alarmManager = context.getSystemService(AlarmManager::class.java)
    if (alarmManager.canScheduleExactAlarms()) {
        val intent = PendingIntent.getBroadcast(
            context, 0,
            Intent(context, AlarmReceiver::class.java),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        alarmManager.setExactAndAllowWhileIdle(
            AlarmManager.RTC_WAKEUP, triggerAtMillis, intent
        )
    } else {
        // Request permission — launches system settings
        val permIntent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
        context.startActivity(permIntent)
    }
}

Line-by-line walkthrough

  1. 1. CoroutineWorker suspends on Dispatchers.IO automatically — no need to manage threads manually compared to the base Worker class.
  2. 2. runAttemptCount tracks how many times WorkManager has retried this worker instance — use it to limit retries before declaring failure.
  3. 3. Result.retry() signals WorkManager to reschedule with the configured backoff policy (EXPONENTIAL doubles the delay each attempt).
  4. 4. PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) sets the repeat interval — the system may delay beyond 15 min for battery optimization.
  5. 5. enqueueUniquePeriodicWork with KEEP policy ensures only one periodic sync worker runs at a time — calling it again does nothing if one already exists.
  6. 6. startForeground(NOTIF_ID, notification) must be called within 5 seconds of onStartCommand() to keep the service alive on Android 8+.
  7. 7. START_NOT_STICKY means if the system kills the service, it won't be restarted unless there are pending intents — correct for one-shot upload services.
  8. 8. stopSelf() in the coroutine's completion block ensures the service stops after the upload finishes, removing the persistent notification.
  9. 9. alarmManager.canScheduleExactAlarms() is required before calling setExact() on Android 12+ to avoid SecurityException.
  10. 10. PendingIntent.FLAG_IMMUTABLE is required for PendingIntents on Android 12+ as a security requirement.
  11. 11. setExactAndAllowWhileIdle() fires the alarm even during Doze mode — use only for user-facing time-critical alarms.
  12. 12. WorkManager.enqueue() with addTag() allows you to cancel or observe a group of related tasks by tag using cancelAllWorkByTag().

Spot the bug

class DataSyncWorker(ctx: Context, params: WorkerParameters) :
    CoroutineWorker(ctx, params) {

    override suspend fun doWork(): Result {
        return try {
            syncData()
            Result.success()
        } catch (e: Exception) {
            Result.retry()  // Bug 1
        }
    }
}

class BackgroundSyncService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        CoroutineScope(Dispatchers.IO).launch {
            longRunningSync()  // Bug 2
            stopSelf()
        }
        return START_STICKY
    }
    override fun onBind(intent: Intent?) = null
}

fun scheduleSync(context: Context) {
    val work = PeriodicWorkRequestBuilder<DataSyncWorker>(
        5, TimeUnit.MINUTES  // Bug 3
    ).build()
    WorkManager.getInstance(context).enqueue(work)  // Bug 4
}

fun scheduleReminder(context: Context, timeMs: Long) {
    val alarmManager = context.getSystemService(AlarmManager::class.java)
    val pi = PendingIntent.getBroadcast(context, 0,
        Intent(context, ReminderReceiver::class.java), 0)  // Bug 5
    alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeMs, pi)
}
Need a hint?
Check retry logic for non-retryable errors, missing startForeground(), minimum periodic interval, unique work enqueueing, and PendingIntent flags on Android 12+.
Show answer
Bug 1: Catching all Exception and always returning Result.retry() means HTTP 4xx errors (bad request, unauthorized) will retry indefinitely until WorkManager gives up — wasting battery and generating server load. Fix: catch IOException separately for Result.retry(), catch HttpException and check statusCode — for 4xx return Result.failure(), for 5xx return Result.retry() if runAttemptCount < 3. Bug 2: BackgroundSyncService never calls startForeground() — on Android 8+, a service that does not call startForeground() within 5 seconds of onStartCommand() is killed by the system. This will cause a crash or silent failure. Fix: call startForeground(NOTIF_ID, buildNotification()) immediately at the top of onStartCommand(), before launching the coroutine. Bug 3: PeriodicWorkRequestBuilder with 5 minutes is below the 15-minute minimum — WorkManager will throw an IllegalArgumentException at runtime: 'Interval duration lesser than minimum allowed duration'. Fix: use 15, TimeUnit.MINUTES as the minimum. Bug 4: WorkManager.getInstance(context).enqueue(work) for periodic work does not deduplicate — every call to scheduleSync() enqueues another periodic worker. Over time, dozens of duplicate sync workers run simultaneously. Fix: use enqueueUniquePeriodicWork("data_sync", ExistingPeriodicWorkPolicy.KEEP, work) to guarantee only one instance runs. Bug 5: PendingIntent flags value 0 is missing FLAG_IMMUTABLE — on Android 12+ this causes an IllegalArgumentException: 'Targeting S+ requires FLAG_IMMUTABLE or FLAG_MUTABLE'. Fix: use PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE.

Explain like I'm 5

Imagine you have homework to do but you want to do it later. WorkManager is like a very reliable assistant who promises to do your homework even if you fall asleep, your computer restarts, or you forget about it entirely. A Foreground Service is like doing homework with a sticky note on your forehead saying 'I'm busy' so people don't interrupt you. And AlarmManager is like setting an alarm clock — but from Android 12, the alarm clock company says you need special permission to set alarms at exactly the right second.

Fun fact

WorkManager uses a Room database internally to persist enqueued work across process death and device reboots. Every WorkRequest you enqueue is written to a SQLite table — that's why WorkManager can survive a force-stop and still execute your task after the device restarts.

Hands-on challenge

Implement a production-ready sync architecture: (1) A CoroutineWorker that reads pending items from Room, uploads via Retrofit with retry logic (Result.retry() on IOException, Result.failure() on HttpException 4xx), and returns output data with the count of synced items; (2) A unique periodic work enqueue that survives app restarts and reuses an existing worker if already enqueued (KEEP policy); (3) A function triggered by an FCM data message that enqueues a one-time immediate sync with CONNECTED constraint; (4) A foreground service for large file upload that updates the notification progress percentage and calls stopSelf() on completion or failure.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery