An Android client for the Measurement Lab's MSAK measurement server. Uses the Locate API to select a measurement server and runs latency and throughput (download and upload speed) tests.
Used for CellWatch App
Due to the lack of a suitable WebSocket client for Android that provides access to detailed socket information, the transport-layer byte counts are gathered via a bit of a hack. In our testing, the app must use the Conscrypt security provider to get correct transport-layer byte counts when accessing MSAK servers over TLS. Adding a dependency on org.conscrypt:conscrypt-android and then adding the following early in your app's initialization should do the trick:
Security.insertProviderAt(Conscrypt.newProvider(), 1)(The MSAK throughput tests also collect application-layer byte counts, which work regardless with or without Conscrypt.)
You can get msak-android via jitpack.io's Maven repository. First add the repository to your settings.gradle file:
dependencyResolutionManagement {
repositories {
// ...
maven { url 'https://jitpack.io' }
}
}
Then add msak-android to your dependencies in the app-level build.gradle, replacing <tag> with the desired version tag:
dependencies {
implementation 'com.github.CellWatch:msak-android:<tag>'
}
For more instructions, see https://jitpack.io/#CellWatch/msak-android.
The Javadoc comments aim to document all public classes and methods. A few useful examples are included below.
// pick a latency server
val latencyServer = LocateManager().locateLatencyServers().first()
// start the latency test and monitor its updates while it runs
val latencyTest = LatencyTest(latencyServer)
latencyTest.start()
latencyTest.updatesChan.consumeEach { update ->
Log.d(TAG, "got latency update $update")
}
// handle the latency result
Log.d(TAG, "latency result: ${latencyTest.result}")
// pick a throughput server
val throughputServer = LocateManager().locateThroughputServers().first()
// start the download test and monitor its updates while it runs
val downloadTest = ThroughputTest(throughputServer, ThroughputDirection.DOWNLOAD)
downloadTest.start()
downloadTest.updatesChan.consumeEach { update ->
Log.d(TAG, "got download update: $update")
}
// handle the download result
Log.d(TAG, "download result: ${downloadTest.result}")
// start the upload test and monitor its updates while it runs
val uploadTest = ThroughputTest(throughputServer, ThroughputDirection.UPLOAD)
uploadTest.start()
uploadTest.updatesChan.consumeEach { update ->
Log.d(TAG, "got upload update: $update")
}
// handle the upload result
Log.d(TAG, "upload result: ${uploadTest.result}")You can use a local MSAK server by providing a few additional arguments when constructing the LocateManager:
val locate = LocateManager(
serverEnv = ServerEnv.LOCAL,
msakLocalServerHost = "localhost",
msakLocalServerSecure = false,
)In this case, the LocateManager will not actually call M-Lab's Locate API but will instead construct URLs to access the local MSAK server.
You may need to adjust the msakLocalServerHost depending on where your local MSAK server is running.
By default, msak-android uses android.util.Log to log. You can call setLogger to use a custom logger object instead.
msak-android is an Android Studio project. For local testing, it may be useful to publish to the configured local repository (./gradlew publishAllPublicationsToLocalRepository) and then set up your app to consume the library from that local repository:
// in settings.gradle (make sure you adjust the path)
dependencyResolutionManagement {
repositories {
maven { url 'file:///path/to/msak-android/msak-android/build/repo' }
// ...
}
}
// in build.gradle (make sure you adjust the version)
dependencies {
implementation 'edu.gatech.cc.cellwatch:msak:<version>'
}
This project is sensitive to JDK mismatches across shell, Android Studio, and Xcode-driven Gradle runs.
Use JDK 17 and keep all three configuration points aligned to the same absolute JDK path:
- Shell (SDKMAN, jEnv, manual
JAVA_HOME, or similar) - Android Studio (
GRADLE_LOCAL_JAVA_HOMEvia.gradle/config.properties) - Gradle property pin used by non-interactive Gradle/Xcode runs (
org.gradle.java.home)
Example JDK path used below (use your own local JDK 17 path if different):
/opt/homebrew/opt/sdkman-cli/libexec/candidates/java/17.0.8-tem
.sdkmanrc is tracked as one option for terminal sessions:
sdk envIf you do not use SDKMAN, set JAVA_HOME via your preferred tool/process.
If your shell does not auto-apply .sdkmanrc, configure SDKMAN auto-env or use a shell hook.
Set Android Studio Gradle JDK to GRADLE_LOCAL_JAVA_HOME.
That value comes from:
.gradle/config.properties
Create/update it (example):
java.home=/opt/homebrew/opt/sdkman-cli/libexec/candidates/java/17.0.8-temThis repo currently pins Gradle JVM in:
gradle.properties
with (example):
org.gradle.java.home=/opt/homebrew/opt/sdkman-cli/libexec/candidates/java/17.0.8-temIf your local path differs, update this value accordingly.
From terminal:
java -version
./gradlew -versionIn Android Studio:
- Check
Build, Execution, Deployment > Build Tools > Gradle > Gradle JDK. - Ensure it is
GRADLE_LOCAL_JAVA_HOME. - Re-sync project.
If behavior looks inconsistent, stop daemons and clear caches before retrying:
./gradlew --stop
rm -rf .gradle .gradle-local
rm -rf ~/.gradle/caches ~/.gradle/daemon ~/.gradle/nativemsak-android was developed using Roberto D'Auria's early MSAK implementation as a reference. It also provides a basic re-implementation of M-Lab's memoryless package in Kotlin.
Copyright 2024 Georgia Tech Research Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
See the LICENSE file for the full license text.