This example demonstrates how to integrate the SpeedOf.Me speed test API into an Android app using WebView.
The SpeedOf.Me API is JavaScript-based, so Android apps use WebView to:
speedtest.html to your registered domain, so the WebView can load it over https. See "Hosting the Test Page" below: this is a requirement, not a preference.SpeedTestActivity.kt to your source directorySomApi.account = "YOUR_API_KEY";
SomApi.domainName = "your-domain.com";
In app/build.gradle.kts:
dependencies {
implementation("androidx.activity:activity-compose:1.8.0")
implementation("androidx.compose.material3:material3:1.1.2")
implementation("androidx.compose.ui:ui:1.5.4")
}
<uses-permission android:name="android.permission.INTERNET" />
<activity
android:name=".SpeedTestActivity"
android:exported="true" />
Android App
└── SpeedTestActivity (Compose)
└── WebView
└── speedtest.html (on your domain)
└── api.js
▼
window.Android.onMessage(json)
▼
@JavascriptInterface onMessage()
The HTML page sends messages via:
window.Android.onMessage(JSON.stringify({
type: 'completed',
data: result
}));
Kotlin receives them via @JavascriptInterface:
class SpeedTestJsInterface(private val viewModel: SpeedTestViewModel) {
@JavascriptInterface
fun onMessage(json: String) {
viewModel.handleMessage(json)
}
}
// Add to WebView
webView.addJavascriptInterface(
SpeedTestJsInterface(viewModel),
"Android" // This becomes window.Android in JS
)
speedtest.html sends five message types: ready, started, progress, completed and error. It normalizes the numbers before forwarding them, because the engine sends an empty string for any value that is not available, which would break Kotlin's typed parsing:
function num(v) { return typeof v === 'number' ? v : 0; }
progress payload, forwarded as {type, pass, percentDone, currentSpeed}:
type: "download", "upload" or "latency"pass: current pass number. The engine sends '' during the latency phase, normalized to 0percentDone: 0-100currentSpeed: Mbps. Only filled in when SomApi.config.progress.verbose = true, and never on latency events, so it is normalized to 0 therespeedtest.html sets SomApi.config.progress.verbose = true, which is what makes currentSpeed a real number during the download and upload phases.
completed payload, forwarded as {download, upload, latency, jitter, testServer, ip_address, hostname}. The engine's full result also carries maxDownload, maxUpload, userAgent and testDate; add them to the forwarded object and to SpeedTestResult if you need them. Every field is always present: a disabled sub-test sends an empty string rather than omitting the key, which is why the numbers go through num().
error payload is {code, message}:
1001 Invalid Account1002 Domain Mismatch2001 Test Error2002 Invalid server response2003 Request timeout2004 Test timeout2005 Speed test could not start (status N)2006 Speed test engine did not loadCode 2006, and code 2005 with status 0, mean the request never left the WebView, normally a content blocker or a frame-src Content-Security-Policy that does not allow https://speedof.me. A 2005 carrying any other status is a real HTTP response from the server, so read the status in the message.
Host speedtest.html on your registered domain and load it over https:
webView.loadUrl("https://your-domain.com/speedtest.html")
This is the only loading method that works. The engine validates the page that embeds api.js against the domain registered on your account, and it reads that domain from the page's own URL. A page loaded from assets/ (file:///android_asset/...), from any other file:// URL, or from an HTML string has no host, so it can never match your registration and every test fails with error 1002 (Domain Mismatch).
webView.settings.apply {
javaScriptEnabled = true // Required
domStorageEnabled = true // Recommended
mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
}
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
local.properties or BuildConfigwebView.destroy() in onDestroy()speedtest.html is reachable over https on your registered domainfile:// loadingrunOnUiThread {} for UI updates