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:
AlarmReceiver starts BackgroundService from BOOT_COMPLETED with startForegroundService().
BackgroundService.onCreate() always calls ServiceCompat.startForeground().
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
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.
Problem
The remaining top Play crash on
v0.14.0b2(versionCode 40) is an uncaughtForegroundServiceStartNotAllowedExceptionwhileBackgroundService.onCreate()promotes itself withServiceCompat.startForeground().A live, time-bounded Play Developer Reporting API query on 2026-09-13 shows:
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-versionChromeWatchercluster.Primary Play issue ID:
b422f7bf3b71b095ad1cf06a3079e0f0.Stack trace
There is a second, distinct cluster at the caller boundary:
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
masterstill has both unguarded boundaries:AlarmReceiverstartsBackgroundServicefromBOOT_COMPLETEDwithstartForegroundService().BackgroundService.onCreate()always callsServiceCompat.startForeground().onStartCommand()returnsSTART_STICKY, allowing Android to recreate the service after process death while the app is backgrounded.MainActivityalso 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_STICKYbackground-recreation path is the leading hypothesis; the separateAlarmReceiver.onReceivecluster proves that boot/background launch is independently unsafe. This issue should fix both exception boundaries rather than guessing one caller explains all reports.Scope
startForegroundService()inAlarmReceiveron API 31+; a boot broadcast must never crash the process.ServiceCompat.startForeground()inBackgroundService; stop without running native/server initialization or entering a crash/restart loop.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.MainActivity.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
startForegroundService()fromAlarmReceiveris caught and logged, with no app crash.startForeground()during service creation stops cleanly before Rust/server/scheduler initialization, with no app crash or restart loop.MainActivitystill starts the foreground service and local server.Data reproduction
The bounded query is available on ActivityWatch/stats#28:
Related: #185, #176, #252, ActivityWatch/stats#28.