Report the app version to the relay

Android sent no User-Agent at all, so a relay could not tell an old build from
a new one, which is the check that has to pass before the contact graph can be
removed.

The version is set on the client module by the app layer at launch, because the
app layer is the one that knows it: this module is a library with no BuildConfig
of the app's own. Unset it reports "0", which a relay reads as an unknown build
rather than a recent one, so forgetting to set it fails safe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-15 18:14:26 -04:00
co-authored by Claude Opus 5
parent e48f726c0f
commit 73c7b7baef
2 changed files with 32 additions and 1 deletions
@@ -96,6 +96,12 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
// time anybody reads one the app has usually been updated.
AppDiagnostics.noteVersion(this)
// What the relay sees in the User-Agent. Set here because this is the
// layer that knows the version; the client module is a library and has
// no BuildConfig of the app's own.
io.flamenet.client.RelayClient.CLIENT_VERSION =
AppUpdate.installedVersionName(this)
val lock = AppLock(this)
setContent {
@@ -71,6 +71,22 @@ class RelayClient(
private var auth: RelayAuth? = null
companion object {
/**
* The app's marketing version, for the User-Agent.
*
* Set once by the app layer, which is the layer that knows it: this
* module is a library and has no BuildConfig of the app's own. A
* relay reads it to tell whether every client still polling is new
* enough for a server-side feature to be removed (§8.9c), and it
* carries the version and nothing else, no build number and no device.
*
* Unset it reports "0", which a relay reads as an unknown build rather
* than as a recent one, so forgetting to set it is the safe direction.
*/
@Volatile
@JvmStatic
var CLIENT_VERSION: String = "0"
private val JSON = "application/json".toMediaType()
private val OCTETS = "application/octet-stream".toMediaType()
@@ -846,7 +862,16 @@ class RelayClient(
logicalPath: String,
body: RequestBody?,
): Request {
val builder = Request.Builder().url(url).header("Accept", "application/json")
val builder =
Request
.Builder()
.url(url)
.header("Accept", "application/json")
// §8.9c. A relay needs to know whether every client still
// polling is new enough for a server-side feature to be
// removed, and it cannot ask. The marketing version only: no
// build number, no device model, no Android version.
.header("User-Agent", "FlamenetMessenger/$CLIENT_VERSION (Android)")
if (method == "GET") builder.get() else builder.method(method, body ?: ByteArray(0).toRequestBody(JSON))
val bytes = bodyBytes(body)