fix(android): declare NtfyPushService specialUse, not dataSync #22

Merged
mohlec merged 1 commit from fix/ntfy-fgs-special-use into ncw/main 2026-09-01 18:32:42 +00:00
Owner

Fixes #18.

Declares NtfyPushService as specialUse instead of dataSync, adds a
Service.onTimeout() backstop, and guards the boot receiver.

Why

Android 15 put a 6-hour-per-24-hour budget on dataSync foreground services and kills the
process when it runs out. This was 26 of the last 40 crash reports -- the largest single
crash source in the field.

dataSync was also wrong on the merits: this service holds a long-lived SSE subscription to a
self-hosted push relay, which is not a bounded data transfer. Only dataSync and
mediaProcessing carry a time budget.

Second bug fixed by the same change

Android 15+ forbids BOOT_COMPLETED receivers from starting dataSync services, throwing
ForegroundServiceStartNotAllowedException. NtfyBootReceiver does exactly that, so push has
not been surviving a reboot on Android 15/16 at all.
specialUse is not in the disallowed
list, so this fixes the boot path too. Worth verifying explicitly on device, since nobody had
reported it -- a reboot is rare enough that it may simply have gone unnoticed.

Evidence the diagnosis is right

The timeout arrived in Android 15, and the crash distribution matches exactly: every timeout
report is from the two Android 16 handsets, while the Android 13/14 device in the same
window produced none (all of its reports are the unrelated OOM in #19). Expect this to spread as
the fleet updates to 15+.

The Play Store trade, stated plainly

specialUse requires a PROPERTY_SPECIAL_USE_FGS_SUBTYPE justification that Google reviews at
submission. That is the right trade rather than something to design around:

  • The platform PRD has store submission in progress for both platforms, so a review is coming
    regardless.
  • remoteMessaging, the only other untimed candidate, documents its use case as "Transfer text
    messages from one device to another"
    -- device-to-device continuity. Declaring it for a
    server connection would be a mis-declaration, and a policy rejection is a worse outcome than a
    review.
  • Upstream strips FOREGROUND_SERVICE_SPECIAL_USE because removing a library's foreground
    service lets them skip a review they do not need. Upstream uses FCM and has no persistent
    service; this fork needs one precisely because it does not. Not a conflict.

Action for whoever owns Play submission: the Play Console app-content form will now ask about
a special-use foreground service. The justification text is in the manifest and can be pasted in.

Scope notes

  • FOREGROUND_SERVICE_DATA_SYNC is deliberately retained -- androidx WorkManager's
    SystemForegroundService still declares dataSync for RNShare.
  • onTimeout() should never fire now. It is a backstop for the declaration being changed back,
    or a future platform release adding a budget to our type. It compiles at the current
    compileSdk 35 and is simply never called below API 35, so no minSdk 24 guard is needed.
  • The boot-receiver try/catch is defence in depth: an uncaught throw there crashes the process
    during boot, which is the same failure family this PR is closing.

Verification

Static: manifest parses, foregroundServiceType is specialUse, the <property> child is
present, and both permissions are declared as intended.

Not yet verified on device. Needs a build plus:

  1. Leave the app installed and idle well past 6 hours on an Android 15+ handset; confirm no
    ForegroundServiceDidNotStopInTimeException and that push still arrives.
  2. Reboot an Android 15+ handset, do not open the app, send a message; confirm the notification
    arrives (this is the boot-path fix).
  3. Confirm the ongoing foreground notification still behaves -- no badge regression, per #12.

After the upstream rebase

Android 16 adds onTimeout(int startId, int fgsType) alongside the single-argument form. Once
the fork moves to compileSdk 36, re-check which overload the platform calls and whether the
one-argument version is deprecated.

Fixes #18. Declares `NtfyPushService` as `specialUse` instead of `dataSync`, adds a `Service.onTimeout()` backstop, and guards the boot receiver. ### Why Android 15 put a 6-hour-per-24-hour budget on `dataSync` foreground services and kills the process when it runs out. This was **26 of the last 40 crash reports** -- the largest single crash source in the field. `dataSync` was also wrong on the merits: this service holds a long-lived SSE subscription to a self-hosted push relay, which is not a bounded data transfer. Only `dataSync` and `mediaProcessing` carry a time budget. ### Second bug fixed by the same change Android 15+ forbids `BOOT_COMPLETED` receivers from starting `dataSync` services, throwing `ForegroundServiceStartNotAllowedException`. `NtfyBootReceiver` does exactly that, so **push has not been surviving a reboot on Android 15/16 at all.** `specialUse` is not in the disallowed list, so this fixes the boot path too. Worth verifying explicitly on device, since nobody had reported it -- a reboot is rare enough that it may simply have gone unnoticed. ### Evidence the diagnosis is right The timeout arrived in Android 15, and the crash distribution matches exactly: every timeout report is from the two **Android 16** handsets, while the Android 13/14 device in the same window produced none (all of its reports are the unrelated OOM in #19). Expect this to spread as the fleet updates to 15+. ### The Play Store trade, stated plainly `specialUse` requires a `PROPERTY_SPECIAL_USE_FGS_SUBTYPE` justification that Google reviews at submission. That is the right trade rather than something to design around: - The platform PRD has store submission in progress for both platforms, so a review is coming regardless. - `remoteMessaging`, the only other untimed candidate, documents its use case as *"Transfer text messages from one device to another"* -- device-to-device continuity. Declaring it for a server connection would be a mis-declaration, and a policy rejection is a worse outcome than a review. - Upstream strips `FOREGROUND_SERVICE_SPECIAL_USE` because removing a *library's* foreground service lets them skip a review they do not need. Upstream uses FCM and has no persistent service; this fork needs one precisely because it does not. Not a conflict. **Action for whoever owns Play submission:** the Play Console app-content form will now ask about a special-use foreground service. The justification text is in the manifest and can be pasted in. ### Scope notes - `FOREGROUND_SERVICE_DATA_SYNC` is **deliberately retained** -- androidx WorkManager's `SystemForegroundService` still declares `dataSync` for RNShare. - `onTimeout()` should never fire now. It is a backstop for the declaration being changed back, or a future platform release adding a budget to our type. It compiles at the current compileSdk 35 and is simply never called below API 35, so no `minSdk 24` guard is needed. - The boot-receiver `try/catch` is defence in depth: an uncaught throw there crashes the process during boot, which is the same failure family this PR is closing. ### Verification Static: manifest parses, `foregroundServiceType` is `specialUse`, the `<property>` child is present, and both permissions are declared as intended. **Not yet verified on device.** Needs a build plus: 1. Leave the app installed and idle well past 6 hours on an Android 15+ handset; confirm no `ForegroundServiceDidNotStopInTimeException` and that push still arrives. 2. Reboot an Android 15+ handset, do not open the app, send a message; confirm the notification arrives (this is the boot-path fix). 3. Confirm the ongoing foreground notification still behaves -- no badge regression, per #12. ### After the upstream rebase Android 16 adds `onTimeout(int startId, int fgsType)` alongside the single-argument form. Once the fork moves to compileSdk 36, re-check which overload the platform calls and whether the one-argument version is deprecated.
Android 15 introduced a 6-hour-per-24-hour budget for dataSync foreground
services. On expiry the system calls Service.onTimeout() and, absent a clean
stop, kills the process with:

  android.app.RemoteServiceException$ForegroundServiceDidNotStopInTimeException:
  A foreground service of type dataSync did not stop within its timeout:
  ComponentInfo{com.ncwcom.slash/com.ncwcom.slash.NtfyPushService}

This was the single largest crash source in the field: 26 of the last 40 crash
reports, every one of them from an Android 16 handset. The Android 13/14 device
in the same reporting window never produced one, which matches the platform
behaviour arriving in 15 -- so this spreads as the fleet updates.

dataSync was also the wrong declaration on the merits. The service holds a
long-lived SSE subscription to a self-hosted push relay; it is not performing a
bounded data transfer. Only dataSync and mediaProcessing carry a time budget,
so specialUse resolves it, and Android 16 adds no new foreground-service
restrictions.

Second bug fixed by the same change: Android 15+ forbids BOOT_COMPLETED
receivers from starting dataSync services (also camera, mediaPlayback,
phoneCall, mediaProjection, microphone), throwing
ForegroundServiceStartNotAllowedException. NtfyBootReceiver starts this service
from exactly that receiver, so push has not been surviving a reboot on Android
15/16 at all. specialUse is not in the disallowed list.

specialUse requires a PROPERTY_SPECIAL_USE_FGS_SUBTYPE justification, which
Google reviews at submission. That is the correct trade here: the platform PRD
has store submission in progress, and remoteMessaging -- the only other
untimed candidate -- documents its use case as device-to-device message
continuity, so declaring it for a server connection would be a mis-declaration.

FOREGROUND_SERVICE_DATA_SYNC is deliberately retained: androidx WorkManager's
SystemForegroundService still declares dataSync for RNShare.

Also adds Service.onTimeout() as a backstop, and guards the boot receiver so a
refused start logs instead of crashing the process during boot.
mohlec merged commit 3742ff107d into ncw/main 2026-09-01 18:32:42 +00:00
mohlec deleted branch fix/ntfy-fgs-special-use 2026-09-01 18:32:43 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
slash/slash-mobile!22
No description provided.