🤖 Unit 12 • Android Mobile Engineering

Background Work: Threads, Coroutines & WorkManager

Mastering asynchronous runtime concurrency on Android: Main thread UI loop mechanics, the 16.6ms frame budget, ANR barrier diagnostics, cooperative Kotlin Coroutine dispatching, enterprise persistent task scheduling via WorkManager, system Notifications (Android 13+ POST_NOTIFICATIONS), and real-time NetworkCallback connectivity monitoring.

⚡ Concurrency: Kotlin Coroutines & Dispatchers
🛠️ Persistence: WorkManager & PeriodicWork
🔔 Notifications: NotificationChannels & Progress
📡 Network: ConnectivityManager NetworkCallback
01

Main Thread vs Background Thread & ANR Architecture

🎯 The Single-Threaded UI Model & The Main Looper

When an Android application process launches, the Android Runtime (ART) forks from the Zygote template and instantiates a primary thread of execution called the Main Thread (colloquially termed the UI Thread). This thread runs a continuous message dispatch loop powered by three fundamental Android OS primitives:

  • MessageQueue: A low-level, priority-ordered FIFO buffer containing work items (Runnables and Messages) submitted for execution.
  • Looper: An infinite loop mechanism (Looper.loop()) executing on the Main Thread that extracts the next ready message from the MessageQueue and dispatches it to its corresponding Handler.
  • Handler: The IPC and intra-process messaging bridge used to enqueue messages and runnables onto a specific thread's Looper.

The Choreographer 16.6ms Frame Budget: Android's display subsystem refreshes at 60Hz (standard displays) or 120Hz (ProMotion/High Refresh Rate panels). To render smooth 60 FPS graphics, the UI thread must measure, lay out, draw, and commit every frame within a strict 16.6 milliseconds (8.3ms for 120Hz). If any blocking operation (disk I/O, heavy JSON parsing, database query, network socket fetch) stalls the Main Looper past this deadline, the device drops frames (causing visible UI jank and stutter).

🚨 Application Not Responding (ANR) Mechanics

If the Main Thread remains blocked and cannot process queued user input events or lifecycle callbacks, the Android Activity Manager Service (AMS) declares the application unresponsive and displays the fatal Application Not Responding (ANR) system dialog, prompting the user to force-close the process.

  • Input Dispatch Timeout: No response to a screen touch, key press, or gesture within 5 seconds.
  • BroadcastReceiver Timeout: A BroadcastReceiver fails to complete its onReceive() execution within 10 seconds (foreground broadcast) or 20 seconds (background broadcast).
  • Service Lifecycle Timeout: A Started Service fails to execute its lifecycle methods (onCreate(), onStartCommand()) within 20 seconds (foreground) or 200 seconds (background).

NetworkOnMainThreadException: Starting in Android 3.0 (Honeycomb), the OS unconditionally throws an uncatchable runtime crash if an app attempts to open a network socket or transmit HTTP traffic directly on the UI thread, enforcing architectural thread separation.

Dimension Main (UI) Thread Background Worker Thread
Primary Responsibility Inflating views, Jetpack Compose recomposition, handling user touches, window animations, lifecycle callbacks. Disk read/writes, Room SQLite queries, Retrofit network requests, cryptographic hashing, image decoding.
UI Modification Allowed? Yes (Direct access to View hierarchy and Compose Snapshot state). Strictly Prohibited (Calling view.setText() throws CalledFromWrongThreadException).
Permitted Work Duration Strictly < 16.6 milliseconds per frame cycle. Unbounded (seconds, minutes, or deferred indefinitely via WorkManager).
Consequence of Blocking UI jank, frame drops, and system ANR dialog crash. No visual impact on UI interactivity or frame render timing.
Underlying Engine Single OS thread running Looper.getMainLooper(). Java ThreadPoolExecutor or Kotlin Coroutine worker thread pool.
🛡️ Detecting Main-Thread Violations with StrictMode

Google provides the StrictMode diagnostic tool to catch accidental disk I/O and network requests executed on the UI thread during local debug builds:

DevAscentApplication.kt Kotlin
class DevAscentApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork() // Flashes red screen or logs violation
                    .penaltyLog()
                    .penaltyFlashScreen()
                    .build()
            )
        }
    }
}
Visual Architecture: Main Looper Event Loop vs Background Worker Pool
User Touch Event Input tap / Drag / Frame tick
➔
MessageQueue FIFO Work Item Buffer
➔
Main Looper UI Thread (<16.6ms budget)
⬇ Stalling operations must be offloaded
Dispatchers.IO Thread Pool (Max 64 threads)
⇄
Dispatchers.Default CPU Pool (# Cores)
⇄
WorkManager Service Persistent OS Scheduler
⬇ Results returned safely to UI
withContext(Dispatchers.Main) Mutate State / Recompose UI
02

Kotlin Coroutines: Suspend Functions, Builders & Structured Concurrency

✨ What are Coroutines? Cooperative Multitasking vs. OS Threads

A Coroutine is a lightweight, cooperative concurrency primitive that can suspend its execution without blocking the underlying operating system thread. While creating a standard Java thread incurs substantial overhead (~1MB of dedicated stack memory allocation and expensive kernel context switching), thousands of coroutines can execute concurrently across a small pool of shared threads with minimal memory footprint (~few kilobytes).

The suspend Modifier & Continuation Passing Style (CPS):
When a function is marked with the suspend keyword, the Kotlin compiler transforms its bytecode under the hood using Continuation Passing Style (CPS). The compiler appends a hidden parameter, continuation: Continuation<T>, to the function signature and converts the function body into a finite state machine:

CPS_Decompiled_Conceptual.kt Kotlin Under the Hood
// Developer writes:
suspend fun fetchCourse(id: String): Course

// Kotlin Compiler generates bytecode equivalent to:
fun fetchCourse(id: String, continuation: Continuation<Course>): Any? {
    // Returns Course data, or COROUTINE_SUSPENDED if asynchronous I/O is ongoing
}

When a coroutine hits a suspension point (e.g. waiting for a network socket or database query), it saves its local variable state in the Continuation object and yields the underlying thread back to the OS. The thread is immediately free to draw UI frames or process other tasks. Once the data arrives, the continuation's resumeWith() is invoked, restoring the state and continuing execution seamlessly.

🏗️ Coroutine Builders: launch vs async

Coroutine builders create and launch coroutines from non-suspending contexts:

  • launch { }: A "fire-and-forget" coroutine builder that returns a Job. It does not return any result value to the caller. Any unhandled exceptions thrown inside launch immediately propagate to the parent scope or uncaught exception handler.
  • async { }: A "compute-and-return" builder that returns a Deferred<T> (a lightweight, non-blocking future). To retrieve the result, call await(), which suspends until the deferred computation completes or rethrows any encapsulated exception.
ParallelDataFetch.kt Kotlin
// Executing two parallel network requests concurrently
suspend fun loadDashboardData(): Dashboard = coroutineScope {
    val userDeferred: Deferred<User> = async(Dispatchers.IO) { api.getUserProfile() }
    val coursesDeferred: Deferred<List<Course>> = async(Dispatchers.IO) { api.getCourses() }

    // Both network requests execute concurrently! Total time = max(req1, req2), NOT req1 + req2
    Dashboard(
        user = userDeferred.await(),
        courses = coursesDeferred.await()
    )
}
🧱 Structured Concurrency & Cooperative Cancellation

Modern Android development prohibits global asynchronous tasks (e.g. GlobalScope.launch) because they outlive their parent screens and cause severe memory leaks. Kotlin enforces Structured Concurrency:

  • Every coroutine must be launched inside a bounded CoroutineScope (such as viewModelScope or lifecycleScope).
  • Parent scopes track all child jobs. If a parent scope is cancelled (e.g. when the user presses Back and the ViewModel triggers onCleared()), all active child coroutines are cancelled automatically.
  • A parent scope does not complete until all its child coroutines have completed.
  • Cancellation is Cooperative: A coroutine cannot be killed forcefully from the outside. The running code must cooperate with cancellation by either calling built-in suspending functions (which check cancellation automatically) or manually verifying isActive or ensureActive() during tight CPU loops.
03

Coroutine Dispatchers & Main-Safe Context Switching

🧭 The Four Standard Dispatchers

A CoroutineDispatcher determines what thread or thread pool the coroutine uses for its execution. Kotlin Coroutines provides four optimized dispatchers:

Dispatcher Underlying Thread Pool Target Workload & Use Cases
Dispatchers.Main Android Main (UI) Looper thread. Lightweight UI interactions, invoking Compose state updates, updating LiveData/StateFlow, showing Snackbars/Dialogs.
Dispatchers.IO Elastic thread pool (scales up to 64 threads or the number of CPU cores). Blocking I/O operations: Retrofit HTTP requests, Room SQLite queries, file reads/writes, reading SharedPreferences/DataStore.
Dispatchers.Default Fixed thread pool sized strictly to the number of CPU cores (min 2). CPU-intensive computations: Large JSON serialization (parsing 10,000 items), complex sorting algorithms, image/matrix filtering.
Dispatchers.Unconfined Starts execution in the caller thread; resumes in whatever thread resumed the continuation. Non-deterministic; strictly used in specialized unit tests or internal library architectures. Avoid in UI application code.
🛡️ The "Main-Safe" Principle with withContext

In modern enterprise Android architecture, every suspend function must be main-safe. This means a caller should be able to invoke any repository or data-source suspend function directly from Dispatchers.Main without fearing an ANR or thread crash. The suspend function itself is responsible for switching dispatchers using withContext():

MainSafeRepository.kt Kotlin
class CourseRepository(
    private val api: DevAscentApiService,
    private val dao: CourseDao
) {
    // MAIN-SAFE: Safe to call directly from viewModelScope on Dispatchers.Main
    suspend fun syncAndSaveCourses(): List<Course> = withContext(Dispatchers.IO) {
        // 1. Network fetch on Dispatchers.IO
        val remoteCourses = api.getCourses()
        
        // 2. Disk persistence on Dispatchers.IO
        dao.insertAll(remoteCourses)
        
        return@withContext remoteCourses
    }
}
04

WorkManager: Guaranteed & Persistent Background Scheduling

⚙️ Why WorkManager? Background Limits & Doze Mode

Prior to Android 8.0 (API 26), developers used raw background Services and AlarmManager to run background tasks. However, rogue apps constantly drained device battery and consumed RAM when idle. Android introduced aggressive power management:

  • Background Execution Limits: Apps cannot execute long-running background services when the app is in the background or killed.
  • Doze Mode & App Standby: When a device is unplugged, stationary, and screen-off, the OS restricts CPU access, network connectivity, and sync adapters into maintenance windows.

Enter Jetpack WorkManager: WorkManager is Google's recommended library for persistent, deferrable, guaranteed background work. A task is persistent if it must execute even if the app process is killed or the physical device reboots.

Requirement Recommended Solution Example Use Case
Work must complete only while user is actively on screen. Kotlin Coroutines (viewModelScope / lifecycleScope) Fetching course details when opening a screen; searching a list in real time.
Work must complete even if app is closed; can be deferred until WiFi/charging. WorkManager (OneTimeWorkRequest / PeriodicWorkRequest) Uploading offline crash analytics; syncing local database with cloud; nightly backup.
User actively observes immediate progress; must not be killed by OS. Foreground Service with active Notification Active GPS turn-by-turn navigation; streaming audio playback; ongoing phone call.
Work must trigger at an exact, millisecond-precise time. AlarmManager (setExactAndAllowWhileIdle()) Morning wake-up alarm clock; calendar appointment reminder.
🧩 The Core Pillars of WorkManager Architecture

WorkManager operates through five primary architectural components:

  1. Worker / CoroutineWorker: The class containing the actual background execution logic. In modern Kotlin, we extend CoroutineWorker, which overrides the suspending function doWork(): Result.
  2. WorkRequest: Configures how and when the Worker should run:
    • OneTimeWorkRequest: Executes a task once (e.g. uploading a single user profile avatar).
    • PeriodicWorkRequest: Executes recurring tasks at fixed intervals. OS Constraint: The minimum repeating interval is hardcoded to 15 minutes (to protect battery life).
  3. Constraints: Triggers that gate execution. WorkManager holds the task in a waiting state until ALL declared conditions are satisfied:
    • setRequiredNetworkType(NetworkType.UNMETERED): Only run on unmetered Wi-Fi.
    • setRequiresCharging(true): Device must be plugged into AC/USB power.
    • setRequiresBatteryNotLow(true): Battery cannot be below warning threshold.
    • setRequiresDeviceIdle(true): User is not actively interacting with the device.
  4. WorkManager Engine (SQLite + OS Schedulers): Under the hood, WorkManager persists every work request, input data, and constraint into a local Room SQLite database. If the device reboots, WorkManager inspects the database and reschedules the tasks with JobScheduler (API 23+) or AlarmManager fallback automatically!
  5. WorkInfo & Observable State: Exposes reactive Flow<WorkInfo> or LiveData<WorkInfo> to track execution states: ENQUEUED, RUNNING, SUCCEEDED, FAILED, BLOCKED, and CANCELLED.
Visual Architecture: WorkManager Persistence & Execution Pipeline
WorkRequest Defined Worker + Constraints
➔
WorkManager.enqueue() Client dispatch call
➔
Internal Room DB Persisted to disk immediately
⬇ Survives App Process Death & Device Reboots
OS JobScheduler API 23+ System Daemon
⇄
System Constraints Check Unmetered Wi-Fi? Charging? Idle?
⬇ All conditions satisfied
CoroutineWorker.doWork() Background Execution
➔
Result.success() / retry() State updated in Room DB
05

Android Notification Architecture & API 33+ Permissions

🔔 Modern Notifications & POST_NOTIFICATIONS Permission

Notifications display messages outside an application's normal UI to inform users of background events, incoming sync updates, or ongoing downloads.

Android 13 (API 33) Runtime Permission: Historically, notification posting was granted automatically upon installation. Starting with Android 13 (Tiramisu), applications must declare and request the runtime permission android.permission.POST_NOTIFICATIONS. If denied, all notifications posted by the app are silently suppressed by the OS.

AndroidManifest.xml XML
<!-- Mandatory for Android 13+ (API 33) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
📢 Mandatory NotificationChannels (API 26+)

Starting in Android 8.0 (API 26), all notifications must be assigned to a NotificationChannel. Channels allow users to customize notification settings (e.g. disable marketing alerts while keeping critical download alerts enabled) directly in system settings without uninstalling the app.

  • IMPORTANCE_HIGH: Makes a sound and pops up on screen as a heads-up notification banner.
  • IMPORTANCE_DEFAULT: Makes a sound, appears in notification shade, but does not intrude with a heads-up banner.
  • IMPORTANCE_LOW: Silent; only visible when the user expands the notification shade. Ideal for background sync progress.
NotificationHelper.kt Kotlin
object NotificationHelper {
    const val CHANNEL_DOWNLOADS = "channel_downloads"

    fun createNotificationChannels(context: Context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_DOWNLOADS,
                "Course Material Downloads",
                NotificationManager.IMPORTANCE_LOW // Silent for progress updates
            ).apply {
                description = "Displays progress when downloading offline course assets."
                enableVibration(false)
            }

            val manager = context.getSystemService(NotificationManager::class.java)
            manager?.createNotificationChannel(channel)
        }
    }
}
06

Downloading Data in the Background & Network Availability

📡 Observing Network State with ConnectivityManager NetworkCallback

Legacy Android applications registered global CONNECTIVITY_ACTION broadcast receivers, which Google deprecated because thousands of idle apps woke up simultaneously whenever a device toggled Wi-Fi. The modern standard is to register a ConnectivityManager.NetworkCallback:

NetworkConnectivityObserver.kt Kotlin
class NetworkConnectivityObserver(context: Context) {
    private val connectivityManager = 
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    val isConnectedFlow: Flow<Boolean> = callbackFlow {
        val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                trySend(true)
            }

            override fun onLost(network: Network) {
                trySend(false)
            }
        }

        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()

        connectivityManager.registerNetworkCallback(request, callback)

        // Cleanup callback when Flow collection stops
        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
    }
}
📥 Streaming Large Downloads via CoroutineWorker with Progress Updates

When downloading large binary payloads (e.g. course video MP4s or lecture PDF zip files) in the background:

  • Never load the entire byte array into memory (which causes an OutOfMemoryError). Instead, stream the response body directly to a disk FileOutputStream.
  • Regularly inspect isStopped inside the byte loop so the worker immediately aborts if WorkManager cancels the task or the Wi-Fi connection drops.
  • Update a foreground NotificationCompat.Builder.setProgress(100, percent, false) to give the user real-time progress feedback.
07

Production Capstone: Enterprise Background Download Worker & Scheduler

Below is the production-grade implementation of DevAscentMaterialDownloadWorker, demonstrating a complete CoroutineWorker that validates network constraints, downloads course materials with live notification progress, and publishes a completion alert:

DevAscentMaterialDownloadWorker.kt Kotlin
package me.devascent.academy.workers

import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.concurrent.TimeUnit

class DevAscentMaterialDownloadWorker(
    private val appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    companion object {
        const val KEY_COURSE_ID = "key_course_id"
        const val KEY_DOWNLOAD_URL = "key_download_url"
        const val NOTIFICATION_ID = 2001
        const val CHANNEL_ID = "channel_downloads"

        // Factory method to build constrained OneTimeWorkRequest
        fun buildWorkRequest(courseId: String, downloadUrl: String): OneTimeWorkRequest {
            val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .setRequiresBatteryNotLow(true)
                .build()

            val inputData = workDataOf(
                KEY_COURSE_ID to courseId,
                KEY_DOWNLOAD_URL to downloadUrl
            )

            return OneTimeWorkRequestBuilder<DevAscentMaterialDownloadWorker>()
                .setConstraints(constraints)
                .setInputData(inputData)
                .setBackoffCriteria(
                    BackoffPolicy.EXPONENTIAL,
                    15, TimeUnit.SECONDS
                )
                .addTag("download_course_$courseId")
                .build()
        }
    }

    override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
        val courseId = inputData.getString(KEY_COURSE_ID) ?: return@withContext Result.failure()
        val downloadUrl = inputData.getString(KEY_DOWNLOAD_URL) ?: return@withContext Result.failure()

        val notificationManager = NotificationManagerCompat.from(appContext)

        // 1. Build initial persistent progress notification
        val builder = NotificationCompat.Builder(appContext, CHANNEL_ID)
            .setContentTitle("Downloading Course Materials: $courseId")
            .setContentText("Connecting to DevAscent CDN...")
            .setSmallIcon(android.R.drawable.stat_sys_download)
            .setOngoing(true)
            .setOnlyAlertOnce(true)
            .setProgress(100, 0, false)

        try {
            notificationManager.notify(NOTIFICATION_ID, builder.build())

            // 2. Simulate chunked download stream with cooperative cancellation checks
            val targetFile = File(appContext.filesDir, "course_${courseId}_archive.zip")
            
            for (progress in 10..100 step 20) {
                // Check if WorkManager cancelled the task
                if (isStopped) {
                    targetFile.delete()
                    notificationManager.cancel(NOTIFICATION_ID)
                    return@withContext Result.retry()
                }

                delay(300) // Non-blocking simulation of network chunk intake

                // Update progress on notification
                builder.setProgress(100, progress, false)
                    .setContentText("Downloading: $progress%")
                notificationManager.notify(NOTIFICATION_ID, builder.build())

                // Emits progress to WorkInfo observers
                setProgress(workDataOf("progress" to progress))
            }

            // 3. Post download completion notification
            val completeNotification = NotificationCompat.Builder(appContext, CHANNEL_ID)
                .setContentTitle("Download Complete")
                .setContentText("Course $courseId assets ready for offline study.")
                .setSmallIcon(android.R.drawable.stat_sys_download_done)
                .setOngoing(false)
                .setAutoCancel(true)
                .build()

            notificationManager.notify(NOTIFICATION_ID, completeNotification)

            val outputData = workDataOf("file_path" to targetFile.absolutePath)
            Result.success(outputData)

        } catch (e: Exception) {
            notificationManager.cancel(NOTIFICATION_ID)
            if (runAttemptCount < 3) {
                Result.retry()
            } else {
                Result.failure()
            }
        }
    }
}
Simulated Android Studio Logcat Execution Stream
08:14:02.102 D/WM-WorkManagerImpl: Enqueuing WorkRequest: [id=d3f92b71-789a, tag=download_course_CS301] 08:14:02.115 D/WM-GreedyScheduler: Constraints not met: Waiting for CONNECTED network 08:14:03.420 I/ConnectivityManager: NetworkCallback: onAvailable(Network: 104) [Transport: WIFI, Capability: INTERNET] 08:14:03.428 D/WM-SystemJobService: Constraints satisfied for JobId 4022. Launching worker... 08:14:03.435 I/DevAscentDownload: Starting doWork() on Dispatchers.IO (Thread: androidx.work-1) 08:14:03.442 D/NotificationManager: notify(tag=null, id=2001) - Channel: channel_downloads, Progress: 0/100 08:14:04.050 D/NotificationManager: notify(tag=null, id=2001) - Progress: 40/100 08:14:04.655 D/NotificationManager: notify(tag=null, id=2001) - Progress: 80/100 08:14:05.260 D/NotificationManager: notify(tag=null, id=2001) - Download Complete [AutoCancel: true] 08:14:05.275 I/WM-WorkerWrapper: Worker result SUCCESS for WorkRequest d3f92b71-789a
08

University Exam & Technical Interview Viva Guide (8 Master Q&As)

Q1. What causes an ANR in Android and what are the exact OS timeout thresholds?
An Application Not Responding (ANR) occurs when an application's Main (UI) Thread is blocked by a long-running operation, preventing it from processing input events or component lifecycle callbacks. The OS enforces three critical thresholds:
  1. Input Events: No response to user touch, key, or gesture events within 5 seconds.
  2. Broadcast Receivers: onReceive() fails to finish within 10 seconds (foreground) or 20 seconds (background).
  3. Services: Started service lifecycle methods fail within 20 seconds (foreground) or 200 seconds (background).
Q2. How do Kotlin Coroutines achieve non-blocking suspension under the hood?
Kotlin Coroutines use Continuation Passing Style (CPS). When the Kotlin compiler encounters a suspend function, it appends a hidden Continuation<T> parameter to the function signature and transforms the function body into a finite state machine. When a suspension point is reached (e.g. waiting for I/O), the function yields the underlying thread back to the OS without blocking. When the asynchronous operation finishes, Continuation.resumeWith() is called, restoring the local state and resuming execution.
Q3. What is the fundamental difference between Dispatchers.IO and Dispatchers.Default?
Dispatchers.IO is designed for blocking I/O operations (network, disk, database). It utilizes an elastic thread pool that can expand up to 64 threads (or more) because threads spend most of their time waiting on external hardware sockets or storage without burning CPU. Dispatchers.Default is designed for CPU-intensive computations (JSON parsing, bitmap filtering, sorting). Its thread pool is strictly capped to the number of physical CPU cores to prevent CPU thrashing and context-switch overhead.
Q4. Why does WorkManager enforce a minimum 15-minute interval for PeriodicWorkRequest?
Android enforces a strict 15-minute minimum interval (defined by PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS) to protect battery life and prevent aggressive background battery drain. If a developer sets an interval lower than 15 minutes (e.g. 5 minutes), the WorkManager library automatically clamps the interval up to 15 minutes at runtime. For real-time frequent events, developers must use WebSockets or Firebase Cloud Messaging (FCM) high-priority data pushes.
Q5. When would you choose WorkManager over Kotlin Coroutines (viewModelScope)?
Choose Kotlin Coroutines (viewModelScope) when the work is bound to the screen lifecycle and can be safely cancelled if the user navigates away (e.g. searching a list, fetching screen details). Choose WorkManager when the task is deferrable, constrained, and guaranteed to execute even if the user leaves the screen, force-kills the app process, or the device reboots (e.g. database cloud sync, uploading logs, offline message queue).
Q6. How does WorkManager survive application process death and device reboots?
WorkManager writes every WorkRequest, input dataset, constraint set, and retry policy into an internal Room SQLite database stored in the app's private sandbox. When a request is enqueued, WorkManager registers a job with the system JobScheduler. If the app process dies or the device is rebooted, the Android OS JobScheduler or a BOOT_COMPLETED broadcast wakes WorkManager, which queries its SQLite database and resumes pending tasks.
Q7. What changed in notification handling in Android 13 (API 33) and how must apps adapt?
In Android 13 (API 33), notification posting shifted from an automatic install-time grant to a runtime dangerous permission: android.permission.POST_NOTIFICATIONS. Apps must:
  1. Declare <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> in AndroidManifest.xml.
  2. Explicitly request runtime permission from the user before posting notifications.
  3. Handle cases where the user denies permission gracefully without crashing.
Q8. Why was the legacy CONNECTIVITY_ACTION broadcast deprecated in favor of NetworkCallback?
The legacy CONNECTIVITY_ACTION broadcast caused massive battery and memory storms: every time the device switched between Wi-Fi and mobile data, every installed app that registered this receiver woke up simultaneously in the background. Google deprecated this in Android 7.0 and replaced it with ConnectivityManager.registerNetworkCallback(), which only wakes apps currently in memory and allows precise filtering of network capabilities (such as NET_CAPABILITY_INTERNET or TRANSPORT_WIFI).