ActivityWatch / ActivityWatch/aw-android

ForegroundServiceStartNotAllowedException crashes BackgroundService on Android 14+

オープン
#255 コメント 2 件 リアクション 0 件 担当者 0 名 GitHub で見る
主要言語
Kotlin
スター
266
フォーク
58
平均マージ
1日 18時間
マージ済み PR(30日)
29

説明

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

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

評価

技術スタック
android, kotlin, rust
領域
backend, mobile
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
56/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。