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 CaseBest ChoiceWhy
User clicks “Upload video now” and expects live progressForeground ServiceImmediate, user-visible, progress notification
User uploads large video and can leave app, but upload must continue visiblyForeground Service or WorkManager long-running WorkerBoth can show notification; FGS gives more direct control
Upload can wait until Wi-Fi/charging/network availableWorkManagerConstraint-based reliable background work
Upload should retry after failureWorkManagerBuilt-in retry/backoff support
Upload should continue after app process dies or device restartsWorkManagerPersistent work scheduling
Upload is very long, user-initiated data transferUser-Initiated Data Transfer Job, Android 14+Modern API for long user-started network transfers
Upload is only a few seconds while app is openCoroutine/ViewModelService 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."