Files
Android-app/app/src/main/AndroidManifest.xml
wtclaude 690441a8c3
All checks were successful
PR Checks / android-build (pull_request) Successful in 24m52s
fix(push): hold the ntfy connection as specialUse and restart it on boot
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
2026-10-29 07:47:43 -05:00

102 lines
5.5 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The app is purely an HTTPS API client of a shard's website backend. -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 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).
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_SPECIAL_USE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".RunicGatewayApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RunicGateway">
<!-- singleTop so the SSO Custom Tab returning via the deep link reuses the
running task (onNewIntent) instead of stacking a second activity. -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.RunicGateway">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Native SSO callback (M9, PLAN.md §4.2). The bridge deep-links the
one-time authorization code back to this fixed, app-owned custom
scheme; it must match SsoAuthManager.REDIRECT_URI and the backend's
MOBILE_AUTH_REDIRECT_URIS allowlist exactly. This is the permanent
fallback on every build (docs/android/APP_LINKS.md). -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="runicgateway"
android:host="auth"
android:path="/callback" />
</intent-filter>
<!-- App Links hardening (docs/android/APP_LINKS.md): a verified https
callback that only the domain's real owner can claim. autoVerify
needs a literal host, so ${appLinkHost} is baked at build time
(build.gradle.kts). The generic build leaves it as the reserved
runic-gateway.invalid sentinel — the filter then matches no real
link and never verifies. A white-label build sets -PappLinkHost. -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="${appLinkHost}"
android:path="/mobile/callback" />
</intent-filter>
</activity>
<!-- The embedded distributor's persistent ntfy connection (M7, PLAN.md §11).
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="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>