Skip to main content

One post tagged with "setForegroundAsync"

View All Tags

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

Published: · 10 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.

How to detect this in production: Because race conditions (like a user backgrounding the app right as a Worker executes) can be hard to catch during local testing, monitoring real-user crashes is essential. With Appxiom, you can automatically detect ForegroundServiceStartNotAllowedException crashes , view the exact stack traces, and correlate them with app lifecycle events to pinpoint exactly when background WorkManager tasks are causing issues.

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.
  • Detecting Foreground Service Failures Early:
    • Even with try/catch blocks in place, silent failures (where work falls back to background execution or drops foreground status) can impact user experience or app performance.
  • Track caught exceptions:
    • Log non-fatal ForegroundServiceStartNotAllowedException catches to Appxiom to monitor how often your fallback logic is triggered across different OS versions.
  • Inspect session state:
    • Use Appxiom session logs to see the exact UI state and lifecycle transitions immediately preceding an FGS crash.

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+.