The official Android app is available on Google Play and, for sideloading, as a signed standalone APK on selected GitHub Releases. Not every release includes the APK and checksum. See Install outside Google Play to find and verify both files. It is a companion node and requires a running Apus Gateway. Source: apps/android (build instructions).

Support snapshot

Its reply field switches to masked input for secret prompts. Tap it again if a prompt change closes the keyboard. Android sends sensitive replies without trimming them and clears unsent drafts when you leave this page or background the app.

New replies stay in view while you are at the end of the settings conversation. Scroll up to read earlier steps without being pulled away, then tap Jump to latest to resume following. Following also resumes if resizing the window makes the whole conversation visible. Restart, when offered after an error, opens a new conversation at its latest reply.

System control (launchd/systemd) lives on the Gateway host — see Gateway.

Simultaneous Gateway sessions

Pair each Gateway once, then open Settings → Gateway. The checkmark marks the focused Gateway and each switch controls whether a non-focused Gateway's operator session stays connected. Enabled Gateways reconnect independently while the app is in the foreground, so switching focus does not tear down the others. The focused Gateway alone owns the Android node session and device capabilities; this prevents simultaneous Gateways from issuing camera, location, screen, or notification commands to the same phone. Android can suspend the secondary connections after the app leaves the foreground.

The sidebar defaults to Home → Threads → Skills → Overview. Existing personalized orders and pinned work pages are preserved; Pages pencil → Edit pinned items → Reset pinned items restores these defaults. The Settings gear beside the Gateway selector opens all settings, including while offline. Settings stays in the footer rather than the Pages menu or pin editor.

Use the pencil beside Pages to open Agents, Automations, Usage, Skills, Skill Workshop, Dreaming, Terminal, or Desktop (when available). Pin the pages you use often with Edit pinned items. These work pages also remain reachable through search; Settings focuses on this phone, connections, configuration, and diagnostics. Settings, sidebar, search, and detail headers share the Web UI's icon meanings. Connection and approval states are written out rather than represented by unlabeled green or gray dots.

The sidebar footer's Gateway selector opens Add Gateway when none are saved and Gateway settings when one is saved. With multiple saved Gateways, it opens a native quick picker with a checkmark for the focused route, Add Gateway, and Manage Gateways.

Add Gateway opens the QR scanner without disconnecting the current Gateway or restarting onboarding. You can also enter a setup code, choose a QR image, or enter a Gateway URL manually. A valid code opens a confirmation; only Connect starts the handoff. Cancel returns to the previous screen without changing the current conversation, drafts, attachments, or saved Gateways. Adding an already saved Gateway uses its existing connection settings; use Manage Gateways to replace its setup. Saved offline entries remain listed; connection status is separate from selection.

In Manage Gateways, tap Rename to choose a name used only on this phone. The name appears in the sidebar and picker and survives switching Gateways, reconnecting, app restarts, and discovery updates. The secondary address still distinguishes Gateways with the same name. Clear the name to restore the default. Renaming does not change the Gateway's address, identity, or saved credentials. Downgrading to an older Android build can discard these local names when that build starts and rewrites the registry. Gateway addresses and credentials are unaffected; after upgrading again, choose the local names again if needed.

Unsent text and finished attachments stay with their Gateway, agent, and session when you switch away and back. Finish recording, stop dictation or Talk, and let media imports or pending send admission finish before using the quick picker. The composer stays protected during handoff, but a committed offline Gateway remains usable without waiting for a network connection.

Dictation and attachments

Tap the composer microphone to dictate. The composer shows when recognition is starting, listening, or transcribing, with an interim transcript while you speak. Only the final transcript is added to your draft. Tap Stop to finish listening, or Cancel while starting or transcribing to discard that attempt. Recognition errors leave your existing draft intact and explain how to retry.

After picking or sharing photos and files, Preparing attachments… stays visible until they are ready. Send remains disabled during preparation. Queuing message… covers local send admission; the message's outbox status then shows delivery or any failure. Offline messages still use the durable queue.

Wear OS companion

The Wear OS companion uses the paired Android phone's authenticated Gateway connection; the watch never receives or stores Gateway credentials. It can select agents and sessions, read bounded transcripts, send text or dictated replies, abort an active run, start realtime Talk inside the selected session, and connect or disconnect the paired phone's Gateway. It also offers local reply notifications, dark or light appearance, and optional automatic speech for replies. Agent and Gateway controls are capability-negotiated for staggered phone/watch updates. Realtime Talk streams microphone and playback audio over a temporary Wear OS Data Layer channel and stops when the selected phone, Gateway connection, or audio channel is lost.

Install outside Google Play

Selected GitHub Releases include a universal Apus-Android.apk and Apus-Android-SHA256SUMS.txt. The APK is built from the release tag, signed with the Apus Android release key, and carries GitHub Actions provenance. Android assets may be attached after a release becomes public. Select a release by its listed assets, not by the latest Gateway release tag.

List published releases that contain both required assets:

gh api --paginate "repos/apus/apus/releases?per_page=50" \
  --jq '.[] | select(.draft | not) | {
    tag: .tag_name,
    apk: ([.assets[].name] | any(. == "Apus-Android.apk")),
    checksum: ([.assets[].name] | any(. == "Apus-Android-SHA256SUMS.txt"))
  } | select(.apk and .checksum)'

Pick a release that lists both assets, then download and verify that exact tag before sideloading:

release_tag=vYYYY.M.PATCH
gh release download "$release_tag" \
  --repo apus/apus \
  --pattern Apus-Android.apk \
  --pattern Apus-Android-SHA256SUMS.txt
sha256sum --check Apus-Android-SHA256SUMS.txt
gh attestation verify Apus-Android.apk \
  --repo apus/apus \
  --signer-workflow apus/apus/.github/workflows/android-release.yml \
  --source-ref "refs/tags/${release_tag}" \
  --deny-self-hosted-runners

If no release lists both files, use Google Play or build from source with your own signing identity.

Google Play and standalone APK installs use different update channels and may have different signing identities. Android may require uninstalling the existing app before switching channels, which removes its local app data. Stay on one channel for normal updates.

Building a release artifact (APK or app bundle) from source or a fork requires your own Android signing identity. Debug builds use an automatically generated debug signing key. The official Apus release key is not included in the repository. See Sign your app for how to generate and configure a signing key for release builds.

App and Gateway compatibility

The Android app and Gateway do not need matching release version numbers. Connection compatibility depends on the Gateway protocol versions they support, not the release tag that contains the APK.

Android uses separate connections for chat and UI actions (the operator connection) and device capabilities (the node connection). The operator connection must support the Gateway's current protocol. An authenticated node connection may use the previous protocol within the documented compatibility window; that exception does not extend to chat or UI actions. See Gateway protocol versioning for the current versions and node capability restrictions.

Successful pairing does not guarantee that every feature works across an arbitrary app/Gateway version combination. New features may require updates on both sides. If a connection reports protocol mismatch, check the protocol requirements and available updates for both the app and Gateway. For sideloaded apps, use the APK selection and verification steps above rather than assuming the latest Gateway release contains a newer app.

Mirror and control Android from a remote Mac

scrcpy mirrors an Android screen in a macOS window and forwards keyboard and pointer input through Android Debug Bridge (ADB). This is an operator-side workflow, separate from the Apus node connection. It is useful when the Android device and the Mac are in different locations but share a private Tailscale network.

Before you begin

Enable ADB over TCP

For the initial setup, connect the Android device by USB to a trusted computer and approve its debugging prompt. Then run:

adb devices
adb tcpip 5555

You can now disconnect USB. If port 5555 stops listening after a device reboot or debugging reset, repeat this local setup step. Android 11 and later can also establish the initial trust with Wireless debugging → Pair device with pairing code and adb pair.

Allow only the controller Mac

Tailnets with restrictive grants must explicitly allow the controller Mac to reach TCP port 5555 on the Android device. Add a narrow rule to the tailnet policy, replacing the example addresses with the two devices' stable Tailscale IPs:

{
  grants: [
    {
      src: ["<remote-mac-tailnet-ip>"],
      dst: ["<android-tailnet-ip>"],
      ip: ["tcp:5555"],
    },
  ],
}

See Tailscale grants for host aliases and other selectors. Do not grant this port to the public internet or expose it with Funnel: an authorized ADB client has broad control of the device.

Connect and start mirroring

On the remote Mac:

adb connect <android-tailnet-ip>:5555
adb devices
scrcpy --serial <android-tailnet-ip>:5555

The first adb connect from this Mac shows an authorization dialog on Android. Unlock the device, confirm the key fingerprint, and select Always allow from this computer only when the Mac is trusted. A successful adb devices entry ends in device; unauthorized means the on-device prompt has not been approved.

Once the scrcpy window opens, use it directly or target it with a macOS screen-automation tool such as Peekaboo. scrcpy carries the display and input; Tailscale provides only the private network path.

Troubleshooting

When finished, close scrcpy and disconnect ADB:

adb disconnect <android-tailnet-ip>:5555

Connection runbook

Android node app ⇄ (mDNS/NSD + WebSocket) ⇄ Gateway

Android connects directly to the Gateway WebSocket and uses device pairing (role: node).

For Tailscale or public hosts, Android requires a secure endpoint:

Prerequisites

1. Start the Gateway

Use an authenticated Gateway. If it is not configured yet, run apus onboard first to configure a token or password.

For a trusted same-LAN setup, persist the LAN bind before starting:

apus config set gateway.bind lan
apus gateway --port 18789

Bare-metal and virtual-machine hosts default to loopback, which a phone cannot reach. Detected containers can default to auto instead. Set the bind explicitly for this setup.

Use the config command rather than --bind lan alone: a startup-only flag does not change the configuration read by a separate apus qr command. Without another configured URL route, setup-code creation still sees loopback and refuses to mint a code.

Run apus gateway status. Its Gateway: line should show bind=lan (0.0.0.0) and port=18789.

For remote Android access, choose managed Tailscale Serve as an alternative to LAN binding. Keep its settings in config so setup-code creation can use the same route:

apus config set gateway.bind loopback
apus config set gateway.tailscale.mode serve
apus gateway --port 18789

Tailscale must be installed and logged in. Managed Serve and Funnel require loopback binding; do not leave gateway.bind=lan set when switching to them. See Tailscale for Serve and password-authenticated Funnel setup.

This gives Android a secure wss:// / https:// endpoint. A plain gateway.bind: "tailnet" setup is not enough for first-time remote Android pairing unless you also terminate TLS separately.

2. Verify discovery (optional)

From the Gateway machine:

dns-sd -B _apus-gw._tcp local.

More debugging notes: Bonjour.

If you also configured a wide-area discovery domain, compare against:

apus gateway discover --json

That shows local. plus the configured wide-area domain in one pass, using the resolved service endpoint instead of TXT-only hints.

Cross-network discovery via unicast DNS-SD

Android NSD/mDNS discovery does not cross networks. If the Android node and the Gateway are on different networks but connected via Tailscale, use Wide-Area Bonjour / unicast DNS-SD instead. Discovery alone is not sufficient for tailnet/public Android pairing — the discovered route still needs a secure endpoint (wss:// or Tailscale Serve):

  1. Set up a DNS-SD zone (example apus.internal.) on the Gateway host and publish _apus-gw._tcp records.
  2. Configure Tailscale split DNS for your chosen domain pointing at that DNS server.

Details and example CoreDNS config: Bonjour.

3. Connect from Android

Create a setup code in the Control UI (Devices → Pair device) or with apus qr.

That mobile setup code (and its QR) is what Android Scan QR or setup code / Enter setup code accept. It is a different artifact from the gateway join URL minted by apus devices join-code (https://…/j/<code>), which enrolls a headless node host via apus connect. Pasting a join URL or bare join code into Android setup is rejected — generate a fresh mobile QR/setup code with apus qr.

An explicit --url or --public-url override wins. Otherwise, setup-code URL selection uses this order:

  1. plugins.entries.device-pair.config.publicUrl, unless remote preference was requested.
  2. gateway.remote.url when explicitly preferred.
  3. Managed Tailscale Serve or Funnel.
  4. The ordinary gateway.remote.url setting.
  5. A usable configured bind, such as the LAN bind from step 1.

apus qr --remote selects remote credentials, ignores the configured device-pair publicUrl, and prefers gateway.remote.url before managed Tailscale. See QR.

URL selection does not test network reachability. Resolution errors stop setup-code creation instead of triggering a lower-priority route. A loopback-only Gateway with no configured URL or managed Tailscale route refuses to mint a code.

In the Android app:

Gateway tokens, bootstrap tokens, passwords, and setup codes are masked and accept paste. The app requests password input with autocorrection disabled, but cannot guarantee how a third-party keyboard stores or learns from input.

After the first successful pairing, Android auto-reconnects on launch to the active paired Gateway (best-effort for discovered Gateways, which must be visible on the network).

Android retries temporary connection losses automatically. For a fresh attempt with the saved endpoint, open Settings → Gateway and tap Reconnect. Disconnect stops the connections and suppresses automatic reconnect for the current app session; it does not forget the pairing. Authentication or pairing errors can pause retries until you address the reported problem.

Gateway summary rows wrap long labels and values. Tap Instance ID in Settings → Gateway to copy this phone's full identifier.

Official setup codes connect Android as a node and grant full Gateway operator access by default over wss://. Plaintext non-loopback ws:// setup automatically uses limited access for bearer-token safety. Settings → Gateway shows Full or Limited access. For a limited connection, configure wss:// or Tailscale Serve, generate a new full-access code in Control UI or with apus qr, then scan or paste it on that page and reconnect. Operators who want the reduced profile can select Limited access in Control UI or run apus qr --limited.

Manage paired Gateways

The app keeps a registry of every Gateway it has paired with, so you can keep operator sessions connected and change focus without pairing again:

Opening or replying to a conversation notification reconnects its saved Gateway when needed. An already connecting or connected target is retained. Replies wait for that target connection to become ready, including required TLS approval. If the target is no longer available, opening the notification shows Gateway unavailable and opens Gateway settings without disconnecting another Gateway. Disconnect is checked again before a notification reply enters the durable send queue; already queued input keeps its normal recovery behavior.

Reply queued confirms that the reply entered the durable send queue, not that it was delivered. The notification keeps a private preview of the submitted text and offers Open conversation. If the reply status is unknown, open the conversation to check before sending again; the notification does not offer another Reply action. Feedback updates only the latest notification for that conversation, so an older result cannot replace a newer notice. A notification posted before an app update can still send replies. Its result does not rewrite or dismiss that notification, so open the conversation to check its status.

The Channels, Dreaming, Health logs, Skills, and Usage pages keep their last loaded data while refreshing. A failed first load shows an error rather than empty counts or default health values. When refreshes overlap, only the latest request updates the page's data, error, and progress. Disconnecting clears the displayed summaries.

On Health, Chat: Not ready means chat health is unconfirmed or its check failed; the Gateway can still be Online. The chat header's accessibility status uses the same readiness wording. Use Refresh chat in chat actions to check again; Refresh Logs only reloads logs. The Overview's Gateway card reports connection status and highlights known issues, not overall system health.

Presence alive beacons

After the authenticated node session connects, and when the app moves to the background while the foreground service is still connected, Android calls node.event with event: "node.presence.alive". The Gateway records this as lastSeenAtMs/lastSeenReason on the paired node/device metadata only after the authenticated node device identity is known.

The app counts the beacon as successfully recorded only when the Gateway response includes handled: true. A Gateway that acknowledges node.event with { "ok": true } and no handled field is compatible, but that response does not count as a durable last-seen update.

4. Approve pairing (CLI)

On the Gateway machine:

apus devices list
apus devices approve <requestId>
apus devices reject <requestId>

Pairing details: Pairing.

Optional: if the Android node always connects from a tightly controlled subnet, you can opt in to first-time node auto-approval with explicit CIDRs or exact IPs:

{
  gateway: {
    nodes: {
      pairing: {
        autoApproveCidrs: ["192.168.1.0/24"],
      },
    },
  },
}

This is disabled by default. It applies only to fresh role: node pairing with no requested scopes. Operator/browser pairing and any role, scope, metadata, or public-key change still require manual approval.

5. Verify the node is connected

apus nodes status
apus gateway call node.list --params "{}"

6. Chat + history

The draft has its own full-width row above the attachment and voice/send controls, so larger text and narrow screens do not squeeze it between buttons. The empty hint stays on one line; drafts show up to six lines and scroll when space is limited. The composer has narrower side gutters than the transcript. +, model, and reasoning stay together on the left; the microphone and Talk/send stay on the right in one row. Controls remain 48dp tall; very short views use narrower icon buttons to make room for Details while retaining an editable line. The placeholder and typed text share the same alignment.

Open + for a compact icon list with Camera, Gallery, Files, Location, and Permissions. The Permissions row shows the current access mode. Open the top-right Chat actions (⋮) menu to see the live Context usage ring. Choose Context to open context usage, latest-run tokens, and the cost breakdown. Viewing usage does not require permission to change session settings. A reported model-call total remains visible when no cost breakdown is available. Missing usage is shown as unknown. Tap the model name to open a compact menu above the composer, search by model name, ID, or provider, and expand provider groups. Search accepts multiple terms and small typos in words of at least four letters, including swapped adjacent letters. Every term must match; short terms and version numbers are not typo-corrected. Exact matches rank first within each provider, and provider groups follow their best match. Clearing the search restores the usual ordering without changing your selection. The picker has no settings buttons. The Gateway's configured default is labeled on its model row. Selecting a named model pins that model to the session; Default model separately resets the override to follow the Gateway's current default. Pinned and recent models remain available. Long model names use a middle ellipsis, with the full name available in the picker. The effort dial opens its slider and Fast mode without expanding the composer. Dragging the slider previews the effort on the dial; releasing it applies the selection.

With an empty draft and no active run, the trailing button starts Talk. Entering text changes it to Send; an active run with no draft shows Stop. During Talk, the live waveform replaces the microphone and remains tappable to end Talk. If a run is also active, a separate, softly tinted Stop button stays at the trailing edge to abort that run.

Open Home from the sidebar's Pages menu to chat, or select an existing session from the sidebar:

Reactions

Saved prompts and assistant replies show emoji reaction chips with counts and reactor names, highlighting your own reactions. Tap a chip to toggle your reaction, or long-press a message (including a media-only message) and choose Add reaction. The quick palette matches the Control UI; More… accepts one emoji, including joined emoji and flags. Controls follow the Gateway's operator and session-sharing permissions; read-only viewers can still see reactions. Updates appear live while the conversation is open and do not create notifications. See Reactions.

Agent browser in chat

When the Browser plugin returns an identifiable tab, Chat shows a preview of the session's latest browser tab under Agent browser. Tap the upward chevron to interact without leaving the conversation or replacing your draft. The downward chevron or Android Back returns to the preview. Collapsing dismisses the browser's keyboard without changing your chat draft. Open in your browser is a separate, explicit action. Ordinary website links and Desktop keep their existing behavior.

The close icon removes the card from chat without closing the agent's remote tab. To restore it, choose Chat actions > Agent browser. Refreshing the same browser result does not reopen a dismissed card; a new browser-tool presentation can show it again. Closing is also available while the browser is offline or unavailable.

The preview uses the connected Gateway and the exact browser profile, host or node, and tab from the tool result. It never starts another browser or substitutes a different tab. Switching sessions or Gateways replaces the viewer; going offline removes its controls until the connection returns. A stopped or closed remote tab stays unavailable rather than creating a replacement.

This uses the Gateway's existing Browser panel and operator.admin browser permission. The connected Gateway must advertise browser-focus support and use its bundled Control UI. Older Gateways, disabled Control UI, and custom UI roots show an unavailable notice instead of loading an unsupported page; update the Gateway and use its bundled UI to enable embedded control. The card remains closable. No additional browser service or session-sharing permission is created.

7. Camera

Camera commands (foreground only; permission-gated): camera.snap (jpg), camera.clip (mp4). See Camera node for parameters and CLI helpers.

8. Voice + expanded Android command surface

9. Workspace files (read-only)

Open Overview from the sidebar's Pages menu to find the Files card. It browses the active agent's workspace through the read-only agents.workspace.list / agents.workspace.get Gateway RPCs: directory drill-down, text and image previews, and export through the Android share sheet. There are no write operations, and previews are size-capped by the Gateway.

If the app cannot prepare a file or open the share sheet, it shows Could not share file and keeps the preview open so you can retry or go back.

Review command approvals

An operator connection with operator.admin, or a paired operator.approvals connection explicitly targeted by the Gateway, can review pending exec requests under Settings -> Approvals. The app loads the Gateway's sanitized approval record before enabling its buttons, shows any security warning and the exact decisions offered by that request, and submits the approval ID and owner kind back to the Gateway.

Approval state is shared with the Control UI and supported chat surfaces. The first committed answer wins; Android displays that canonical result even when another surface answered first. If a resolve response is lost or the Gateway disconnects, the app keeps the action locked and reads the approval again before offering another decision.

Gateways that predate the unified approval methods fall back to the shipped exec-specific methods. Pending review still works, but retained terminal state and the richer cross-surface result require an updated Gateway.

Answer agent questions

Chat shows pending Gateway questions as native cards for operator connections with operator.questions (or operator.admin). Cards support single- and multi-select options, option descriptions, free-text Other answers, and an expiry countdown. Reconnects reload pending questions from the Gateway. A card locks when this device answers it, another surface answers it first, or the question expires or is cancelled.

Secret answer fields mask typed or pasted values and request password input with autocorrection disabled. Android submits secret answers without trimming leading or trailing whitespace.

Assistant entrypoints

Android supports launching Apus from the system assistant trigger (Google Assistant). Holding the home button (or another ACTION_ASSIST trigger) opens the app; saying "Hey Google, ask Apus <prompt>" matches the app's declared App Actions query pattern and hands the prompt into the chat composer without auto-sending it.

This uses Android App Actions (shortcuts.xml capability) declared in the app manifest. No gateway-side configuration is needed — the assistant intent is handled entirely by the Android app.

App Actions availability depends on the device, Google Play Services version, and whether the user has set Apus as the default assistant app.

Notification forwarding

Android can forward device notifications to the Gateway as node.event items. This is configured on the device, in the app's Settings sheet — not in Gateway/apus.json config.

Setting Description
Forward Notification Events Master toggle. Off by default; requires Notification Listener Access to be granted first.
Package Filter Allowlist (only listed package IDs forwarded) or Blocklist (default: all packages except listed IDs). Apus's own package is always excluded in Blocklist mode to prevent forwarding loops.
Quiet Hours Local HH:mm start/end window that suppresses forwarding. Disabled by default; defaults to 22:00-07:00 once enabled.
Max Events / Minute Per-device rate limit on forwarded notifications. Default 20.
Route Session Key Optional. Pins forwarded notification events into a specific session instead of the device's default notification route.

Notification forwarding requires the Android Notification Listener permission. The app prompts for this during setup.

WhatsApp, WhatsApp Business, Telegram, Telegram X, Discord, and Signal notifications are always excluded. Their messages are already owned by native Apus channel sessions; forwarding the Android notification as a separate node event could route a reply through the wrong conversation.

Related