What Changed in Android 15 and Why Your App May Crash
Android 15 (API 35) tightens the screws on background work harder than any release since Doze. If you’re targeting API 35 and your app relies on long-running services, you’re already on borrowed time — Google’s compatibility shims expire fast. Android 15 foreground services now require explicit type declarations, a documented user-initiated trigger, and a permission per type, and the platform enforces all three at runtime.
However, the new restrictions aren’t arbitrary. Most apps using foreground services for periodic sync, expedited jobs, or “keep-alive” patterns were never the right tool for the job. WorkManager and the new user-initiated APIs cover the legitimate cases more cleanly. Here’s the migration playbook we used across two production apps.
Required foregroundServiceType declarations
Every <service> element with foreground=true must now declare a foregroundServiceType in the manifest. The valid values include mediaPlayback, location, dataSync, connectedDevice, mediaProjection, camera, microphone, health, remoteMessaging, shortService, specialUse, and systemExempted.
Furthermore, each type maps to a paired permission like FOREGROUND_SERVICE_DATA_SYNC or FOREGROUND_SERVICE_LOCATION. Declaring the type without the corresponding permission throws SecurityException on startup. Lint catches most of these mismatches, but only if you actually run lint in CI.
The userInitiated requirement
The shortService and dataSync types now require a documented user trigger when started from the background. Specifically, the start request must originate within a few seconds of an explicit user action — a button tap, a notification action, an incoming call. Otherwise the platform throws ForegroundServiceStartNotAllowedException.
However, services started while your app is in the foreground are exempt, as are services tied to bound activities like a media session or a navigation overlay. The vast majority of crashes I’ve debugged on this come from “sync after push notification” patterns where the push handler tries to start a foreground service from cold launch.
// AndroidManifest.xml additions
// <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
// <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
// <service
// android:name=".sync.UploadService"
// android:foregroundServiceType="dataSync"
// android:exported="false" />
class UploadService : Service() {
override fun onStartCommand(
intent: Intent?, flags: Int, startId: Int
): Int {
val notification = buildNotification()
try {
ServiceCompat.startForeground(
this,
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} catch (e: ForegroundServiceStartNotAllowedException) {
// Fall back to WorkManager — the request was not user-initiated
UploadWorker.enqueueOneShot(applicationContext)
stopSelf(startId)
return START_NOT_STICKY
}
scope.launch { performUpload(startId) }
return START_REDELIVER_INTENT
}
override fun onBind(intent: Intent?): IBinder? = null
private fun buildNotification(): Notification {
val channelId = ensureChannel()
return NotificationCompat.Builder(this, channelId)
.setContentTitle(getString(R.string.uploading))
.setSmallIcon(R.drawable.ic_upload)
.setOngoing(true)
.setForegroundServiceBehavior(
NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
)
.build()
}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
companion object {
private const val NOTIFICATION_ID = 4711
fun start(context: Context, uri: Uri) {
val intent = Intent(context, UploadService::class.java)
.putExtra("uri", uri)
ContextCompat.startForegroundService(context, intent)
}
}
}
When to migrate to WorkManager instead
Many “foreground service” patterns were never user-visible work. Periodic sync, opportunistic uploads, expedited push handling — these all belong in WorkManager. Specifically, expedited WorkRequest instances with OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST handle most “I need this within a minute” cases without needing a foreground service at all.
Conversely, genuinely long-running, user-visible work — playing a podcast, recording a workout, navigating a route — still belongs in a foreground service with the appropriate type. The dividing line is whether the user expects to see ongoing progress in their notification shade. For a deeper view on background scheduling, see my earlier post on Android baseline profiles, which covers cold-start patterns that often interact with these services.
Real migration examples
For a Spotify-style media app, the answer is unchanged: declare mediaPlayback, attach a MediaSession, and you’re compliant. The platform treats media as a first-class long-running case. Just make sure the service is started in response to a user play action, not auto-resume on launch.
For a Strava-style activity tracker, declare location with the matching permission and start the service from the explicit “Start workout” button. Auto-starting on app launch — for example to “warm up” the GPS — is now denied. Push that warmup to the foreground UI itself.
For a file uploader, the cleanest pattern is a one-shot expedited WorkRequest per upload, with a setExpedited call. If the upload genuinely needs to run for more than 10 minutes, escalate to a foreground service of type dataSync from within the worker — this transition is allowed even from background.
Mitigating ForegroundServiceStartNotAllowedException
This is the exception you’ll see most often during migration. The fix is rarely “wrap in try/catch and retry” — that just defers the crash. Instead, audit every code path that calls startForegroundService and ask whether the user just did something that justifies it.
Additionally, the shortService type is a useful escape hatch for genuinely short tasks (under 3 minutes) that need foreground priority. It carries fewer restrictions than dataSync but cannot run indefinitely. Use it for tasks like “finish this single upload after the app is backgrounded” rather than persistent sync loops. Architecture-wise, this pairs well with patterns I’ve covered in shared ViewModel architectures.
Debugging with adb shell dumpsys
The single most useful command during migration is adb shell dumpsys activity services <package>. It dumps every active service, its declared type, the trigger source, and the elapsed runtime. Specifically, look at the fgRequired and fgServiceType fields — mismatches between manifest and runtime always cause crashes.
Furthermore, adb logcat -s ActivityManager surfaces the platform’s reasoning when it denies a service start. The error messages now name the offending policy explicitly, which is a huge improvement over Android 14. The official Android 15 behavior changes documentation is the canonical reference.
Testing the new restrictions
Run your app under a debug build with adb shell am compat enable FGS_TYPE_PERMISSION_CHANGE_ID <package> to force-enable the new enforcement on older preview devices. Furthermore, exercise every push-handler, broadcast receiver, and alarm trigger path — these are the cold-start scenarios that bite hardest.
In conclusion, Android 15 foreground services require explicit type declarations, paired permissions, and a documented user trigger for most types, and the platform enforces all three at runtime. The migration is mostly an audit exercise: identify each service, classify its purpose, and route it to the right primitive — type-specific foreground service for user-visible long work, expedited WorkManager for everything else. Apps that complete this audit cleanly land on a more predictable, battery-friendly platform; apps that don’t will simply crash on launch for users running the new release.