API Reference

HTTP Routes

PlainApp exposes a small set of HTTP endpoints alongside the GraphQL API at POST /graphql. These endpoints handle file serving, multipart uploads, zip streaming, DLNA casting, WebSocket events, and system control. All file/upload/zip endpoints require the c-id header (and usually an encrypted id query parameter).

Type Definitions

bash
# Common headers for protected HTTP endpoints:
c-id: <client-id>
# /fs, /proxyfs, /zip/* use an encrypted "id" query parameter produced
# by the GraphQL files()/fileIds() queries. The id is opaque to the client.

# Upload endpoints additionally require an encrypted "info" multipart part:
#   info = chaCha20Encrypt(token, json({ dir, size, replace, isAppFile }))
#   file = <raw bytes>
# Chunked uploads use:
#   info = chaCha20Encrypt(token, json({ fileId, index, size }))

# Status codes follow HTTP semantics:
#   200 OK              – success
#   201 Created         – upload stored
#   204 No Content      – init pending password
#   400 Bad Request     – missing/invalid params
#   401 Unauthorized    – bad token
#   403 Forbidden       – expired/invalid encrypted id
#   404 Not Found       – file/entry missing
#   410 Gone            – server shutting down
#   429 Too Many Requests – concurrent zip limit hit

Operations

http

POST /graphql

Main GraphQL endpoint. Send all GraphQL queries and mutations here with c-id + Authorization headers. The /peer_graphql endpoint is the peer-to-peer equivalent used between paired devices.

bash
curl --request POST \
  --url http://192.168.1.100:8080/graphql \
  --header 'c-id: <client-id>' \
  --header 'Authorization: Bearer <api-token>' \
  --header 'Content-Type: application/json' \
  --data '{"query":"{ app { battery deviceName } }"}'
http

GET /fs

Serve a file, content:// URI, or package icon. The id query parameter is an encrypted string returned by files()/fileIds(). Supports thumbnail generation (?w=&h=&cc=), byte-range (?offset=&length=) for low-throughput transports, HEIF→PNG conversion, 3gp→MP4 transcoding, and download mode (dl=1) with a Content-Disposition attachment header.

bash
# Original (full size, inline)
curl http://192.168.1.100:8080/fs?id=<encrypted-id> -o file.jpg

# Thumbnail (center-cropped to 200x200)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&w=200&h=200' -o thumb.jpg

# Download (attachment)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&dl=1' -OJ

# Byte range (BLE chunked download)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&offset=0&length=4096' \
  -o chunk.bin
http

GET /proxyfs

Proxy a peer HTTP URL. The id query parameter decrypts to a full http(s) URL on a paired peer device; the server streams the upstream response back. Used for peer-to-peer file downloads over Wi-Fi Aware.

bash
curl http://192.168.1.100:8080/proxyfs?id=<encrypted-peer-url> -o peer-file.jpg
http

POST /upload

Upload a single file via multipart/form-data. The "info" part (ChaCha20-encrypted JSON) must precede the "file" part. When isAppFile=true the bytes are imported into the content-addressable AppFileStore (deduplicated by hash); otherwise the file is written to info.dir/<fileName>. Returns the final file name (may have a "(1)" suffix when avoiding overwrite).

bash
curl --request POST \
  --url http://192.168.1.100:8080/upload \
  --header 'c-id: <client-id>' \
  --form 'info=<encrypted-info-bytes>;type=application/octet-stream' \
  --form 'file=@/path/to/local.jpg'
http

POST /upload_chunk

Upload one chunk of a resumable, chunked upload. Each chunk is written to upload_tmp/{fileId}/chunk_{index} on disk. Returns "<index>:<savedSize>". After all chunks arrive, call the mergeChunks GraphQL mutation to assemble the final file.

bash
curl --request POST \
  --url http://192.168.1.100:8080/upload_chunk \
  --header 'c-id: <client-id>' \
  --form 'info=<encrypted-chunk-info-bytes>;type=application/octet-stream' \
  --form 'file=@/path/to/chunk_0.bin'
http

GET /zip/dir

Stream a single directory as a zip archive. The id query parameter decrypts to a directory path. Only one zip operation runs at a time on the device — concurrent requests get HTTP 429.

bash
curl 'http://192.168.1.100:8080/zip/dir?id=<encrypted-dir-id>' \
  -o folder.zip
http

GET /zip/files

Stream multiple files (or media search results) as a single zip. The id decrypts to { type, query, id, name }. For FILE type the file list is stored in TempHelper under request.id by the GraphQL files() query; for media types the server runs searchZipItems(type, query, id).

bash
curl 'http://192.168.1.100:8080/zip/files?id=<encrypted-request>' \
  -o selection.zip
http

GET /media/{id}

DLNA media endpoint. Serves a previously-registered media path (registered via UrlHelper.getMediaHttpUrl) to a TV / DLNA renderer. URL sources are proxied, content:// URIs are streamed, images served as-is, and all other files are served with DLNA-specific headers + HTTP 206 range support so renderers accept the stream.

bash
# Play on a DLNA renderer:
curl http://192.168.1.100:8080/media/<id>.mp4 -o video.mp4
http

NOTIFY /callback/cast

DLNA renderer callback. Receives the renderer's event NOTIFY XML and updates CastPlayer state. On TransportState=STOPPED (without AVTransportURIMetaData) the player auto-advances to the next playlist item. Also parses RelTime / TrackDuration for position updates.

bash
# Sent by the DLNA renderer (not by the client):
NOTIFY /callback/cast HTTP/1.1
Content-Type: text/xml

<?xml ...><e:propertyset>...TransportState val="PLAYING"...</e:propertyset>
http

GET /health

Unauthenticated health-check endpoint. Returns the app package name as plain text. Use this to verify the HTTP server is reachable.

bash
curl http://192.168.1.100:8080/health
http

GET /shutdown

Shut down the HTTP server. Only callable from localhost — remote requests get HTTP 403. Closes all WebSocket sessions, clears the online client set, and disposes the HTTP server.

bash
# Must be run on the device (e.g. via adb shell):
curl http://localhost:8080/shutdown
http

POST /init

Initialize a client session. Requires the c-id header. If a valid encrypted body is present (decrypted with the cached token) the session is considered authenticated and the server responds 200. Otherwise: if no password is set, the server responds with a freshly-reset password (200); if a password is set, responds 204 No Content and the client must authenticate via the WebSocket login flow.

bash
curl --request POST \
  --url http://192.168.1.100:8080/init \
  --header 'c-id: <client-id>' \
  --data-binary '<encrypted-token-bytes>'
http

WS /

Main WebSocket endpoint. Use ?cid=<client-id> to register a session; the first binary frame must be an encrypted timestamp (or, with ?auth=1, an encrypted AuthRequest containing the password). After authentication the server pushes encrypted event frames (notifications, chat updates, etc.) over this socket.

bash
# Browser / JS example
const ws = new WebSocket('ws://192.168.1.100:8080/?cid=<client-id>')
ws.binaryType = 'arraybuffer'
ws.send(encryptWithToken(token, Date.now().toString()))

// Login variant:
const ws = new WebSocket('ws://192.168.1.100:8080/?cid=<client-id>&auth=1')
ws.send(encryptWithPassword(password, JSON.stringify({ password: hash })))
http

WS /status

Peer-presence WebSocket. Use ?cid=<peer-id> to keep a paired peer marked online. The first binary frame must be a PeerChatParser-encrypted request; on success the server sends "ok" text and marks the peer online until the socket closes.

bash
# Peer-to-peer keepalive (typically opened automatically by the chat
# client after pairing — not usually invoked manually)
const ws = new WebSocket('ws://192.168.1.100:8080/status?cid=<peer-id>')
ws.send(peerEncryptedFrame)