Skip to main content

One post tagged with "Android Foreground Service"

View All Tags

WorkManager on Android 14 to 17: Fixing ForegroundServiceStartNotAllowedException & Execution Crashes

Published: · 9 min read
Robin Alex Panicker
Cofounder and CPO, Appxiom

Modern Android tightened foreground service (FGS) rules, and many apps that “worked fine” on Android 12/13 started crashing on Android 14+ after targeting API 34. A common stack trace is:

  • android.app.ForegroundServiceStartNotAllowedException
  • IllegalStateException: Not allowed to start service … app is in background

If your Worker calls setForegroundAsync() at the wrong time, WorkManager will attempt to start its system foreground service and the process can crash on Android 14+. In this post, I’ll show how to make WorkManager-based features resilient across Android 14–17 by preventing illegal FGS starts, using expedited work where appropriate, and structuring work so it never crashes even as platform rules tighten.

This is a practical, production-focused walkthrough you can apply to ongoing uploads, long-running syncs, and similar jobs.

Prerequisites​

  • Android Studio 2023.3 (Jellyfish) or newer
  • Kotlin 1.8+ (or newer)
  • Compile/target SDK: 34 or newer
  • Min SDK: per your app (examples below use 23+)
  • Dependencies:
    • androidx.work:work-runtime-ktx 2.9.x or newer
    • androidx.core:core-ktx 1.12+ or newer
    • androidx.lifecycle:lifecycle-runtime-ktx 2.6+ or newer

Note: Always prefer the latest stable AndroidX versions.

What changed on Android 14+ and why your Workers crash​

  • Foreground services can’t be started from the background unless the app qualifies under narrow, system-defined reasons (for example, an incoming call) or uses specific user-initiated flows. Simply scheduling a Worker and then promoting it to the foreground later (setForegroundAsync) is often illegal if the app isn’t visible.
  • WorkManager’s “foreground Worker” is implemented by starting an internal service (SystemForegroundService). If Android considers your app background when that service starts, the platform throws ForegroundServiceStartNotAllowedException.
  • On 14+, these checks are stricter and more consistent. Code that used to slip by now fails deterministically.

Implication: If you need FGS, start it only when the app is visible and user-initiated. Otherwise, don’t use FGS at all - use regular or expedited WorkRequests.

Choose the right tool for the job​

  • Immediate, short, user-initiated work (e.g., quick upload after pressing “Send”):
    • Use WorkManager expedited work (setExpedited).
    • No FGS needed; finishes quickly, runs immediately, but subject to quotas.
  • Long-running, user-initiated work that must keep running while the UI is visible:
    • You may use a foreground Worker but only start FGS while the app is visible.
    • If the user backgrounds the app, gracefully continue as non-FGS Worker (no crash) or pause/resume later.
  • Long-running background tasks without strict immediacy (sync, cleanup, batching):
    • Use regular WorkManager with constraints; do not use FGS.

If you truly need a continuous FGS while the app is backgrounded (rare under current policy), consider a dedicated Service started lawfully from the foreground UI and ensure it qualifies for allowed cases. Don’t expect WorkManager to bypass platform restrictions.

End-to-end example: crash-free “upload” Worker on Android 14+​

Goals:

  • Never crash due to FGS restrictions.
  • Prefer FGS while the app is visible, fall back to non-FGS when it’s not.
  • Use expedited work for immediate starts, but degrade cleanly if quota is exceeded.
  • Report progress via WorkManager so UI can observe it.

Manifest setup​

  • Declare foreground service permission for older/newer platforms as applicable to your target types.
  • Create a notification channel at app startup (for progress/FGS notifications).

Example (minimal):

<!-- AndroidManifest.xml -->
<manifest ...>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Add POST_NOTIFICATIONS runtime permission for Android 13+ if you show non-FGS notifications to users -->

<application ...>
<!-- No need to declare WorkManager’s SystemForegroundService manually; the library provides it. -->
</application>
</manifest>

Create your “uploads” notification channel once (e.g., in Application.onCreate):

fun Context.ensureUploadChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val mgr = getSystemService(NotificationManager::class.java)
val channel = NotificationChannel(
"uploads",
"Uploads",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Progress for user-initiated uploads"
}
mgr.createNotificationChannel(channel)
}
}

Foreground gating: only try FGS when the app is visible​

We’ll gate foreground promotion on process visibility using ProcessLifecycleOwner. If the app is backgrounded, we skip setForegroundAsync() entirely and keep the work running as normal background work.

class AppVisibilityTracker {
fun isAppVisible(): Boolean {
return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
}
}

ForegroundInfo builder​

Always provide the correct foreground service type for your use case. For uploads/sync, DATA_SYNC is typically appropriate.

class ForegroundNotifications(private val context: Context) {

fun buildUploadForegroundInfo(
notificationId: Int,
progress: Int
): ForegroundInfo {
context.ensureUploadChannel()

val notification = NotificationCompat.Builder(context, "uploads")
.setSmallIcon(R.drawable.ic_upload) // your icon
.setContentTitle("Uploading")
.setContentText("$progress%")
.setOngoing(true)
.setOnlyAlertOnce(true)
.setProgress(100, progress, false)
.build()

val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC

return ForegroundInfo(notificationId, notification, type)
}
}

A resilient Worker​

Key points:

  • Try to promote to foreground only if the app is visible at the time you start the work.
  • Call setForegroundAsync early, before heavy I/O, to satisfy the “startForeground quickly” rule.
  • Catch ForegroundServiceStartNotAllowedException to protect against race conditions (the user backgrounds the app between checks).
  • Continue the work without FGS if promotion fails.
  • Report progress via setProgress to let the UI observe WorkInfo.
class UploadWorker(
appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {

private val visibilityTracker = AppVisibilityTracker()
private val fg = ForegroundNotifications(appContext)

override suspend fun doWork(): Result {
val uploadId = inputData.getString("uploadId") ?: return Result.failure()
val notificationId = uploadId.hashCode()

// Optional: This flag indicates the caller wants FGS while visible.
val preferForeground = inputData.getBoolean("preferForeground", true)

// Attempt foreground promotion if allowed.
if (preferForeground && visibilityTracker.isAppVisible()) {
try {
setForegroundAsync(
fg.buildUploadForegroundInfo(notificationId, progress = 0)
).await()
} catch (e: android.app.ForegroundServiceStartNotAllowedException) {
// App went background or platform denied; continue without FGS.
// Log to crash/analytics but DO NOT fail the work.
} catch (t: Throwable) {
// Be defensive - never crash here.
}
}

// Simulated upload loop with progress. Replace with your real repository code.
return try {
val total = 100
for (p in 0..total step 5) {
// Respect cancellation to avoid stalled FGS and wasted work.
if (isStopped) return Result.retry()

// If we’re currently in FGS and app is backgrounded, you can optionally
// drop FGS to be conservative. Foreground downgrades are always allowed.
if (preferForeground && !visibilityTracker.isAppVisible()) {
// No API to “unset” foreground explicitly. Simply stop updating FG
// notification. WorkManager will keep the work going under JobScheduler.
}

setProgress(workDataOf("progress" to p))
// Update FGS notification if still in FG mode.
// This is harmless if we never successfully promoted.
try {
setForegroundAsync(
fg.buildUploadForegroundInfo(notificationId, progress = p)
).await()
} catch (_: android.app.ForegroundServiceStartNotAllowedException) {
// Ignore; keep running without FG.
} catch (_: Throwable) {
// Ignore non-fatal notification errors.
}

// Do a real chunk upload here (suspend), not delay
delay(300)
}
Result.success()
} catch (io: IOException) {
// Backoff on transient failures
Result.retry()
} catch (t: Throwable) {
Result.failure()
}
}
}

Notes:

  • If you need deterministic “once foreground, always foreground” until completion, don’t rely on WorkManager on Android 14+. Use a dedicated Service started while the UI is visible and keep the user in-app for the duration.
  • The Worker above optimizes for “never crash” and “do useful work even if we can’t legally be foreground.”

Scheduling from the UI​

  • Enqueue unique work to prevent duplicate concurrent jobs.
  • Use expedited work for immediate starts; degrade to non-expedited if the quota is exhausted.
fun enqueueUserInitiatedUpload(context: Context, uploadId: String) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val request = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(constraints)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setInputData(
workDataOf(
"uploadId" to uploadId,
"preferForeground" to true
)
)
.build()

WorkManager.getInstance(context).enqueueUniqueWork(
"upload-$uploadId",
ExistingWorkPolicy.KEEP,
request
)
}

Observing progress from UI (e.g., ViewModel):

class UploadViewModel(
private val workManager: WorkManager
) : ViewModel() {

fun observeProgress(uploadId: String): Flow<Int> {
return workManager.getWorkInfosForUniqueWorkLiveData("upload-$uploadId")
.asFlow()
.map { list ->
val info = list.firstOrNull() ?: return@map 0
info.progress.getInt("progress", 0)
}
.distinctUntilChanged()
}
}

Why this pattern fixes crashes​

  • We never blindly promote to foreground. We check visibility first and still guard calls with try/catch because race conditions can happen in the wild.
  • If promotion fails, we keep going as a background job under JobScheduler. No process crash.
  • We call setForegroundAsync early, complying with the “startForeground quickly” rule when we do run in FG.
  • We use expedited work for user-initiated immediacy without relying on FGS.

Common mistakes and how to fix them​

  • Calling setForegroundAsync() after a long delay or after starting the heavy work:
    • Fix: Call it at the start of doWork and before network/IO when you intend to be FG.
  • Promoting to FGS unconditionally in a Worker that could start anytime (including hours later):
    • Fix: Gate on app visibility and user intent. If the task isn’t immediate and user-initiated, don’t use FGS.
  • Assuming WorkManager can keep a foreground service alive while the app is backgrounded:
    • Reality: Android 14+ heavily restricts this. Expect denial unless the case matches system-allowed reasons.
  • Not handling ForegroundServiceStartNotAllowedException:
    • Fix: Try/catch around setForegroundAsync(). Never let it bubble up and kill the process.
  • Missing notification channel or invalid notification:
    • Fix: Create a channel on O+ and always provide a valid small icon and content.
  • Relying only on FGS for survivability:
    • Fix: Design for resumability. Persist progress, support retries, and be prepared to continue without FGS.

Troubleshooting guide​

  • Crash with ForegroundServiceStartNotAllowedException when Worker calls setForegroundAsync:
    • Your app was backgrounded when WorkManager tried to start its FGS. Gate by ProcessLifecycleOwner and add try/catch.
  • IllegalStateException: Not allowed to start service Intent:
    • Same root cause. Avoid background FGS starts.
  • Expedited work not starting immediately:
    • You hit quota. Use OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST and set user expectations (e.g., “Queued” state).
  • Foreground “did not call startForeground within 5s” style issues:
    • You promoted too late or built a bad notification. Promote early and ensure the notification is valid.
  • No notification appears:
    • Missing channel or no POST_NOTIFICATIONS permission for normal (non-FGS) notifications on Android 13+. Foreground notifications have special handling, but your progress UI outside FGS may still require permission.

Implementation checklist​

  • Adopt the latest WorkManager (2.9.x or newer).
  • Target SDK 34+ and test on Android 14+ devices and emulators.
  • Only use FGS for truly user-initiated, immediate work while visible.
  • Gate FGS promotion on app visibility and catch ForegroundServiceStartNotAllowedException.
  • Prefer expedited work for immediacy; gracefully handle quota fallback.
  • Build valid notifications and channels; update progress frequently and respect cancellation.
  • Make work resumable. Design for interruption and background execution without FGS.

Key takeaways and next steps​

  • Android 14+ makes background FGS starts illegal in most cases. If your Worker promotes to foreground at the wrong time, you’ll crash.
  • Fixes are architectural: prefer expedited jobs for immediacy, restrict FGS to visible, user-initiated flows, and ensure Workers continue without FGS when denied.
  • Add defensive coding - gate foreground promotion and catch exceptions - to make your app robust across Android 14–17.

Next steps:

  • Audit all Workers that call setForegroundAsync().
  • Implement the visibility gating and try/catch pattern shown above.
  • Migrate long-running background FGS use cases to regular WorkManager with constraints, plus resumability.
  • For rare cases that truly require background FGS, evaluate a dedicated Service launched from the UI and ensure it fits allowed categories on Android 14+.