[feat](client): deliver a trigger as a real alarm-clock takeover

A notification is not a delivery path: Android 14 restricts full-screen intents to apps it classifies as alarm apps, and setAlarmClock is both the strongest alarm API and the evidence for that classification. Schedule the takeover through AlarmManager instead of startActivity, keep a standing alarm-clock entry while armed, restore it after reboot (the wake also re-arms the socket), and persist the honest listening state so a platform stop is visible. Run the socket as specialUse on API 34+ (dataSync is capped at 6h/24h on 35+) and keep onTimeout as a guard. Restore the Lead release build placeholder so a plain-HTTP instance is reachable, and allow the app to arm only when exact alarms and full-screen intent are actually granted.
This commit is contained in:
2026-10-19 02:34:02 +01:00
parent 5fa1e8e83e
commit a804223139
12 changed files with 959 additions and 109 deletions
@@ -15,14 +15,17 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate
/**
* The full-screen alarm takeover.
*
* Launched by a full-screen intent from the alarm notification, so it appears
* over the lock screen and turns the screen on. It reuses the main JS bundle
* and is pointed at the `LiveAlarm` route via the intent extras, so the takeover
* is the same React screen whether the app was foreground, background or dead.
* Launched by the `PendingIntent` of an `AlarmManager.setAlarmClock` entry (see
* [AlarmScheduler]), so it appears over the lock screen and turns the screen on
* — an alarm launch, not a notification tap. It reuses the main JS bundle and is
* pointed at the `LiveAlarm` route via the intent extras, so the takeover is the
* same React screen whether the app was foreground, background or dead.
*
* Manifest requirements that make this work (see AndroidManifest.xml):
* `launchMode=singleTask`, `excludeFromRecents=true`, `showWhenLocked` and
* `turnScreenOn` on the activity, and `USE_FULL_SCREEN_INTENT`.
* `turnScreenOn` on the activity. `USE_FULL_SCREEN_INTENT` is still declared
* because it is the permission the platform uses to decide whether this app may
* take over the screen at all.
*/
class AlarmActivity : ReactActivity() {
@@ -71,9 +74,8 @@ class AlarmActivity : ReactActivity() {
companion object {
const val EXTRA_ALERT_ID = "alarm.alertId"
const val EXTRA_MESSAGE = "alarm.message"
const val EXTRA_AUTO_STOP = "alarm.autoStopSeconds"
fun intent(context: Context, alertId: String, message: String, autoStopSeconds: Int): Intent =
fun intent(context: Context, alertId: String, message: String): Intent =
Intent(context, AlarmActivity::class.java).apply {
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
@@ -83,7 +85,6 @@ class AlarmActivity : ReactActivity() {
)
putExtra(EXTRA_ALERT_ID, alertId)
putExtra(EXTRA_MESSAGE, message)
putExtra(EXTRA_AUTO_STOP, autoStopSeconds)
}
/** The alert id this launch is for, if any — read by the JS bridge. */