3ReadySpeed 3ReadySpeed SDK Documentation
Admin Portal

SpeedTester SDK

Run speed tests, monitor network status, and manage Wi-Fi — all through the SpeedTester TV app, with a clean Kotlin API. No AIDL. No service binding. No boilerplate.

Android 7.0+ Kotlin Coroutines Flow

Overview

The SpeedTester SDK wraps the IPC layer between your app and the SpeedTester TV service. Under the hood it uses AIDL, but you never touch it — the SDK handles service binding, reconnection, and marshalling automatically.

Your App
consumer
→
SpeedTester SDK
AAR
→
AIDL / Binder IPC
hidden
→
SpeedTester App
tv.threess.speedtester
RequirementMinimum
Android SDK24 (Android 7.0)
Kotlin1.9+
Coroutines1.7+
SpeedTester TV appInstalled on device

Authentication

Every call into the SpeedTester service is authorised. Two mechanisms are available — whichever passes first is used.

📦 Package allowlist

Register your app's package name via the admin portal (Tenants → Allowed Packages). The service verifies the calling UID automatically — no code needed on your side.

🔑 Tenant API key

Request a key from 3SS and pass it to SpeedTesterSdk.init(). Useful for multi-app or white-label scenarios where package registration isn't practical.

Dev mode: If the SpeedTester app has no allowlist or keys configured (fresh dev install), all callers are permitted — no credentials needed for local development.

Setup

Add the dependency

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        maven { url = uri("https://maven.3ss.tv/releases") }
    }
}
// app/build.gradle.kts
dependencies {
    implementation("tv.threess:speedtest-sdk:1.3.1")
}

Declare package visibility (Android 11+)

<!-- AndroidManifest.xml -->
<queries>
    <package android:name="tv.threess.speedtester" />
</queries>

Initialise the SDK

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        SpeedTesterSdk.init(
            context = this,
            apiKey  = "YOUR_TENANT_API_KEY"  // omit if using package allowlist
        )
    }
}
Important: Creating a client before calling init() throws an IllegalStateException at runtime.

Wake the SpeedTester app (SDK 1.2.0)

// right after init(), once per app start
SpeedTesterSdk.wakeInBackground(this)

// or, from a coroutine: returns false (never throws) if the app is unreachable
val awake = SpeedTesterSdk.wake(context)

Android keeps a freshly installed app stopped until one of its components runs, and a stopped app receives no BOOT_COMPLETED. Until someone opened the SpeedTester UI once it therefore never started on boot and had no tenant configuration when your first call arrived. Binding to it fixes both: the platform clears the stopped state, and from app 1.2.0 the services bootstrap themselves on bind. wake holds the binding for 20 s so that completes, then releases. Every other SDK call has the same side effect; wake just moves the cold start to your startup instead of the user's first speed test.

Speed test

Use SpeedTestClient to trigger a test and observe progress as a Flow. Cancelling the coroutine automatically cancels the test. The service runs one measurement at a time: a startTest() while a test is already in flight joins it and receives the same result, and a single engine run is capped at 90 s, after which the flow ends with an Error event.

val client = SpeedTestClient(context)

client.startTest().collect { event ->
    when (event) {
        is SpeedTestClient.Event.Progress   -> showProgress(event.downloadMbps, event.percent)
        is SpeedTestClient.Event.Completed -> showResult(event.result)
        is SpeedTestClient.Event.Error     -> showError(event.message)
    }
}

client.release() // call in onDestroy / onCleared

Network status

Observe live network conditions as a Flow. Emits immediately with the current state, then on every change.

val client = NetworkStatusClient(context)

client.networkStatus().collect { status ->
    println("${status.connectionType} — ${status.ssid} (${status.signalStrengthDbm} dBm)")
}

Wi-Fi

val client = WifiClient(context)

when (val result = client.connect("MyNetwork", "password123")) {
    is WifiConnectResult.Connected -> println("Connected to ${result.ssid}")
    is WifiConnectResult.Failed    -> println("Failed: ${result.reason}")
}

AI integration — NetworkAiBridge

NetworkAiBridge exposes the whole SDK as three generic JSON-in / JSON-out functions designed for LLM tool-calling (e.g. an AI assistant in your launcher). Register them as tools with your model — the bridge handles capability discovery, batched signal reads and action dispatch on top of the typed clients, and every return value is a JSON string you can feed straight back to the model.

val bridge = NetworkAiBridge(context)

// 1. Tell the model what it can see and do
val manifest = bridge.getAdvancedNetworkContext()

// 2. Read the signals the model asked for
val values = bridge.getNetworkSignals(listOf("wifi.signalDbm", "speedtest.downloadMbps"))

// 3. Execute an action the model chose
val result = bridge.networkAction("wifi.connect", mapOf("ssid" to "MyAp", "passphrase" to "secret"))

bridge.release() // call in onDestroy / onCleared

getAdvancedNetworkContext()

Returns a manifest of every signal id and action id (with parameter schemas and an available flag — Wi-Fi actions are marked unavailable on devices without a Wi-Fi radio), plus a live network snapshot and the last speed test result so the model has immediate context without a follow-up call.

getNetworkSignals(signalIds)

Batch-reads signal values by id. Returns {"signals": {id: value}, "errors": {id: message}}. Unknown ids land in errors; a signal with no value yet (e.g. no speed test has run) is null.

Signal idTypeDescription
net.connectionTypestringWIFI, CELLULAR, ETHERNET, UNKNOWN
net.ipv4 / net.ipv6stringDevice IP addresses on the active network
wifi.ssid / wifi.bssidstringConnected network name / AP MAC address
wifi.signalDbmintSignal strength in dBm (more negative = weaker)
wifi.frequencyMhzintChannel frequency (2.4 GHz vs 5 GHz band)
wifi.linkSpeedMbpsintNegotiated PHY link speed
wifi.visibleSsidsstring[]SSIDs currently visible to the device
wifi.scanResultsobject[]Detailed scan: ssid, bssid, level, frequencyMhz, capabilities, isOpen, isKnownNetwork
speedtest.lastResultobjectFull last result including the network it ran on
speedtest.downloadMbps / speedtest.uploadMbpsfloatThroughput of the last test
speedtest.pingMs / speedtest.jitterMsfloatLatency / jitter of the last test
speedtest.packetLossPercentfloatPacket loss of the last test
speedtest.timestampEpochMslongWhen the last test completed
speedtest.enginestring"ookla" or "librespeed"

networkAction(actionId, params)

Dispatches an action by id with string key-value params. Returns {"ok": true, "result": …} on success or {"ok": false, "code": …, "error": …} on failure.

Action idParamsDescription
speedtest.run—Runs a full speed test; suspends until finished (typically 15–60 s, never more than 120 s) and returns the result. If a test is already in progress it joins that test instead of starting a second one. Cancel the calling coroutine to abort.
wifi.scan—Triggers a Wi-Fi scan and returns visible access points
wifi.connectssid, passphraseConnects to a network with a passphrase
wifi.connectKnownssidReconnects to a known network; fails with needs_passphrase if one is required again
wifi.forgetssidRemoves a network from the known-networks store
wifi.disconnect—Disconnects from the current Wi-Fi network

Error codes

CodeMeaning
unknown_actionAction id not recognised (valid ids are listed in the error message)
missing_paramsA required param was absent or empty
unavailableDevice has no Wi-Fi radio, so Wi-Fi actions cannot run
needs_passphraseNetwork requires a passphrase — the model should ask the user, then retry with wifi.connect
connect_failed / forget_failedThe Wi-Fi operation was rejected by the system (reason included)
speedtest_failedThe engine reported an error mid-test, including the service's own 90 s engine timeout
speedtest_timeoutNo result within 120 s (bridge safety net). Do not retry immediately — fall back to quality.summary / speedtest.lastResult
action_failedUnexpected failure (service unreachable, binder error, …)
Tip: speedtest.run is the only long-running action. If your assistant needs a responsive UI while it runs, launch the tool call in its own coroutine and keep streaming getNetworkSignals reads in parallel. If the user cancels the assistant turn, cancel that coroutine too so the test is not left running with nobody listening. You do not need your own timeout or "already running" guard — the bridge always returns within 120 s and concurrent calls join the in-flight test.

Live progress — speedTestEvents

networkAction("speedtest.run") only returns when the test is over, but the bridge also exposes speedTestEvents: SharedFlow<SpeedTestClient.Event> so your UI can show the numbers while they are being measured. Collect it from the same NetworkAiBridge instance you hand the tool calls to: you get Started once with the engine ("ookla" / "librespeed" — pick the "Powered by" logo here; SpeedTester app 1.3.0+), then Progress roughly once a second (download Mbps during the download phase, upload Mbps during the upload phase, percent capped at 99), then exactly one Completed or Error. A bridge timeout arrives as an Error too, so the collector never hangs. The most recent event is replayed to new collectors — treat a replayed terminal event as "last known result", not as a running test.

// once, when the assistant session starts
scope.launch {
    bridge.speedTestEvents.collect { event ->
        when (event) {
            is SpeedTestClient.Event.Started   -> widget.showPoweredBy(event.engine)
            is SpeedTestClient.Event.Progress  -> widget.showMeasuring(event.downloadMbps, event.uploadMbps)
            is SpeedTestClient.Event.Completed -> widget.showResult(event.result)
            is SpeedTestClient.Event.Error     -> widget.showLastKnown()
        }
    }
}

Data models

SpeedTestResult

FieldTypeDescription
sessionIdStringUnique test identifier
engineString"ookla" or "librespeed"
timestampEpochMsLongCompletion time (Unix ms)
downloadMbpsFloatDownload speed
uploadMbpsFloatUpload speed
pingMsFloatLatency
jitterMsFloatJitter
packetLossPercentFloatPacket loss %
networkNetworkStatusNetwork context at test time

NetworkStatus

FieldTypeDescription
connectionTypeConnectionTypeWIFI, CELLULAR, ETHERNET, UNKNOWN
ssidString?Wi-Fi SSID. Null off Wi-Fi or when the platform withholds it (the SpeedTester app needs location granted; never the raw <unknown ssid> sentinel)
bssidString?Access point MAC address; null when withheld
signalStrengthDbmIntSignal in dBm
linkSpeedMbpsIntPHY link speed
ipAddressString?IPv4 address
visibleSsidsList<String>Nearby network names

Error reference

ErrorCauseFix
"Not authorised" Package not on allowlist; API key missing or invalid Add package in Tenants → Allowed Packages, or pass a valid API key
IllegalStateException: init()… Client created before SpeedTesterSdk.init() Move init() to Application.onCreate()
SpeedTesterException.BindTimeout Service resolved but did not bind within 5 s — usually a slow cold start of the SpeedTester process Call SpeedTesterSdk.wake() at startup so the cold start happens before your first real call; the next call re-binds
First test after a fresh install fails SpeedTester app older than 1.2.0 had no tenant config until its UI was opened once Update the SpeedTester app to ≥ 1.2.0 and call wake() at startup

Permissions

All sensitive permissions are held by the SpeedTester app. Your AndroidManifest.xml does not need ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE, CHANGE_WIFI_STATE, or INTERNET. You only need the <queries> entry from step 2.

Wi-Fi name (SSID) availability. Android 10+ only discloses the SSID and BSSID to a process that is foreground-for-location. From SDK 1.1.1 the binding passes your app's foreground capabilities to the SpeedTester process (BIND_INCLUDE_CAPABILITIES), so NetworkStatus.ssid is populated while your app is on screen. It is null (never a raw <unknown ssid>) when the SpeedTester app has no location permission, location services are off, or the read happened with no client on screen. Signal strength, frequency and link speed are unaffected.

Version history

VersionDateChanges
1.3.12026-09-23No SDK API change: version kept in step with the SpeedTester app. App 1.3.1 fixes a crash on every launch on Android 14 (UninitializedPropertyAccessException: workerFactory): WorkManager is now resolved lazily and its configuration reads the worker factory from a Hilt entry point.
1.3.02026-09-23SpeedTestClient.Event.Started(sessionId, engine) (also on NetworkAiBridge.speedTestEvents and as ConnectionQualityClient.Event.Started(engine)): the engine — "ookla" or "librespeed" — is announced before the first Progress, so you can show the right "Powered by" logo from the start. New AIDL callback onStarted, appended last so mixed versions keep working; an older SpeedTester app never sends it, so fall back to result.engine. Exhaustive when (event) blocks need a Started branch. Pairs with SpeedTester app 1.3.0, which also uploads SDK-triggered results with aggregate network context for the portal’s Network Analytics.
1.2.02026-09-16SpeedTesterSdk.wake() / wakeInBackground() and NetworkAiBridge.wake(): bind once at host-app start so a never-launched SpeedTester install leaves Android's stopped state (arming BOOT_COMPLETED) and bootstraps its tenant config. Pairs with SpeedTester app 1.2.0, whose services bootstrap on bind, wait for the first config fetch before running a test, and persist caller allowlists across process restarts.
1.1.12026-09-09Version jumps from 1.0.2 to match the SpeedTester app, so one number identifies a compatible pair. Binding uses BIND_INCLUDE_CAPABILITIES so SSID/BSSID are no longer redacted while the host app is on screen. SpeedTester app 1.1.1 pairs with it: redaction sentinels map to null, and connectionType now follows the SDK order (Ethernet was previously reported as CELLULAR).
1.0.22026-09-02NetworkAiBridge.speedTestEvents republishes live progress of speedtest.run.
1.0.12026-09-02speedtest.run capped at 120 s (speedtest_timeout); concurrent startSpeedTest calls join the in-flight test.
1.0.0—Initial release.
For API key requests, package registration, or support: sdk@3ss.tv