Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 55 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,62 @@ English | [Tiếng Việt](README.vi.md)

# handlive-android

The HandLive app for Android phones, written in Kotlin (minSdk 29). The app runs continuity features, such as Handoff, on Android. The phone stays in sync with a Mac, iPhone and iPad, and those Apple devices sync back.
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)

Kotlin, Gradle KTS, AGP 9.4, Kotlin 2.4, compileSdk 37 / targetSdk 35, Jetpack Compose. The UI is multilingual: English by default, Vietnamese as the second language (strings come from `../shared/strings`, detailed design 0.12). Specification: `../docs/detailed-design/` (hub repository). Plan: `../plans/20260925-implementation/`.
The HandLive companion service and application for Android phones, written in Kotlin with Jetpack Compose (minSdk 29, compileSdk 37). It connects Android with macOS, iOS, and iPadOS devices to provide two-way clipboard sync, an SMS bridge, and call notifications with action controls.

This repository is one part of the HandLive workspace: the hub repository `handlive` (docs, plans) is the parent directory, and `../shared` is the `handlive-shared` repository (test vectors, schemas, design tokens); the `core/design` tests also read the hub's `../docs/design-system`. Clone the whole set from the hub with `tools/workspace.sh clone <group-url>`. See this repository's `CLAUDE.md`.
## Overview

- **Cross-platform continuity:** Seamlessly syncs clipboard, bridges SMS messages, and displays call notifications across your devices.
- **End-to-end encrypted:** All data transferred over local Wi-Fi or through the relay is protected with XChaCha20-Poly1305 and X25519 session keys.
- **Two build flavors:**
- `foss` (default): Fully open-source build without Google Play Services or Firebase dependencies; connects directly over LAN or via WebSocket relay.
- `gms`: Adds Firebase Cloud Messaging (FCM) high-priority data wake-ups.
- **Multilingual UI:** English by default, Vietnamese as second language, with localized strings generated from `handlive-shared`.
- **Call integration without `InCallService`:** Uses `TelecomManager` for actions (Answer, Decline, End) and system telephony listeners without invasive call-handling services. Call audio on the computer is not supported (Phase 4 stopped).

## Workspace context

This repository is one component of the HandLive workspace:
- Hub repository [`handlive`](https://github.com/HandLive/handlive) (docs, plans) is the parent directory.
- [`handlive-shared`](https://github.com/HandLive/handlive-shared) (`../shared`) supplies shared protocol schemas, test vectors, and UI strings.
- Clone the full workspace from the hub using `tools/workspace.sh clone <group-url>`. See [`CLAUDE.md`](CLAUDE.md) for local workspace workflows.

## Layout

| Path | Contents |
|------|----------|
| `app/` | Compose app, package `app.handlive.android`: first-run setup (SET-01), the Devices and Settings tabs with pairing, device details (clipboard, SMS and calls per device), the Accessibility disclosure, the SMS and Calls switches with their permission primers, Calls from Other Apps with the Notification access primer, Permissions & Background, and Remove Device from Server / Delete All HandLive Data; installs the features at process start. Two flavors: `foss` (default, no Play Services or Firebase) and `gms` (FCM wake-ups, see Build settings) |
| `buildSrc/` | Task `:core:design:generateHandLiveTheme` generates `HandLiveTheme` from `../shared/design-tokens/tokens.json`; `:core:strings:generateStringResources` generates the string resources from `../shared/strings/ui-strings.json` |
| `core/protocol` | Envelope, Payload, Ack, `ErrorCode` (0.8.1), HL frames, clipboard chunks, SMS and `call_event` messages, relay wrappers, control ops, `HR` frames and REST bodies, UUIDv7, b64/b64u; test fixtures read `../shared` and validate against its JSON schemas |
| `core/crypto` | Tink XChaCha20-Poly1305, X25519, Ed25519, HKDF, `device_id`, PRK, session/rekey/stream key schedule, `K_push` and push envelopes, `hl_master` key store |
| `core/transport` | Ktor/Netty WSS (TLS 1.3, self-signed P-256 certificate), server-side handshake, capability, rekey, session replacement (4409), admission limits and idle close (4410/4411/4429); the relay client on OkHttp with certificate pinning (registration, JWT, REST, `/v1/relay`) and `/v1/ctl` sessions carried over the relay; instrumented Netty + TLS smoke test in `src/androidTest` |
| `core/data` | Room `handlive.db` version 3 (`paired_device`, `sms_observer_state`, `push_outbox` with the `reason` of each queued push; schemas in `core/data/schemas`), DataStore settings keys (0.9.5), `HandLiveData` container |
| `core/strings` | String resources generated at build time from the catalog (English default, Vietnamese); tests match them to the catalog and forbid hard-coded UI text |
| `core/design` | `HandLiveTheme` (4 appearances, Inter / Be Vietnam Pro / Roboto Mono), `HLButton`, `HLSwitch`, `HLGroupedList`, `HLStatusIndicator`, `HLIcon` (Material Symbols Rounded) |
| `feature/connection` | `HandLiveService` (foreground service `connectedDevice`), TLS server on 47800–47809 with `/v1/pair`, mDNS with hourly hints, capability updates (clipboard, SMS, calls, relay, missing permissions), envelope routing, `HLBENCH/1` lines in debug builds |
| `feature/pairing` | Pairing by QR code (CameraX + ZXing core) or PIN (Argon2id), device list and Security Code, unpairing |
| `feature/clipboard` | Clipboard sync: copy detection through Accessibility, `ClipboardReadActivity`, Send Clipboard button, Quick Settings tile and Share target, writes and forwarding, chunked images, safe auto-clear |
| `feature/sms` | SMS bridge: `ContentObserver` on the SMS provider (no `RECEIVE_SMS`), `sms/new`, paged `sms/sync` and `sms/history`, sending with `SmsManager` (SIM choice, multipart, `SendRegistry`, forward-only statuses), `sms/read_changed`, the permission suggestion notification, `HLBENCH/1` SMS events |
| `feature/call` | Calls without `InCallService` (C12): the call state listeners (`TelephonyCallback` / `PhoneStateListener`, default and per SIM) and the `PHONE_STATE` receiver feed the call context (`call_id`, waiting calls, late numbers, SIM labels, rebuilt midway); `call_event/state` per session with its `controls`; `answer`, `reject`, `end` through `TelecomManager` (hold, DTMF, mute answer `CALL_HFP_REQUIRED`); `call_event/log_sync` pages and `log_new` from a `ContentObserver` on the call log, with the `end_reason` correction; the incoming and missed calls handed to the relay for iPhone and iPad without a session; `HLBENCH/1` call events; calls of other apps (CALL-05, `appcall/`): a `NotificationListenerService` reads only call notifications (`CallStyle` or an ongoing notification linked to a ringing call within 3 s), keeps their contexts in memory, sends `call_event/app_call` to the sessions with `features.call.app_calls` in effect and carries out `answer`, `reject`, `end` with the app's own PendingIntents (a "tap to answer" notification without the background-start exemption) |
| `feature/relay` | The phone on the relay: `/v1/relay` only while a client may be waiting there (idle close after 5 minutes, `RECONNECT_BACKOFF`; held open while a call rings), device, pair and push token registration, remote revocation, pairing through a rendezvous, alert pushes for iPhone and iPad sealed with `K_push` (new SMS, incoming and missed calls) with the `push_outbox` retry queue, and the device deletion of SET-02 |
| `config/` | ktlint, detekt |
| `app/` | Compose app (`app.handlive.android`): first-run setup (SET-01), Devices and Settings tabs, pairing flows, per-device details, Accessibility disclosures, SMS and Calls permission primers, background execution settings, and device deregistration. Flavors: `foss` (default) and `gms` |
| `buildSrc/` | Code generation tasks: `:core:design:generateHandLiveTheme` from `../shared/design-tokens/tokens.json`; `:core:strings:generateStringResources` from `../shared/strings/ui-strings.json` |
| `core/protocol` | Wire formats: Envelope, Payload, Ack, `ErrorCode` (0.8.1), HL frames, clipboard chunks, SMS and `call_event` messages, relay wrappers, control ops, `HR` frames, REST bodies, UUIDv7, Base64/Base64URL; validates against schemas in `../shared` |
| `core/crypto` | Cryptographic engine: Tink XChaCha20-Poly1305, X25519, Ed25519, HKDF, `device_id`, PRK, session/rekey/stream key schedules, `K_push`, and `hl_master` KeyStore storage |
| `core/transport` | Embedded Ktor/Netty WSS server (TLS 1.3, self-signed P-256 certificate), handshake, capability exchange, session management (4409 replacement, 4410/4411/4429 idle/limits); OkHttp relay client with certificate pinning |
| `core/data` | Room database `handlive.db` v3 (`paired_device`, `sms_observer_state`, `push_outbox`), DataStore settings keys (0.9.5), and `HandLiveData` container |
| `core/strings` | Build-time string resources generated from the catalog; unit tests enforce catalog coverage and prevent hardcoded UI text |
| `core/design` | `HandLiveTheme` (4 appearance variants, Inter / Be Vietnam Pro / Roboto Mono typography), `HLButton`, `HLSwitch`, `HLGroupedList`, `HLStatusIndicator`, `HLIcon` |
| `feature/connection` | `HandLiveService` (foreground service), TLS server on ports 47800–47809, mDNS discovery with hourly hints, capability synchronization, envelope routing, and `HLBENCH/1` logging in debug builds |
| `feature/pairing` | Pairing via QR code (CameraX + ZXing) or PIN (Argon2id), paired device list, Security Code verification, and unpairing |
| `feature/clipboard` | Two-way clipboard synchronization: Accessibility copy detection, `ClipboardReadActivity`, Quick Settings tile, Share target, chunked image transfer, and safe auto-clear |
| `feature/sms` | SMS bridge: `ContentObserver` on SMS provider (no `RECEIVE_SMS`), `sms/new`, paged sync (`sms/sync`, `sms/history`), multi-part sending via `SmsManager`, forward-only status updates, and read state sync |
| `feature/call` | Call monitoring and controls: telephony listeners and `PHONE_STATE` receiver feeding call state contexts; `answer`, `reject`, `end` via `TelecomManager`; call log sync via `ContentObserver`; alert pushes for incoming and missed calls; app call support (`appcall/`) |
| `feature/relay` | Relay client: on-demand `/v1/relay` connection, device registration, pairing rendezvous, and alert push triggers (`K_push`) with `push_outbox` retry queue |
| `config/` | Linter configs: ktlint, detekt |

## Requirements

- **JDK:** Version 21 (e.g. OpenJDK 21)
- **Android SDK:** Compile SDK 37 (`platforms;android-37.0`), Min SDK 29, Target SDK 35
- **Build tools:** Android Gradle Plugin (AGP) 9.4 with Kotlin 2.4

## Build settings

Nothing secret is in git; every setting is a Gradle property (`~/.gradle/gradle.properties` or `-P`) or an environment variable, and the build succeeds without them (CI sets none):
Configuration uses Gradle properties (`~/.gradle/gradle.properties` or `-P`) or environment variables. All settings are optional; the project builds clean with defaults:

| Gradle property | Environment variable | Use |
|-----------------|----------------------|-----|
| `handlive.relayHost` | `HANDLIVE_RELAY_HOST` | The relay host (CONN-03); empty = a build without the relay |
| `handlive.relayExtraPins` | `HANDLIVE_RELAY_EXTRA_PINS` | Extra `sha256/<base64>` certificate pins, comma-separated, next to ISRG Root X1 and X2 in the code |
| `handlive.fcm.applicationId`, `.apiKey`, `.projectId`, `.senderId` | `HANDLIVE_FCM_APPLICATION_ID`, `_API_KEY`, `_PROJECT_ID`, `_SENDER_ID` | `gms` flavor only: the Firebase options for FCM wake-ups (no `google-services.json`); empty = no push |
| Gradle property | Environment variable | Description |
|-----------------|----------------------|-------------|
| `handlive.relayHost` | `HANDLIVE_RELAY_HOST` | Relay server hostname (CONN-03); empty builds without relay support |
| `handlive.relayExtraPins` | `HANDLIVE_RELAY_EXTRA_PINS` | Additional `sha256/<base64>` certificate pins, comma-separated, complementing ISRG Root X1/X2 |
| `handlive.fcm.applicationId`, `.apiKey`, `.projectId`, `.senderId` | `HANDLIVE_FCM_APPLICATION_ID`, `_API_KEY`, `_PROJECT_ID`, `_SENDER_ID` | `gms` flavor only: Firebase credentials for FCM wake-up pushes; empty disables push |

## Commands

Expand All @@ -48,6 +69,15 @@ HL_WRITE_ROUNDTRIP=1 ./gradlew :core:crypto:test # rewrites ../shared/test-vec
adb logcat -s HLBENCH # debug builds: benchmark lines for ../shared/tools/bench
```

## Documentation and community

- [System Architecture](https://github.com/HandLive/handlive/blob/main/docs/system-architecture.md) — System design and security model
- [Detailed Design](https://github.com/HandLive/handlive/tree/main/docs/detailed-design) — Protocols, formats, and specifications
- [Deployment Guide](https://github.com/HandLive/handlive/blob/main/docs/deployment-guide.md) — Packaging and distribution guidelines
- [CONTRIBUTING](https://github.com/HandLive/.github/blob/main/CONTRIBUTING.md) — PR workflow, small commits, and DCO sign-off (`git commit -s`)
- [SECURITY](https://github.com/HandLive/.github/blob/main/SECURITY.md) — Vulnerability reporting policy
- [CODE_OF_CONDUCT](https://github.com/HandLive/.github/blob/main/CODE_OF_CONDUCT.md) — Community standards

## License

Apache License 2.0 — see [LICENSE](LICENSE); bundled fonts and icons keep their own licenses, listed in [NOTICE](NOTICE). Contributions follow [CONTRIBUTING](https://github.com/HandLive/.github/blob/main/CONTRIBUTING.md) (small commits under a real name, DCO sign-off with `git commit -s`); report vulnerabilities privately as described in [SECURITY](https://github.com/HandLive/.github/blob/main/SECURITY.md).
Apache License 2.0 — see [LICENSE](LICENSE). Bundled fonts and icons retain their respective upstream licenses, listed in [NOTICE](NOTICE).
Loading
Loading