fix(android): declare NtfyPushService specialUse, not dataSync #22
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/ntfy-fgs-special-use"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #18.
Declares
NtfyPushServiceasspecialUseinstead ofdataSync, adds aService.onTimeout()backstop, and guards the boot receiver.Why
Android 15 put a 6-hour-per-24-hour budget on
dataSyncforeground services and kills theprocess when it runs out. This was 26 of the last 40 crash reports -- the largest single
crash source in the field.
dataSyncwas also wrong on the merits: this service holds a long-lived SSE subscription to aself-hosted push relay, which is not a bounded data transfer. Only
dataSyncandmediaProcessingcarry a time budget.Second bug fixed by the same change
Android 15+ forbids
BOOT_COMPLETEDreceivers from startingdataSyncservices, throwingForegroundServiceStartNotAllowedException.NtfyBootReceiverdoes exactly that, so push hasnot been surviving a reboot on Android 15/16 at all.
specialUseis not in the disallowedlist, 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
specialUserequires aPROPERTY_SPECIAL_USE_FGS_SUBTYPEjustification that Google reviews atsubmission. That is the right trade rather than something to design around:
regardless.
remoteMessaging, the only other untimed candidate, documents its use case as "Transfer textmessages 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.
FOREGROUND_SERVICE_SPECIAL_USEbecause removing a library's foregroundservice 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_SYNCis deliberately retained -- androidx WorkManager'sSystemForegroundServicestill declaresdataSyncfor 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 24guard is needed.try/catchis defence in depth: an uncaught throw there crashes the processduring boot, which is the same failure family this PR is closing.
Verification
Static: manifest parses,
foregroundServiceTypeisspecialUse, the<property>child ispresent, and both permissions are declared as intended.
Not yet verified on device. Needs a build plus:
ForegroundServiceDidNotStopInTimeExceptionand that push still arrives.arrives (this is the boot-path fix).
After the upstream rebase
Android 16 adds
onTimeout(int startId, int fgsType)alongside the single-argument form. Oncethe 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.