ActivityWatch / ActivityWatch/aw-android

ForegroundServiceStartNotAllowedException crashes BackgroundService on Android 14+

Ouverte
#255 2 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
Langage dominant
Kotlin
Étoiles
266
Forks
58
Merge moyen
1 j 18 h
PR mergées (30 j)
29

Description

## 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

```text
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:

```text
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:

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

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

Guide de contribution

Aucun guide de contribution indexé pour ce dépôt

Piste de recherche

Start with AlarmReceiver.kt:22 and BackgroundService.kt:51, then compare MainActivity’s normal foreground launch and the START_STICKY path in onStartCommand(). Use the listed Play query to establish the two crash clusters, and verify behavior on API 34 and 36+ for boot, sticky recreation, and activity launches. Done means both rejected foreground-service boundaries exit cleanly before initialization, normal activity startup still works, and diagnostics identify the start origin.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
android, kotlin, rust
Domaine
backend, mobile
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Active
Clarté
Plutôt claire
Accessibilité débutants
56/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.