Below are your 2 questions in 1 answer, with upload scenario, decision rules, pseudocode, and Kotlin examples.
1. Upload case: when to prefer Foreground Service and when WorkManager?
First understand the main difference
Foreground Service = Do it now, user is watching, show notification. WorkManager = Do it reliably, maybe now/maybe later, survives app close/reboot.
Android says foreground services are for operations noticeable to the user and they must show a status-bar notification. WorkManager is for deferrable, asynchronous, reliable work that should run when constraints are met.
Simple upload decision table
| Upload Case | Best Choice | Why |
|---|---|---|
| User clicks “Upload video now” and expects live progress | Foreground Service | Immediate, user-visible, progress notification |
| User uploads large video and can leave app, but upload must continue visibly | Foreground Service or WorkManager long-running Worker | Both can show notification; FGS gives more direct control |
| Upload can wait until Wi-Fi/charging/network available | WorkManager | Constraint-based reliable background work |
| Upload should retry after failure | WorkManager | Built-in retry/backoff support |
| Upload should continue after app process dies or device restarts | WorkManager | Persistent work scheduling |
| Upload is very long, user-initiated data transfer | User-Initiated Data Transfer Job, Android 14+ | Modern API for long user-started network transfers |
| Upload is only a few seconds while app is open | Coroutine/ViewModel | Service may be overkill |
Android’s latest background-task docs specifically mention using user-initiated data transfers for large downloads/uploads instead of creating a generic data-sync foreground service.
Case A: Prefer Foreground Service for upload
Use Foreground Service when upload is:
Immediate User-started User-visible Long-running Needs live progress Should not silently happen in background
Example scenario:
User selects 2 GB video User taps Upload Now App shows notification: Uploading 35% User can leave app Upload continues User can cancel from notification
This is a good foreground service case because the upload is actively visible to the user.
Foreground Service upload flow
User taps Upload | v Activity starts UploadForegroundService | v Service calls startForeground() | v Upload starts | v Service updates notification progress | v Upload complete/fail/cancel | v Service updates final notification | v Service stops itself
Pseudocode
WHEN user clicks upload: create Intent put fileUri in Intent startForegroundService(intent) IN UploadService: create notification channel show notification with progress 0% start uploading file while uploading: calculate progress update notification progress if upload success: show "Upload complete" stop service if upload failed: show "Upload failed" stop service
Case B: Prefer WorkManager for upload
Use WorkManager when upload is:
Reliable Retryable Deferrable Constraint-based Should survive app close/reboot Not necessarily immediate
Example scenario:
User creates a report offline App should upload it when internet is available If upload fails, retry automatically If phone restarts, upload should continue later
This is a good WorkManager case.
WorkManager upload flow
User saves report | v App creates OneTimeWorkRequest | v Constraint: network required | v WorkManager stores job | v When network is available, Worker runs | v Worker uploads file | v Worker updates progress | v Worker returns success/failure/retry
Pseudocode
WHEN upload needed: create WorkRequest add input fileUri add network constraint enqueue work IN UploadWorker: read fileUri set foreground notification if long upload while uploading: calculate progress update WorkManager progress update foreground notification if success: return Result.success() if temporary failure: return Result.retry() if permanent failure: return Result.failure()
WorkManager supports observing intermediate progress using setProgress() in Kotlin, and progress is stored asynchronously in WorkManager’s database.
Important modern Android point
For long-running upload, WorkManager can also run as a foreground worker.
That means:
WorkManager + foreground notification
So it is not always:
Foreground Service OR WorkManager
Sometimes it is:
WorkManager running with foreground notification
Android docs say WorkManager supports long-running workers by managing a foreground service on your behalf. But starting with Android 16, long-running workers can exhaust job quota, and Android suggests considering direct foreground service or user-initiated data transfer jobs for some cases.
My practical recommendation
For normal app upload feature
Use this rule:
Small upload, app open: Coroutine in ViewModel Upload should retry / survive app restart: WorkManager Large upload user starts and watches: Foreground Service Large upload with reliability + constraints: WorkManager with foreground notification Very long user-started upload/download on Android 14+: User-Initiated Data Transfer Job
2. How notification updates from Service and WorkManager for upload progress
Now let’s understand progress notification.
Upload progress formula:
progress = uploadedBytes * 100 / totalBytes
Example:
Total file size = 100 MB Uploaded = 35 MB Progress = 35 * 100 / 100 = 35%
A. Notification progress from Foreground Service
Notification update concept
In Foreground Service, you update notification using:
NotificationManagerCompat.notify(notificationId, updatedNotification)
Important:
Use same notificationId
If you use the same notification ID, Android updates the existing notification.
If you use different IDs, Android shows multiple notifications.
Foreground Service upload notification flow
startForeground(UPLOAD_ID, notification 0%) | v Upload 10% | v notify(UPLOAD_ID, notification 10%) | v Upload 50% | v notify(UPLOAD_ID, notification 50%) | v Upload complete | v notify(UPLOAD_ID, completed notification) | v stopForeground() | v stopSelf()
Foreground Service code example
Manifest
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <service android:name=".UploadForegroundService" android:exported="false" android:foregroundServiceType="dataSync" />
For Android 14+ target apps, foreground service types are required for many foreground-service use cases.
Service Kotlin code
class UploadForegroundService : Service() { companion object { private const val CHANNEL_ID = "upload_channel" private const val NOTIFICATION_ID = 101 } override fun onCreate() { super.onCreate() createNotificationChannel() } override fun onStartCommand( intent: Intent?, flags: Int, startId: Int ): Int { val fileUri = intent?.getStringExtra("file_uri") // 1. Start foreground immediately with 0% notification val initialNotification = createUploadNotification(progress = 0) startForeground(NOTIFICATION_ID, initialNotification) // 2. Start upload on background thread/coroutine Thread { uploadFile(fileUri) }.start() return START_NOT_STICKY } private fun uploadFile(fileUri: String?) { val totalBytes = 100L // Example total size var uploadedBytes = 0L while (uploadedBytes < totalBytes) { Thread.sleep(500) uploadedBytes += 10 val progress = ((uploadedBytes * 100) / totalBytes).toInt() // Update notification progress updateNotification(progress) } showCompletedNotification() stopForeground(STOP_FOREGROUND_DETACH) stopSelf() } private fun updateNotification(progress: Int) { val notification = createUploadNotification(progress) NotificationManagerCompat .from(this) .notify(NOTIFICATION_ID, notification) } private fun createUploadNotification(progress: Int): Notification { return NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_upload) .setContentTitle("Uploading file") .setContentText("$progress% uploaded") .setOnlyAlertOnce(true) .setOngoing(true) .setProgress(100, progress, false) .build() } private fun showCompletedNotification() { val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_upload) .setContentTitle("Upload complete") .setContentText("Your file uploaded successfully") .setOnlyAlertOnce(true) .setOngoing(false) .setAutoCancel(true) .setProgress(0, 0, false) .build() NotificationManagerCompat .from(this) .notify(NOTIFICATION_ID, notification) } override fun onBind(intent: Intent?): IBinder? = null private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, "Upload Progress", NotificationManager.IMPORTANCE_LOW ) val manager = getSystemService(NotificationManager::class.java) manager.createNotificationChannel(channel) } } }
Start service from Activity
val intent = Intent(this, UploadForegroundService::class.java) intent.putExtra("file_uri", selectedFileUri.toString()) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent) } else { startService(intent) }
Important notification properties
.setProgress(100, progress, false)
Meaning:
100 = max progress progress = current progress false = determinate progress
Example:
.setProgress(100, 40, false)
Means:
40% upload completed
For unknown progress:
.setProgress(0, 0, true)
Meaning:
Indeterminate progress Example: Preparing upload...
B. Notification progress from WorkManager
WorkManager has two types of progress:
1. WorkManager progress 2. Notification progress
They are related but different.
1. WorkManager progress
This updates progress for your app UI.
Example:
Activity observes worker progress ProgressBar shows 35%
You update it using:
setProgress(workDataOf("progress" to progress))
2. Notification progress
For long upload, Worker can run in foreground mode and show a notification.
You update it using:
setForeground(createForegroundInfo(progress))
WorkManager long-running workers use foreground service behavior internally, and WorkManager manages the service lifecycle for you.
WorkManager upload flow with notification
Activity enqueues UploadWorker | v UploadWorker starts | v setForeground(notification 0%) | v Upload 10% | v setProgress(10) for UI | v setForeground(notification 10%) for notification | v Upload complete | v Result.success()
Add WorkManager dependency
implementation("androidx.work:work-runtime-ktx:2.10.0")
Use the latest compatible version in your project.
UploadWorker code example
class UploadWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { companion object { private const val CHANNEL_ID = "upload_channel" private const val NOTIFICATION_ID = 201 const val KEY_FILE_URI = "file_uri" const val KEY_PROGRESS = "progress" } override suspend fun doWork(): Result { createNotificationChannel() val fileUri = inputData.getString(KEY_FILE_URI) ?: return Result.failure() // Start as foreground worker with 0% notification setForeground(createForegroundInfo(progress = 0)) return try { uploadFile(fileUri) Result.success() } catch (e: Exception) { Result.retry() } } private suspend fun uploadFile(fileUri: String) { val totalBytes = 100L var uploadedBytes = 0L while (uploadedBytes < totalBytes) { delay(500) uploadedBytes += 10 val progress = ((uploadedBytes * 100) / totalBytes).toInt() // 1. Update progress for UI observer setProgress( workDataOf(KEY_PROGRESS to progress) ) // 2. Update notification progress setForeground( createForegroundInfo(progress) ) } } private fun createForegroundInfo(progress: Int): ForegroundInfo { val notification = NotificationCompat.Builder( applicationContext, CHANNEL_ID ) .setSmallIcon(R.drawable.ic_upload) .setContentTitle("Uploading file") .setContentText("$progress% uploaded") .setOnlyAlertOnce(true) .setOngoing(true) .setProgress(100, progress, false) .build() return ForegroundInfo( NOTIFICATION_ID, notification ) } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, "Upload Progress", NotificationManager.IMPORTANCE_LOW ) val manager = applicationContext .getSystemService(NotificationManager::class.java) manager.createNotificationChannel(channel) } } }
Enqueue WorkManager upload
val inputData = workDataOf( UploadWorker.KEY_FILE_URI to selectedFileUri.toString() ) val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>() .setInputData(inputData) .setConstraints(constraints) .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS ) .build() WorkManager.getInstance(context) .enqueue(uploadRequest)
Observe WorkManager upload progress in Activity/ViewModel
WorkManager.getInstance(context) .getWorkInfoByIdLiveData(uploadRequest.id) .observe(this) { workInfo -> if (workInfo != null) { val progress = workInfo.progress.getInt( UploadWorker.KEY_PROGRESS, 0 ) progressBar.progress = progress when (workInfo.state) { WorkInfo.State.RUNNING -> { textView.text = "Uploading $progress%" } WorkInfo.State.SUCCEEDED -> { textView.text = "Upload complete" } WorkInfo.State.FAILED -> { textView.text = "Upload failed" } WorkInfo.State.ENQUEUED -> { textView.text = "Waiting for network" } else -> Unit } } }
Main difference in progress update
Foreground Service
You directly update notification.
Code:
NotificationManagerCompat .from(this) .notify(NOTIFICATION_ID, notification)
WorkManager
You update Worker foreground notification using setForeground(). You update UI progress using setProgress().
Code:
setProgress(workDataOf("progress" to progress)) setForeground(createForegroundInfo(progress))
Foreground Service vs WorkManager progress architecture
Foreground Service architecture
Activity | | startForegroundService() v UploadForegroundService | | startForeground() v Notification 0% | | upload loop v NotificationManager.notify(101, progress notification) | v Notification 10%, 20%, 30%
WorkManager architecture
Activity | | enqueue UploadWorker v WorkManager | v UploadWorker | | setForeground() v Notification 0% | | upload loop v setProgress() -> UI observes progress setForeground() -> notification updates
Scenario-based deep explanation
Scenario 1: WhatsApp-style media upload
User selects video User taps send User expects upload immediately Progress should be visible User may leave chat screen
Best option:
Foreground Service
Why?
Upload is immediate and user-visible. Progress notification is expected. User may want cancel/pause.
Scenario 2: Expense app report upload
User fills expense report App can upload when internet is available If app closes, report should upload later If upload fails, retry
Best option:
WorkManager
Why?
Upload is reliable background work. It can wait for network. It needs retry. It should survive app/process death.
Scenario 3: Google Drive-style large upload
User uploads 5 GB file Upload may take long time User wants visible progress Upload should retry if network fails
Best practical option:
WorkManager with foreground notification
or on modern Android:
User-Initiated Data Transfer Job for very long user-started transfer
Why?
Need both user-visible progress and reliability.
Scenario 4: Upload logs silently at night
App uploads logs once per day User does not care about immediate progress Only upload on Wi-Fi Retry if failed
Best option:
WorkManager
Why?
Deferrable background work. No need for foreground service.
Upload progress with real OkHttp-style callback
Usually your upload progress comes from request body.
Progress callback idea
File size = totalBytes During upload: write bytes to network uploadedBytes += bytesWritten progress = uploadedBytes * 100 / totalBytes callback(progress)
Pseudocode
FUNCTION uploadFile(file, onProgress): totalBytes = file.size uploadedBytes = 0 WHILE reading file: read chunk send chunk to server uploadedBytes += chunk.size progress = uploadedBytes * 100 / totalBytes onProgress(progress)
In Foreground Service:
onProgress(progress): updateNotification(progress)
In WorkManager:
onProgress(progress): setProgress(progress) setForeground(notification(progress))
Cancel upload from notification
For production, upload notification should often support cancel.
Foreground Service cancel idea
Notification has Cancel button Cancel button sends PendingIntent to service Service receives ACTION_CANCEL_UPLOAD Service cancels upload job Service stops itself
Pseudocode:
IF intent.action == ACTION_CANCEL_UPLOAD: cancel upload stop service
WorkManager cancel idea
WorkManager.getInstance(context) .cancelWorkById(uploadRequest.id)
Inside Worker:
if (isStopped) { // stop upload call return Result.failure() }
Important interview answer
For upload, choose Foreground Service when the upload is immediate, user-visible, and needs continuous progress notification. Choose WorkManager when the upload is deferrable, needs retry, constraints like network/Wi-Fi/charging, or must survive app restart/device reboot. For long upload with progress, WorkManager can also run as a foreground worker using setForeground(). In Foreground Service, progress notification is updated using NotificationManager.notify() with the same notification ID. In WorkManager, UI progress is updated using setProgress(), and notification progress is updated by calling setForeground() again with a new ForegroundInfo.
Best memory trick
Foreground Service: "Upload now and show progress to user." WorkManager: "Upload reliably when conditions are good." WorkManager + Foreground: "Upload reliably and still show progress notification." UIDT Job: "Very long user-started upload/download on modern Android."