Skip to main content

2 posts tagged with "WorkManager"

View All Tags

Fixing Android 17 Foreground Service Type Errors: A Practical WorkManager Migration Guide

Published: · 9 min read
Don Peter
Cofounder and CTO, Appxiom

Foreground services used to be the Swiss army knife for “do something now and don’t get killed.” Starting in Android 13 and tightened in 14–15 (and again in recent Android 17 previews), foreground services must declare and use the correct types, hold the right permissions, and start within strict timing windows. Many apps now crash with type mismatches, missing permissions, or background-start exceptions.

If your app still relies on foreground services for deferrable work (uploads, syncs, backups), the correct fix isn’t “pick the right type and pray.” It’s to migrate to WorkManager and only use foreground mode when it’s truly warranted. This post shows a practical, production-grade migration path you can drop into a real app.

Prerequisites

  • Android Studio Koala or newer
  • Kotlin 1.9.20+ (Kotlin 2.x is fine)
  • AGP 8.3+ (8.5+ recommended)
  • compileSdk = 35 (Android 15), targetSdk = 35
  • minSdk = 23+
  • WorkManager 2.9.1

Gradle dependencies:

dependencies {
implementation("androidx.work:work-runtime-ktx:2.9.1")
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
// Optional for Compose sample UI
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.4")
}

The real-world problem

You might see one or more of these on Android 14+ or recent Android 17 builds:

  • SecurityException: Missing required permission for foreground service type: FOREGROUND_SERVICE_DATA_SYNC
  • java.lang.IllegalArgumentException: Service did not specify foregroundServiceType
  • ForegroundServiceStartNotAllowedException: startForegroundService() not allowed while background
  • AppNotResponding or kill due to late startForeground() call

These aren’t intermittent “OEM quirks.” They’re the platform telling you to stop using foreground services for deferrable, batchable work.

When to keep a Foreground Service (FGS) vs. migrate

Keep a dedicated FGS only when the system says it’s the right tool:

  • Media playback (mediaPlayback)
  • Active navigation and continuous location/fitness tracking (location)
  • Phone calls/VoIP (phoneCall)
  • Screen capture/media projection (mediaProjection)
  • Remote messaging and certain accessibility/system-exempted flows

Everything else (uploads, data sync, backups, log shipping, prefetch, long computations) should be WorkManager.

Target feature for migration: reliable photo upload with progress

Scenario:

  • User selects a batch of photos to upload.
  • Work should respect constraints (unmetered network, device charging if desired).
  • Show progress when user stays in-app; continue reliably across process death and device reboots.
  • Comply with Android 14–17 foreground service restrictions.

We’ll migrate from a legacy FGS to WorkManager.

What the old code probably looks like (anti-pattern)

Manifest:

<service
android:name=".upload.UploadService"
android:exported="false"
android:foregroundServiceType="dataSync" />

Service:

class UploadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIFICATION_ID, buildNotification())
// Long-running upload on a thread...
// Crashes on 14+ if types/permissions mismatch or background-start happens.
return START_NOT_STICKY
}
}

Common failures:

  • Missing uses-permission for the declared type.
  • Background-start blocked on newer Android.
  • Timing windows missed if uploads initialize slowly.
  • The service outlives the app process and gets killed mid-upload.

The modern approach with WorkManager

  • Use Worker for deferrable uploads.
  • Enter foreground mode only while needed using ForegroundInfo.
  • Correctly declare the foreground service type and permission only if you actually run in foreground.
  • Let WorkManager handle process death, retries, constraints, and OS quotas.

Step 1: Manifest cleanup

Remove the legacy service declaration. Then add only the permissions you truly need.

If you plan to ever run the upload in foreground (e.g., immediate user-triggered large batch with visible progress), you must add the corresponding FGS permission for your chosen type on Android 14+.

For data sync uploads:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

If you don’t plan to run in foreground mode at all, do not add FGS permissions or types.

For notifications on Android 13+:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Create a notification channel at app startup:

fun ensureUploadChannel(context: Context) {
if (Build.VERSION.SDK_INT >= 26) {
val channel = NotificationChannel(
"upload",
"Uploads",
NotificationManager.IMPORTANCE_LOW
).apply { description = "Background uploads" }
context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
}

Step 2: Define the Worker

We’ll implement a robust UploadWorker with:

  • Constraints (e.g., unmetered network)
  • Exponential backoff
  • Foreground mode with correct type when needed
  • Progress updates
class UploadWorker(
appContext: Context,
params: WorkerParameters,
private val uploader: PhotoUploader, // Inject via DI; shown simplified
) : CoroutineWorker(appContext, params) {

override suspend fun getForegroundInfo(): ForegroundInfo {
// Only called if we entered foreground via setForeground()
return buildForegroundInfo(progress = 0)
}

override suspend fun doWork(): Result {
// Input: list of photo URIs
val uris = inputData.getStringArray(KEY_URIS)?.toList().orEmpty()
if (uris.isEmpty()) return Result.success()

// Decide whether to use foreground mode for this run (e.g., user-initiated large batch)
val needsImmediateUserVisible = inputData.getBoolean(KEY_USER_INITIATED, false)
if (needsImmediateUserVisible) {
setForeground(buildForegroundInfo(progress = 0))
}

try {
var completed = 0
for (uri in uris) {
setProgress(workDataOf(KEY_PROGRESS to ((completed * 100) / uris.size)))
uploader.upload(uri) // make it suspend, chunked, cancellable
completed++
if (needsImmediateUserVisible) {
setForeground(buildForegroundInfo(progress = ((completed * 100) / uris.size)))
}
}
setProgress(workDataOf(KEY_PROGRESS to 100))
return Result.success()
} catch (e: IOException) {
// Network/server issue: retry with backoff
return Result.retry()
} catch (e: CancellationException) {
throw e // cooperatively cancel
} catch (t: Throwable) {
// Unexpected, don't loop forever
return Result.failure()
}
}

private fun buildForegroundInfo(progress: Int): ForegroundInfo {
val notification = NotificationCompat.Builder(applicationContext, "upload")
.setSmallIcon(R.drawable.ic_upload)
.setContentTitle("Uploading photos")
.setContentText("$progress%")
.setOnlyAlertOnce(true)
.setOngoing(true)
.setProgress(100, progress, false)
.build()

// You must set the correct foreground service type on Android 14+ if you use foreground mode
return ForegroundInfo(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
}

companion object {
const val KEY_URIS = "uris"
const val KEY_USER_INITIATED = "user_initiated"
const val KEY_PROGRESS = "progress"
const val NOTIFICATION_ID = 42
}
}

WorkerFactory or Hilt can provide PhotoUploader; omitted for brevity.

Register the worker with WorkManager (if you use a custom WorkerFactory, wire it in your Configuration).

Step 3: Enqueue unique work with constraints and backoff

object UploadWork {
private const val UNIQUE_NAME = "photo_upload"

fun buildRequest(
uris: List<String>,
userInitiated: Boolean
): OneTimeWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // or CONNECTED if you allow metered
.build()

return OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(constraints)
.addTag(UNIQUE_NAME)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30, TimeUnit.SECONDS
)
.setInputData(
workDataOf(
UploadWorker.KEY_URIS to uris.toTypedArray(),
UploadWorker.KEY_USER_INITIATED to userInitiated
)
)
// If user-initiated and you need it to start ASAP, consider expedited:
// .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
}

fun enqueue(
context: Context,
request: OneTimeWorkRequest
) {
WorkManager.getInstance(context).enqueueUniqueWork(
UNIQUE_NAME,
ExistingWorkPolicy.APPEND_OR_REPLACE, // sequentialize batches
request
)
}
}

Notes:

  • Use APPEND or APPEND_OR_REPLACE to serialize uploads and avoid duplicates.
  • Use expedited work only for truly immediate operations; quotas may downgrade it to non-expedited.

Step 4: Observe progress in a ViewModel and display in Compose

ViewModel:

class UploadViewModel(
private val appContext: Context
) : ViewModel() {

private val workManager = WorkManager.getInstance(appContext)
private val _workId = MutableStateFlow<UUID?>(null)

val progress = _workId
.flatMapLatest { id ->
if (id == null) flowOf(0) else
workManager.getWorkInfoByIdFlow(id).map {
it.progress.getInt(UploadWorker.KEY_PROGRESS, 0)
}
}
.stateIn(viewModelScope, SharingStarted.Lazily, 0)

fun startUpload(uris: List<String>, userInitiated: Boolean) {
val request = UploadWork.buildRequest(uris, userInitiated)
_workId.value = request.id
UploadWork.enqueue(appContext, request)
}

fun cancel() {
_workId.value?.let { workManager.cancelWorkById(it) }
}
}

Compose UI:

@Composable
fun UploadScreen(vm: UploadViewModel) {
val progress by vm.progress.collectAsState()

Column(Modifier.padding(16.dp)) {
Text("Upload progress: $progress%")
LinearProgressIndicator(progress / 100f)
Row {
Button(onClick = {
// Example: user picks 3 images
vm.startUpload(uris = listOf("content://a", "content://b", "content://c"), userInitiated = true)
}) { Text("Start upload") }
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = vm::cancel) { Text("Cancel") }
}
}
}

Why this avoids Android 17 FGS type errors

  • No background-started services. WorkManager coexists with JobScheduler and OS constraints, avoiding ForegroundServiceStartNotAllowedException.
  • If and only if you enter foreground mode (setForeground), you:
    • Provide a ForegroundInfo with the correct type (DATA_SYNC).
    • Hold the matching permission (FOREGROUND_SERVICE_DATA_SYNC) on Android 14+.
  • If the work doesn’t need a foreground session, you don’t request foreground service privileges at all.

Additional production considerations

  • Dependency injection: Inject your repository/uploader into Worker via Hilt’s HiltWorker or a custom WorkerFactory.
  • Large files: Stream, chunk, and resume on retry. Avoid buffering entire files in memory.
  • Network/backoff: Use exponential backoff + retry after 5xx/IO errors; treat 4xx as terminal.
  • Constraints: Prefer CONNECTED for general cases; use UNMETERED if required by product.
  • Foreground lifetime: Only call setForeground when actually doing long-running user-visible work. Drop back to background when possible to reduce user-facing churn and battery impact.
  • Unique work: Tag and use enqueueUniqueWork to prevent duplicate runs from rapid user taps.
  • App upgrades and process death: WorkManager persists requests; no custom boot receivers required.

Testing across Android 14–17

  • Deny notification permission on Android 13+ and ensure your UX still works. For foreground mode, guide the user to grant it if you want a richer notification; otherwise, prefer background execution without foreground mode for non-essential cases.
  • Force background and sleep conditions:
    • adb shell cmd appops set your.app RUN_IN_BACKGROUND ignore
    • adb shell cmd deviceidle force-idle
  • Verify foreground service types when used:
    • adb shell dumpsys activity services | grep SystemForegroundService
  • Validate constraints by toggling Wi‑Fi/metered, battery saver, and charging state.

Troubleshooting

  • Crash: Service did not specify foregroundServiceType
    • In WorkManager, this happens if you call setForeground without ForegroundInfo specifying a type on Android 14+. Always pass ForegroundInfo(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_XXX).
  • SecurityException: Missing required permission for foreground service type
    • Add the matching uses-permission for the type (e.g., FOREGROUND_SERVICE_DATA_SYNC). Only add it if you truly run foreground workers.
  • ForegroundServiceStartNotAllowedException
    • You’re starting a service from the background or without user initiation. Use WorkManager. Don’t call startForegroundService directly.
  • Work never runs on some OEM devices
    • Ensure you aren’t doing blocking I/O on the main thread inside doWork.
    • Remove unrealistic constraints. Check WorkManager initialization logs. Consider avoiding expedited work unless necessary.
  • Duplicated uploads
    • Use enqueueUniqueWork with APPEND/KEEP/REPLACE policy and meaningful tags.

Migration checklist

  • Identify all non-essential FGS use cases (uploads, syncs, backups).
  • Replace with WorkManager:
    • OneTimeWorkRequest + Constraints + Backoff
    • Optional foreground mode with correct ForegroundInfo + permission
  • Remove legacy service components from the manifest.
  • Only keep FGS for system-approved continuous use cases.
  • Add UI to observe WorkInfo and show user progress or state.
  • Test on Android 14/15 and latest 17 preview builds with background restrictions.

Key takeaways

  • Most “Android 17 foreground service type” crashes are a symptom of using FGS for work that should be WorkManager.
  • With WorkManager, you get reliability, OS compliance, and fewer sharp edges, while still retaining foreground mode for truly user-visible, long-running tasks.
  • If you must use foreground mode, always set the correct type and permission via ForegroundInfo; otherwise, omit FGS entirely.
  • Adopt constraints, retries, unique work, and progress reporting for a production-grade solution.

Next steps: Audit your app’s services, pick one deferrable feature (like uploads or sync), and migrate it using the patterns in this guide. Once you see the stability and policy-compliance improvements, repeat for the rest.

Adaptive Battery Management Techniques for Background Android Services

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

Modern Android applications are expected to deliver seamless user experiences while minimizing their impact on device battery life. Yet, background services-often essential for features like messaging, location tracking, and data sync-pose some of the toughest challenges in striking the right balance between functionality and efficiency. This post dives deep into adaptive battery management strategies for background Android services, focusing on advanced optimization, observability, debugging, and reliability techniques. Whether you’re a mobile developer looking to tighten your app’s battery footprint, a QA engineer hunting for elusive drains, or an engineering leader shaping mobile architecture, this post delivers specific, actionable guidance ready for the trenches.


1. The Problem: Battery Life vs. Always-On Services

Background services historically consumed excessive battery, sometimes running unchecked and leading to poor device performance or even user uninstalls. Android’s evolution (notably Doze, App Standby, and Background Execution Limits) reflects Google’s battle against battery drains-but developers still face puzzles:

  • Push notifications delayed by aggressive Doze.
  • Essential sync jobs skipped due to background restrictions.
  • Unreliable location tracking under modern OS policies.

Real-world scenario:
A background service polling for updates every 10 minutes keeps your app responsive, but drains the battery rapidly. Conversely, switching to longer intervals or relying on OS-scheduled jobs sometimes causes critical data to arrive too late for the user.


2. Performance Optimization: Smart Scheduling and Work APIs

Leverage WorkManager for Adaptive Scheduling

WorkManager is Android’s recommended API for deferrable, persistent background work. Its strength: it adapts based on system state and battery optimizations, helping you "ask, not insist," for background execution.

Practical example:

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()

val syncWork = PeriodicWorkRequestBuilder<DataSyncWorker>(15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build()

WorkManager.getInstance(context).enqueue(syncWork)
  • setRequiresBatteryNotLow(true) ensures your work won’t run when the battery is critically low.
  • OS will batch background jobs, optimizing wake-ups to conserve battery.

Throttle and Backoff: Avoiding Wake-up Storms

Android 12+ applies throttling to alarm APIs and foreground services. Use exponential backoff to gracefully degrade polling frequency when repeated failures or delays are detected.

val work = OneTimeWorkRequestBuilder<MyWorker>()
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10, TimeUnit.SECONDS
)
.build()

Key strategies:

  • Batch non-urgent work using JobScheduler or WorkManager rather than running tasks on timers.
  • Use exact alarms (AlarmManager.setExactAndAllowWhileIdle), but only for user-visible, time-critical tasks due to steep battery costs.

3. Effective Debugging: Hunting Down Battery Drains

Bugs related to battery inefficiency are notoriously slippery because their impacts accumulate over hours or days.

Measure, Don’t Guess

  • Battery Historian:
    Visualize battery consumption over time, pinpointing abnormal wake locks, jobs, and network use.

  • adb shell dumpsys batterystats:
    Pull app-specific usage stats directly from a connected device.

    adb shell dumpsys batterystats [your.package.name]

Trace Service Lifecycles

  • Strict service lifecycle adherence:
    Mismanaged services that refuse to shut down keep CPUs awake. Always call stopSelf() when work is done.
  • Log key transitions:
    override fun onStartCommand(...): Int {
    Log.d(TAG, "Service started with intent: $intent")
    // work...
    return START_NOT_STICKY
    }
  • Analyze logs using robust filters (logcat | grep MyService) for every service entry, exit, and unexpected longevity.

Common Pitfalls

  • Held wake locks:
    Always acquire and release carefully, or-better-let WorkManager abstract them for you.
    try {
    wakeLock.acquire(timeout)
    // critical section
    } finally {
    wakeLock.release()
    }
  • AlarmManager misuse:
    Avoid frequent alarms and consider system batched jobs unless absolutely necessary.

4. Implementing Observability: Monitoring in Production

Reliability hinges on understanding real user behavior-not just lab assumptions.

Instrument with Modern Observability Tools

  • Appxiom Observability Platform:
    Use Appxiom to gain deep visibility into your app’s runtime behavior:

    • Track latency and error rates for background tasks
    • Monitor service lifecycle events in real time
    • Correlate failures with device state, OS restrictions, and battery conditions
  • Custom telemetry with Appxiom:
    Leverage Appxiom’s Activity Markers and Custom Issue Reporting to capture meaningful signals:

    • Mark important lifecycle events (service start/stop, task execution, etc.)
    • Report failures with severity to enable prioritization
    • Build a timeline of background execution behavior
// Mark important lifecycle or background events
Ax.setActivityMarker(this, "Service started: SyncService")

// Example: marking WorkManager completion
Ax.setActivityMarker(this, "WorkManager task completed: DataSyncWorker")

// Report failures with severity for observability
Ax.reportIssue(
this,
"Background Task Failed",
"DataSyncWorker failed due to timeout",
Severity.MAJOR
)

You can set as many activity markers as needed to trace execution paths and diagnose issues effectively.

Expose Battery Anomalies

  • Monitor real use of background execution quotas. Log or alert when quota is exhausted or service is denied background time.
  • Use App Standby Bucket API to detect your app’s current bucket and adapt behavior accordingly:
    val mUsageStatsManager = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
    val bucket = mUsageStatsManager.appStandbyBucket
    Log.d(TAG, "Current app standby bucket: $bucket")

5. Ensuring Application Reliability: Failures, Retries, and Graceful Degradation

Adaptive battery management is not just about less usage-it’s about doing the right thing for the user and the OS.

Robust Retrying

  • Back off and retry failed background jobs (see previous WorkManager code).
  • Persist failed operations in local storage when possible, then retry when constraints allow.

Graceful Feature Degradation

  • When in a restricted or Doze state, notify users about limitations (e.g., “You may experience delayed notifications”).
  • Avoid silent failures; fallback to lower-fidelity modes.

Testing and QA Practices

  • Simulate Doze and App Standby:
    Use adb shell dumpsys deviceidle and adb shell am set-inactive [package] true to force different power states.
  • Automate background work scenarios:
    Unit and instrumented tests should verify not just correctness, but that services sleep when idle and respect battery constraints.

Conclusion: Engineering Responsibly for Modern Android Devices

Adaptive battery management is both a technical and user-experience imperative in Android mobile engineering. By embracing smarter APIs, deeply instrumenting your code, and designing for reliability even in the face of OS-enforced restrictions, you’ll ship apps that delight users without draining their phones.

Key takeaways:

  • Prefer OS-aware, constraint-driven APIs like WorkManager.
  • Instrument for observability-don’t fly blind.
  • Debug with purpose; measure battery impact post-release.
  • Embrace adaptability and graceful degradation to deliver reliable experiences.

As the Android platform keeps tightening background execution, mastering these adaptive techniques isn’t optional-it’s your competitive edge. Start implementing smarter, observable, and reliable background services today to ensure your app remains both power-efficient and delightful tomorrow.