Android 14 Background Restrictions I Wish I Knew Earlier

April 15, 20265 min readBy Sinnoor C

TL;DR — Android 14 broke our background work in 4 specific ways:

  • Foreground services now require a declared foregroundServiceType in the manifest AND a matching argument to startForeground — miss either and you ship a SecurityException that fires only on some devices.
  • BOOT_COMPLETED no longer auto-wakes your WorkManager scheduler until the user opens the app once after boot.
  • Exact alarms (setExact, setAlarmClock) need the new SCHEDULE_EXACT_ALARM permission — granted via system Settings, no runtime prompt.
  • High-priority FCM pushes are throttled for users in the "restricted" / "rarely used" App Standby buckets, regardless of message priority.
API levelReleasedTarget SDKAffects
34 (Android 14)August 2023targetSdk = 34Foreground services — must declare foregroundServiceType in manifest + pass to startForeground
34 (Android 14)August 2023targetSdk = 34BOOT_COMPLETED — broadcast delayed until first app open after reboot
34 (Android 14)August 2023targetSdk = 34Exact alarms — SCHEDULE_EXACT_ALARM permission required, granted via Settings
34 (Android 14)August 2023targetSdk = 34FCM high-priority — throttled in "restricted" / "rarely used" Standby buckets

When Android 14 (API 34) shipped, Google tightened a cluster of background-work APIs at once. Each change was documented, but the cumulative effect on a real enterprise app was absurd: push notifications stopped arriving for ~15% of users, WorkManager jobs started missing SLAs, and our foreground-service-backed GPS tracker crashed with ForegroundServiceStartNotAllowedException on first cold start.

None of it was a bug. All of it was us not reading the changelog carefully enough. Here's what I wish someone had handed me in one place.

1. Foreground services now require a declared foregroundServiceType

In previous Android versions, any service could startForeground() with a notification. In 14, you MUST declare the type in the manifest AND pass it to startForeground. Types are constrained to a specific list: camera, connectedDevice, dataSync, health, location, mediaPlayback, mediaProjection, microphone, phoneCall, remoteMessaging, shortService, specialUse, systemExempted.

MyForegroundService.kt
class SyncService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("Syncing data")
            .setSmallIcon(R.drawable.ic_sync)
            .build()
 
        // API 34+: must pass the type you declared in the manifest.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            startForeground(
                NOTIFICATION_ID,
                notification,
                ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
            )
        } else {
            startForeground(NOTIFICATION_ID, notification)
        }
 
        return START_STICKY
    }
}

And in your manifest:

AndroidManifest.xml
<service
    android:name=".SyncService"
    android:foregroundServiceType="dataSync"
    android:exported="false" />
 
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

If the permission or the type is missing, you'll get a SecurityException at runtime. Sometimes. On some devices. Not always. That's the worst part.

2. BOOT_COMPLETED no longer auto-wakes your scheduler

Android 14 delays broadcast delivery for apps that haven't been opened since boot. If you scheduled recurring work via WorkManager and expected the service to restart on boot, you'll find it doesn't until the user explicitly opens the app.

The fix: declare your receiver in the manifest (not just dynamically), and make sure the app has the high-priority permission. Or — better — design your sync to tolerate lazy re-initialization the first time the user opens the app.

3. Exact alarms require a new permission

If you used AlarmManager.setExact() or setAlarmClock() for a calendar-style reminder, you now need SCHEDULE_EXACT_ALARM declared in the manifest AND the user has to grant it manually via a system settings page. There's no runtime prompt.

Most apps don't need exact alarms. Check honestly: if your reminder can fire within a 10-minute window, switch to setAndAllowWhileIdle() and drop the permission entirely.

4. FCM delivery is doze-sensitive again

Push notifications via Firebase Cloud Messaging are nominally high-priority, but Android 14 throttles high-priority messages more aggressively for apps in the "restricted" or "rarely used" App Standby buckets. Symptoms: some users report "no pushes" while most receive them fine. What's actually happening is FCM is holding their delivery until the next sync window.

Two mitigations:

  • Ensure your server sends priority: "high" on genuine user-actionable pushes (chat, security alerts). Don't use high-priority for promotional content — Android will downgrade you.
  • When a user repeatedly misses pushes, ask them to exclude your app from battery optimization. Yes, this is a terrible UX. Yes, it's the only thing that works today.

Debugging checklist

When a device reports "no background work running":

adb.sh
# Dump battery / power / standby state for your package
adb shell dumpsys battery
adb shell dumpsys deviceidle
adb shell dumpsys usagestats | grep <your.package.name>
 
# Force the device into doze mode for repro
adb shell dumpsys deviceidle force-idle
 
# Inspect which WorkManager jobs are pending
adb shell dumpsys jobscheduler | grep <your.package.name>

If dumpsys deviceidle shows your app as =BATTERY_SAVER= or =RESTRICTED=, your WorkManager jobs will be delayed indefinitely. The only fix is user-initiated: they need to whitelist your app in Settings → Battery → Battery optimization.

That's the grim reality of shipping long-running work on Android in 2026. The OS is right to crack down on it. Our job is to design around it — not fight it.