Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
57bb5d3
chore: fix deprecated Android permissions
ananyaa06 Aug 26, 2025
7af43b4
fix: fix json schema for host name response
johnnzhou Aug 26, 2025
c4bec24
feat: cursory attempts at replacing runMlabPing()
ananyaa06 Aug 27, 2025
a405cab
Revert "feat: cursory attempts at replacing runMlabPing()"
ananyaa06 Aug 27, 2025
214fe90
feat(networking): replace custom Ping with ndt7 TCPInfo.RTT
ananyaa06 Aug 27, 2025
4a4e2d4
fix: remove references to in-house Ping from HomeScreen.kt
ananyaa06 Aug 27, 2025
1a4ee13
fix: tcpInfo is null
ananyaa06 Sep 2, 2025
68340fa
Revert "fix: tcpInfo is null"
ananyaa06 Sep 2, 2025
59626ce
fix: rectify problems with measurement
ananyaa06 Sep 2, 2025
9961960
fix: get the ping actually working by fixing callbacks
ananyaa06 Sep 2, 2025
1a47a73
fix: get upload to work
ananyaa06 Sep 3, 2025
7e355c0
fix: make sure upload results update on UI correctly
ananyaa06 Sep 3, 2025
eec3d2b
feat: read the data from the database and generate the csv file when …
ananyaa06 Sep 4, 2025
eb0e9d3
refactor: store specific metrics in MLabResult instead of whole TCPInfo
ananyaa06 Sep 7, 2025
2fb2a96
fix: remove smart casting
ananyaa06 Sep 7, 2025
3dea4e2
refactor: min version is 30 so remove checks
ananyaa06 Sep 7, 2025
78a17d6
refactor: replace stack trace with logging
ananyaa06 Sep 7, 2025
4306449
refactor: remove the wrapping around the imported functions from Sett…
ananyaa06 Sep 7, 2025
25c02de
refactor: make onDownloadProgress and onUploadProgress more consistent
ananyaa06 Sep 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ dependencies {
implementation 'org.bouncycastle:bcprov-jdk15to18:1.70'
implementation 'org.apache.commons:commons-csv:1.9.0'
implementation 'io.github.azhon:appupdate:4.3.2'
implementation 'androidx.documentfile:documentfile:1.0.1'

// Analytics
implementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<permission-group android:name="${applicationId}.andpermission"/>
<queries>
Expand Down Expand Up @@ -51,7 +52,7 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
android:resource="@xml/file_paths" />
</provider>

<provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class MainActivity2 : ComponentActivity() {

if (!hasPermission()) {
XXPermissions.with(this)
.permission(Permission.CAMERA, Permission.READ_EXTERNAL_STORAGE, Permission.ACCESS_FINE_LOCATION)
.permission(Permission.CAMERA, Permission.READ_MEDIA_AUDIO, Permission.READ_MEDIA_VIDEO, Permission.READ_MEDIA_IMAGES, Permission.ACCESS_FINE_LOCATION)
.request { _, allGranted ->
run {
if (!allGranted) {
Expand Down
133 changes: 66 additions & 67 deletions app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,8 @@ import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.protobuf.ByteString
import com.lcl.lclmeasurementtool.constants.NetworkConstants
import com.lcl.lclmeasurementtool.features.mlab.MLabRunner
import com.lcl.lclmeasurementtool.features.mlab.MLabTestStatus
import com.lcl.lclmeasurementtool.features.ping.Ping
import com.lcl.lclmeasurementtool.features.ping.PingError
import com.lcl.lclmeasurementtool.features.ping.PingErrorCase
import com.lcl.lclmeasurementtool.features.ping.PingResult
import com.lcl.lclmeasurementtool.location.LocationService
import com.lcl.lclmeasurementtool.model.datamodel.*
import com.lcl.lclmeasurementtool.model.repository.ConnectivityRepository
Expand Down Expand Up @@ -69,11 +64,11 @@ class MainActivityViewModel @Inject constructor(
// Network Testing
private val _isMLabTestActive = MutableStateFlow(false)

private var _mLabPingResult = MutableStateFlow(PingResultState())
private var _mlabRttResult = MutableStateFlow(ConnectivityTestResult())
private var _mLabUploadResult = MutableStateFlow(ConnectivityTestResult())
private var _mLabDownloadResult = MutableStateFlow(ConnectivityTestResult())

var mLabPingResult = _mLabPingResult.asStateFlow()
var mlabRttResult = _mlabRttResult.asStateFlow()
var mlabUploadResult = _mLabUploadResult.asStateFlow()
var mlabDownloadResult = _mLabDownloadResult.asStateFlow()
val isMLabTestActive = _isMLabTestActive.asStateFlow()
Expand Down Expand Up @@ -250,33 +245,7 @@ class MainActivityViewModel @Inject constructor(
started = SharingStarted.WhileSubscribed(5_000)
)

private suspend fun runMLabPing() {
try {
Ping.cancellableStart(address = NetworkConstants.PING_TEST_ADDRESS, timeout = 1000)
.onStart {
Log.d(TAG, "isActive = true")
_isMLabTestActive.value = true
}
.onCompletion {
if (it != null) {
Log.d(TAG, "Error is ${it.message}")
_isMLabTestActive.value = false
}
}
.collect {
_mLabPingResult.value = when(it.error.code) {
PingErrorCase.OK -> PingResultState.Success(it)
else -> {
_isMLabTestActive.value = false
PingResultState.Error(it.error)
}
}
}
} catch (e: IllegalArgumentException) {
_mLabPingResult.value = PingResultState.Error(PingError(PingErrorCase.OTHER, e.message))
Log.e(TAG, "Ping Config error")
}
}
// The runMLabPing method is removed as we now use ndt7's TCPInfo.RTT instead of custom ping

fun cancelMLabTest() {
Log.d(TAG, "cancellation: the test job is $mlabTestJob")
Expand All @@ -287,47 +256,83 @@ class MainActivityViewModel @Inject constructor(

private suspend fun getMLabTestResult() {
try {
Log.d(TAG, "Starting MLab test (DOWNLOAD_AND_UPLOAD)")
MLabRunner.runTest(NDTTest.TestType.DOWNLOAD_AND_UPLOAD)
.onStart {
_isMLabTestActive.value = true
Log.d(TAG, "MLab test started")
}
.onCompletion {
if (it != null) {
Log.d(TAG, "Error is ${it.message}")
Log.e(TAG, "Error in MLab test: ${it.message}", it)
_isMLabTestActive.value = false
} else {
Log.d(TAG, "MLab test completed normally")
}
}
.collect{
when(it.type) {
NDTTest.TestType.UPLOAD -> {
_mLabUploadResult.value = when(it.status) {
MLabTestStatus.RUNNING -> { ConnectivityTestResult.Result(it.speed!!, Color.LightGray) }

MLabTestStatus.FINISHED -> { ConnectivityTestResult.Result(it.speed!!, Color.Black) }

MLabTestStatus.ERROR -> {
_isMLabTestActive.value = false
ConnectivityTestResult.Error(it.errorMsg!!)
if (it.speed != null) {
Log.d(TAG, "Upload speed update: ${it.speed}, status: ${it.status}")
_mLabUploadResult.value = when(it.status) {
MLabTestStatus.RUNNING -> {
ConnectivityTestResult.Result(it.speed, Color.LightGray)
}
MLabTestStatus.FINISHED -> {
Log.d(TAG, "Upload test finished with speed: ${it.speed}")
ConnectivityTestResult.Result(it.speed, Color.Black)
}
MLabTestStatus.ERROR -> {
Log.e(TAG, "Upload test error: ${it.errorMsg}")
_isMLabTestActive.value = false
ConnectivityTestResult.Error(it.errorMsg ?: "Unknown error")
}
}
} else if (it.status == MLabTestStatus.ERROR) {
Log.e(TAG, "Upload test error with null speed: ${it.errorMsg}")
_mLabUploadResult.value = ConnectivityTestResult.Error(it.errorMsg ?: "Unknown error")
_isMLabTestActive.value = false
}
}
NDTTest.TestType.DOWNLOAD -> {
_mLabDownloadResult.value = when(it.status) {
MLabTestStatus.RUNNING -> { ConnectivityTestResult.Result(it.speed!!, Color.LightGray) }

MLabTestStatus.FINISHED -> { ConnectivityTestResult.Result(it.speed!!, Color.Black) }

MLabTestStatus.ERROR -> {
_isMLabTestActive.value = false
ConnectivityTestResult.Error(it.errorMsg!!)
// Extract RTT from TCPInfo if available during download test
it.tcpInfo?.rtt?.let { rtt ->
val rttMs = rtt.toDouble() / 1000.0 // microseconds → milliseconds
_mlabRttResult.value = ConnectivityTestResult.Result(rttMs.toString(), Color.Black)
Log.d(TAG, "RTT from Download test: $rttMs ms")
}

if (it.speed != null) {
Log.d(TAG, "Download speed update: ${it.speed}, status: ${it.status}")
_mLabDownloadResult.value = when(it.status) {
MLabTestStatus.RUNNING -> {
ConnectivityTestResult.Result(it.speed, Color.LightGray)
}
MLabTestStatus.FINISHED -> {
Log.d(TAG, "Download test finished with speed: ${it.speed}")
ConnectivityTestResult.Result(it.speed, Color.Black)
}
MLabTestStatus.ERROR -> {
Log.e(TAG, "Download test error: ${it.errorMsg}")
_isMLabTestActive.value = false
ConnectivityTestResult.Error(it.errorMsg ?: "Unknown error")
}
}
} else if (it.status == MLabTestStatus.ERROR) {
Log.e(TAG, "Download test error with null speed: ${it.errorMsg}")
_mLabDownloadResult.value = ConnectivityTestResult.Error(it.errorMsg ?: "Unknown error")
_isMLabTestActive.value = false
}
}
else -> { }
else -> {
Log.d(TAG, "Other test type: ${it.type}, status: ${it.status}, speed: ${it.speed}")
}
}
}
} catch (e: Exception) {
Log.d(TAG, "catch $e")
Log.e(TAG, "Exception during MLab test", e)
_isMLabTestActive.value = false
}
}

Expand All @@ -341,12 +346,7 @@ class MainActivityViewModel @Inject constructor(
try {
resetMLabTestResult()

runMLabPing()
if (_mLabPingResult.value is PingResultState.Error) {
this.cancel("Ping Test Failed")
}
ensureActive()

// Run ndt7 test (which includes RTT measurement)
getMLabTestResult()
if (_mLabUploadResult.value is ConnectivityTestResult.Error || _mLabDownloadResult.value is ConnectivityTestResult.Error) {
Log.d(TAG, "mlab test job is cancelled")
Expand All @@ -361,7 +361,7 @@ class MainActivityViewModel @Inject constructor(
ensureActive()

_isMLabTestActive.value = false
Log.d(TAG, "ping, upload, download are finished. isMLabTestActive.value=${isMLabTestActive.value}")
Log.d(TAG, "upload, download are finished. isMLabTestActive.value=${isMLabTestActive.value}")
val curTime = TimeUtil.getCurrentTime()
val cellID = signalStrengthMonitor.getCellID()

Expand All @@ -387,8 +387,8 @@ class MainActivityViewModel @Inject constructor(
it.second.deviceID,
(_mLabUploadResult.value as ConnectivityTestResult.Result).result.toDouble(),
(_mLabDownloadResult.value as ConnectivityTestResult.Result).result.toDouble(),
(_mLabPingResult.value as PingResultState.Success).result.avg!!.toDouble(),
(_mLabPingResult.value as PingResultState.Success).result.numLoss!!.toDouble(),
(_mlabRttResult.value as ConnectivityTestResult.Result).result.toDouble(),
0.0, // No packet loss information available from ndt7, defaulting to 0
)

saveToDB(signalStrengthReportModel, connectivityReportModel)
Expand Down Expand Up @@ -446,9 +446,11 @@ class MainActivityViewModel @Inject constructor(
}

private fun resetMLabTestResult() {
_mLabPingResult.value = PingResultState.Error(PingError(PingErrorCase.OK, null))
Log.d(TAG, "Resetting MLab test results")
_mlabRttResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray)
_mLabUploadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray)
_mLabDownloadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray)
_isMLabTestActive.value = false
}
}

Expand All @@ -462,9 +464,6 @@ open class ConnectivityTestResult {
data class Error(val error: String): ConnectivityTestResult()
}

open class PingResultState {
data class Success(val result: PingResult): PingResultState()
data class Error(val error: PingError): PingResultState()
}
// PingResultState removed as we now use ndt7's TCPInfo.RTT instead

data class SignalStrengthResult(val dbm: Int, val level: SignalStrengthLevelEnum)
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package com.lcl.lclmeasurementtool.features.mlab

import net.measurementlab.ndt7.android.NDTTest
import net.measurementlab.ndt7.android.models.ClientResponse
import net.measurementlab.ndt7.android.models.Measurement

interface MLabCallback {
fun onDownloadProgress(clientResponse: ClientResponse)
fun onUploadProgress(clientResponse: ClientResponse)
fun onMeasurementDownloadProgress(measurement: Measurement)
fun onMeasurementUploadProgress(measurement: Measurement)
fun onFinish(clientResponse: ClientResponse?, error: Throwable?, testType: NDTTest.TestType)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.lcl.lclmeasurementtool.features.mlab

import net.measurementlab.ndt7.android.NDTTest
import net.measurementlab.ndt7.android.models.TCPInfo

data class MLabResult(
val speed: String?,
val type: NDTTest.TestType,
val errorMsg: String?,
val status: MLabTestStatus
val status: MLabTestStatus,
val tcpInfo: TCPInfo? = null
)

enum class MLabTestStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import android.util.Log
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import net.measurementlab.ndt7.android.NDTTest
import net.measurementlab.ndt7.android.models.AppInfo
import net.measurementlab.ndt7.android.models.ClientResponse
import net.measurementlab.ndt7.android.models.Measurement
import net.measurementlab.ndt7.android.utils.DataConverter
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
Expand All @@ -20,6 +22,16 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback):
callback.onUploadProgress(clientResponse)
}

override fun onMeasurementDownloadProgress(measurement: Measurement) {
super.onMeasurementDownloadProgress(measurement)
callback.onMeasurementDownloadProgress(measurement)
}

override fun onMeasurementUploadProgress(measurement: Measurement) {
super.onMeasurementUploadProgress(measurement)
callback.onMeasurementUploadProgress(measurement)
}

override fun onFinished(
clientResponse: ClientResponse?,
error: Throwable?,
Expand All @@ -42,8 +54,19 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback):

override fun onUploadProgress(clientResponse: ClientResponse) {
val speed = DataConverter.convertToMbps(clientResponse)
Log.d(TAG, "client upload is $speed")
channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING))
if (speed != null && speed != "0.0") {
channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING))
}
}

override fun onMeasurementDownloadProgress(measurement: Measurement) {
Log.d(TAG, "on measurement download")
channel.trySend(MLabResult(null, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, measurement.tcpInfo))
}

override fun onMeasurementUploadProgress(measurement: Measurement) {
Log.d(TAG, "on measurement upload")
channel.trySend(MLabResult(null, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement.tcpInfo))
}

override fun onFinish(
Expand All @@ -52,13 +75,19 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback):
testType: TestType
) {
if (clientResponse != null) {
Log.d(TAG, "client finish test $testType")
channel.trySend(MLabResult(DataConverter.convertToMbps(clientResponse), testType, null, MLabTestStatus.FINISHED))
val speed = DataConverter.convertToMbps(clientResponse)
Log.d(TAG, "client finish test $testType with speed $speed")
channel.trySend(MLabResult(speed, testType, null, MLabTestStatus.FINISHED))
} else {
Log.e(TAG, "Error during $testType test: ${error?.message}")
channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR))
}

if (testType == TestType.UPLOAD) channel.close()
// Only close the channel after both download and upload tests are complete
if (testType == TestType.UPLOAD || testType == TestType.DOWNLOAD_AND_UPLOAD) {
Log.d(TAG, "Closing channel after $testType test")
channel.close()
}
}
}

Expand All @@ -72,7 +101,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback):
}
}

private fun createHttpClient(connectTimeout: Long = 10, readTimeout: Long = 10, writeTimeout: Long = 10): OkHttpClient {
private fun createHttpClient(connectTimeout: Long = 30, readTimeout: Long = 30, writeTimeout: Long = 30): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(connectTimeout, TimeUnit.SECONDS)
.readTimeout(readTimeout, TimeUnit.SECONDS)
Expand All @@ -81,4 +110,3 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback):
}
}
}

Loading