fix(push): hold the ntfy connection as specialUse and restart it on boot
All checks were successful
PR Checks / android-build (pull_request) Successful in 24m52s

PushService was a dataSync foreground service. Android 15 stops dataSync
after six hours a day and expects the service to stop itself on
onTimeout; it never did, so with target 36 the app would crash. A held-open
push connection is not a transfer either, which is what Play reserves
dataSync for.

- The service is now specialUse, with the PROPERTY_SPECIAL_USE_FGS_SUBTYPE
  reason Play reviews (ntfy does the same on Play). No daily limit.
- PushBootReceiver restarts it after BOOT_COMPLETED and MY_PACKAGE_REPLACED
  when push was on and a registration is saved (a sign-out clears it). It
  starts the service from the saved topic instead of re-registering, since
  the network is often not up at boot.

PLAN.md M8, decided by the org lead 2026-10-29.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-29 07:47:43 -05:00
parent ac60427a3e
commit 690441a8c3
4 changed files with 137 additions and 6 deletions

View File

@@ -8,10 +8,14 @@
<!-- Opt-in push notifications (M7): the runtime notification permission (API 33+)
and a foreground service that holds the persistent ntfy connection open — the
embedded UnifiedPush distributor, so no separate app is needed (PLAN.md §11). -->
embedded UnifiedPush distributor, so no separate app is needed (PLAN.md §11).
specialUse, not dataSync (PLAN.md M8, 2026-10-29): a held-open connection is not a
transfer, and Android 15 stops dataSync after six hours a day. RECEIVE_BOOT_COMPLETED
restarts it after a reboot or an update. -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".RunicGatewayApp"
@@ -70,11 +74,28 @@
</activity>
<!-- The embedded distributor's persistent ntfy connection (M7, PLAN.md §11).
dataSync foreground type; not exported — started only by PushManager. -->
specialUse foreground type, with the reason Play reviews in the property below;
not exported — started only by PushManager and PushBootReceiver. -->
<service
android:name=".core.push.PushService"
android:exported="false"
android:foregroundServiceType="dataSync" />
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Instant push notifications without Google services. When the user turns notifications on, this service holds one connection open to the push relay their community's server names, and shows a notification for each message from it. The ongoing notification shows the service is running; turning notifications off or signing out stops it." />
</service>
<!-- Restarts push after a reboot or an app update, if the member had it on and was
signed in when the phone went down (PLAN.md M8). Not exported: only the system
delivers these broadcasts. -->
<receiver
android:name=".core.push.PushBootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application>
</manifest>