Adds a SmsRelaySettingsActivity that edits the four config knobs
(enabled, base URL, device ID, sender whitelist) directly against the
`sms_relay_prefs` SharedPreferences file via a PreferenceFragmentCompat.
Also shows last-forward time and 24h failure count read-only.
Standalone (its own Activity), not folded into the existing
SettingsActivity, so this lands without touching the 1000-line monolith.
Launch with adb during bring-up:
adb shell am start -n io.heckel.ntfy/.sms.SmsRelaySettingsActivity
A future pass can link it from main_preferences.xml with
app:fragment="io.heckel.ntfy.sms.SmsRelaySettingsActivity$Fragment"
SmsRelayPreferences.whitelist
Storage format moved from StringSet to a single comma-separated
string so an EditTextPreference can edit it directly. External API
is unchanged — getter still returns Set<String>, whitespace and
empty entries stripped on read.
res/xml/sms_relay_preferences.xml
SwitchPreferenceCompat (enabled) + three EditTextPreferences
(base_url, device_id, whitelist) + two read-only summary entries
(last forward, failure count).
SmsRelaySettingsActivity / Fragment
Activity hosts the fragment; fragment loads the XML, points the
PreferenceManager at the `sms_relay_prefs` file, and refreshes the
read-only status entries on resume.
Manifest <activity> declaration goes with the broader pending manifest
commit alongside the SmsRelayReceiver registration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So centralcloud-ops can SEE that this phone is alive and the SMS relay
is working. Server side (POST /api/devices/heartbeat + /devices LiveView)
landed in centralcloud_ops commit 6a7f22e.
DeviceHeartbeatWorker
Periodic CoroutineWorker (15min, the WorkManager floor) that POSTs
sms_relay state + device_state to /api/devices/heartbeat.
sms_relay summary: enabled, whitelist_size, last_forward_at,
failure_count_24h.
device_state: battery_level, is_charging, network_type,
low_power_mode, dnd_active, ringer_mode.
DND read via NotificationManager.currentInterruptionFilter — works
on API 23+ without notification policy access permission.
Skips (Result.success) when relay isn't configured yet — no point
spamming the server, but the schedule stays alive so it picks up
automatically as soon as config lands.
SmsRelayInit
One-liner bootstrap. Call from Application.onCreate when your
managed-config / branding WIP merges:
SmsRelayInit.start(this)
Idempotent (WorkManager KEEP policy) so safe across cold starts.
SmsRelayPreferences (extended)
Tracks lastForwardAtMs (epoch-ms, set by SmsForwardWorker on 200)
and failureCount24h (rolling, reset by heartbeat on success).
SmsForwardWorker (extended)
On 200 → record lastForwardAtMs. On 4xx/5xx/exception → bump
failureCount24h. Heartbeat then reports both up to the server,
which renders them on the /devices LiveView.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Whitelisted incoming SMS are forwarded to the centralcloud-ops
phone-relay endpoint as a redundant inbound path alongside Twilio.
io.heckel.ntfy.sms.SmsRelayPreferences
SharedPreferences-backed config — enabled flag, base URL, device id,
sender whitelist (Set<String>). Strict opt-in: forward nothing unless
explicitly enabled AND a non-empty whitelist matches the sender.
io.heckel.ntfy.sms.SmsRelayReceiver
BroadcastReceiver bound to SMS_RECEIVED_ACTION. Extracts the sender +
concatenated body of multi-part SMS, applies the whitelist, and
enqueues a OneTimeWorkRequest for forwarding. Wraps everything in
try/catch so a failure on this hot path can never crash the device's
SMS handling.
io.heckel.ntfy.sms.SmsForwardWorker
CoroutineWorker that POSTs to {baseUrl}/api/sms/inbound/phone-relay
with X-Device-Id auth. Uses HttpUtil.defaultClient and JSONObject
(no new deps). 4xx -> permanent drop, 5xx/network -> WorkManager
exponential backoff retry. WorkManager constraints require a
connected network so retries don't fire while offline.
Manifest registration (uses-permission RECEIVE_SMS / READ_SMS, receiver
declaration) is left as WIP in the working tree pending the broader
managed-config / branding changes also in progress on the manifest.
Commit those together (or in a follow-up) to make the receiver actually
bind.
No settings UI yet — set values via SharedPreferences directly or wait
for the Phase 2.1 settings screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>