Skip to content

ForegroundServiceStartNotAllowedException crashes BackgroundService on Android 14+ #255

Description

@TimeToBuildBob

Problem

The remaining top Play crash on v0.14.0b2 (versionCode 40) is an uncaught ForegroundServiceStartNotAllowedException while BackgroundService.onCreate() promotes itself with ServiceCompat.startForeground().

A live, time-bounded Play Developer Reporting API query on 2026-09-13 shows:

Window (UTC) Reports Distinct users
Last 24h 41 14
Last 7d 475 83
Last 21d 928 142

The cluster is still active (lastErrorReportTime: 2026-09-12T23:00:00Z), spans Android API 34–37, and only names versionCode 40. This is now larger than the residual old-version ChromeWatcher cluster.

Primary Play issue ID: b422f7bf3b71b095ad1cf06a3079e0f0.

Stack trace

Exception java.lang.RuntimeException:
  at android.app.ActivityThread.handleCreateService (...)
Caused by android.app.ForegroundServiceStartNotAllowedException:
  at android.app.IActivityManager$Stub$Proxy.setServiceForeground (...)
  at android.app.Service.startForeground (...)
  at androidx.core.app.ServiceCompat$Api34Impl.startForeground (ServiceCompat.java:241)
  at androidx.core.app.ServiceCompat.startForeground (ServiceCompat.java:172)
  at net.activitywatch.android.BackgroundService.onCreate (BackgroundService.kt:51)

There is a second, distinct cluster at the caller boundary:

Caused by android.app.ForegroundServiceStartNotAllowedException:
  at android.app.ContextImpl.startForegroundService (...)
  at net.activitywatch.android.watcher.AlarmReceiver.onReceive (AlarmReceiver.kt:22)

That companion cluster contributes another 7 reports / 5 users in 7d (26 / 10 in 21d), on API 33–37, also versionCode 40.

Current paths and likely trigger

Current master still has both unguarded boundaries:

  1. AlarmReceiver starts BackgroundService from BOOT_COMPLETED with startForegroundService().
  2. BackgroundService.onCreate() always calls ServiceCompat.startForeground().
  3. onStartCommand() returns START_STICKY, allowing Android to recreate the service after process death while the app is backgrounded.

MainActivity also starts the service, but that path starts from a visible activity and is expected to be allowed.

The primary stack does not preserve the initiating intent, so Play data alone cannot prove whether every occurrence is a sticky restart, a boot start whose promotion is rejected, or an OEM lifecycle edge. The START_STICKY background-recreation path is the leading hypothesis; the separate AlarmReceiver.onReceive cluster proves that boot/background launch is independently unsafe. This issue should fix both exception boundaries rather than guessing one caller explains all reports.

Scope

  • Gracefully handle a rejected startForegroundService() in AlarmReceiver on API 31+; a boot broadcast must never crash the process.
  • Gracefully handle a rejected ServiceCompat.startForeground() in BackgroundService; stop without running native/server initialization or entering a crash/restart loop.
  • Re-evaluate START_STICKY: retain it only if a system-initiated background recreation can legally promote the service on supported Android versions. Otherwise use an explicit allowed restart path.
  • Preserve the normal foreground launch from MainActivity.
  • Add lifecycle diagnostics identifying the start origin (activity, boot, sticky/null-intent, settings) so future Play clusters can be attributed.

Restoring collection/server availability after an OEM kills the service is related but separate work in #252. This issue's minimum outcome is no process crash when Android refuses foreground-service startup; silently pretending the service is alive is not acceptable either.

Acceptance criteria

  • A denied startForegroundService() from AlarmReceiver is caught and logged, with no app crash.
  • A denied startForeground() during service creation stops cleanly before Rust/server/scheduler initialization, with no app crash or restart loop.
  • Normal launch from MainActivity still starts the foreground service and local server.
  • Boot behavior is verified on API 34 and API 36+ (or the unsupported boundary is documented and surfaced to the user).
  • A simulated/system-initiated service recreation while backgrounded does not crash.
  • After release, the two Play issue IDs are re-queried over a comparable 7-day window and show no new reports on the fixed versionCode.

Data reproduction

The bounded query is available on ActivityWatch/stats#28:

uv run vitals.py errors --days 7 --limit 100 --stacktraces --markdown

Related: #185, #176, #252, ActivityWatch/stats#28.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions