· 3 min read

An emergency button that keeps trying without internet

by · Project: The Finger of Hope

#android #java #flutter #typescript

The Finger of Hope is an emergency alert app. You pair with up to three trusted people. When something goes wrong, one press sends them an alert with your location, and afterwards you can send an "I'm safe" update. The hard requirement is that the alert has to try every route available, including when there is no internet.

There are three codebases: a native Java app built on Huawei Mobile Services (for Huawei phones without Google services), a Flutter app for Google devices, and a TypeScript server.

A state machine for getting the message out

Sending is modeled as a state machine with a try state and a wait state for each transport:

  1. The server over HTTPS, which delivers push notifications to the contacts.
  2. SMS, a compact text format that fits in one 160-character message.
  3. Wi-Fi Direct, phone to phone.
  4. Bluetooth Low Energy, including relaying through other phones running the app.
  5. Huawei Nearby, on Huawei devices.

Each wait has its own timeout: a minute for the server and 10 to 30 seconds for the local transports. A timeout moves the machine to the next route. There is also an offline-only mode for when the device knows it has no connection. It goes straight to Wi-Fi Direct and Bluetooth, retrying with growing delays (plus a little randomness, so nearby phones don't collide) under an overall two-minute limit.

A Bluetooth or Wi-Fi send only counts as delivered when the recipient sends back a delivery report within 20 seconds. Relaying phones pass that report back toward the sender.

Fitting messages into small pipes

Transports have very different limits, so messages are split into fragments: 512 bytes for Bluetooth and 64 KB for Wi-Fi Direct, with room reserved for a header. The receiver puts them back together with a time limit and a per-sender memory cap, so a misbehaving device can't fill another phone's memory with half-finished messages.

Relaying needs rules or a mesh turns into an echo chamber. Messages expire after five minutes and fifteen hops, duplicates are dropped within a time window, and devices that keep sending bad traffic are temporarily quarantined.

Keys on real Android devices

The encryption layer uses X25519 for key agreement, Ed25519 for signatures and AES-256-GCM for the payload, with keys kept in the Android Keystore. That plan meets reality on older Android versions: the Keystore only supports those key types from Android 13 (API 33), and on Android 10 asking for key agreement fails outright. The app detects this and falls back to P-256 keys, held in encrypted storage on devices where the Keystore can't do the job.

Other things Android taught me

  • Timers on their own thread. Some manufacturers' power management stalls timers on the main thread, which is fatal for a state machine built on timeouts.
  • Foreground service timing. Fallback starts after a short delay so the foreground service can register first. Otherwise Android throws before the alert even begins.
  • Bluetooth quirks. The app reconnects on the generic GATT error 133, waits for the connection to negotiate a larger packet size before fragmenting, and reassembles 20-byte chunks into length-prefixed frames.
  • "Connected" isn't online. Connectivity checks look at whether Android validated the network and whether there's a captive portal, then try a real connection before trusting the server route.

The server

The server is Express and TypeScript with PostgreSQL. It covers accounts, devices, pairing requests and accept/decline, location sharing over WebSockets, and notification delivery. Push notifications go through a Redis-backed job queue with retries and exponential backoff, and are sent through Firebase Cloud Messaging or Huawei Push Kit depending on the recipient's phone. Docker Compose brings up the API, a worker, PostgreSQL, Redis, Prometheus and Grafana.

Where it ended

I built the app for Huawei's Innovation Category competition. It placed 1st in 2025 and 2nd in 2026, and I stopped development when the 2026 competition ended. The core features were complete and working at that point: alerts, the transport chain, fragmentation, relaying and the server. The encryption layer was written but never connected to the send path, showing nearby hospitals and police stations stayed a plan, and the Flutter app carries a trimmed version of the offline stack. Source: Huawei app, Android app, server.