From 57bb5d3d34059cacdb3439d49beb8086e5a501c6 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Mon, 25 Aug 2025 22:28:05 -0700 Subject: [PATCH 01/19] chore: fix deprecated Android permissions --- app/src/main/AndroidManifest.xml | 1 + app/src/main/java/com/lcl/lclmeasurementtool/MainActivity2.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19120ea..261d468 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity2.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity2.kt index ee68beb..88804c6 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity2.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity2.kt @@ -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) { From 7af43b41839ef5ab21c2a16b7139b4e0c231f6b7 Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Mon, 25 Aug 2025 22:53:42 -0700 Subject: [PATCH 02/19] fix: fix json schema for host name response --- .../measurementlab/ndt7/android/models/HostnameResponse.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libndt7/src/main/java/net/measurementlab/ndt7/android/models/HostnameResponse.kt b/libndt7/src/main/java/net/measurementlab/ndt7/android/models/HostnameResponse.kt index c102003..d664ace 100644 --- a/libndt7/src/main/java/net/measurementlab/ndt7/android/models/HostnameResponse.kt +++ b/libndt7/src/main/java/net/measurementlab/ndt7/android/models/HostnameResponse.kt @@ -17,7 +17,9 @@ data class Result( @SerialName("machine") val machine: String, @SerialName("urls") - val urls: Urls + val urls: Urls, + @SerialName("hostname") + var hostname: String ) @Serializable From c4bec24844778d98af5bdb12f0085eddcefcc8a8 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Wed, 27 Aug 2025 15:48:06 -0700 Subject: [PATCH 03/19] feat: cursory attempts at replacing runMlabPing() --- .../MainActivityViewModel.kt | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index 05fae3c..3180ae8 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -252,32 +252,50 @@ class MainActivityViewModel @Inject constructor( private suspend fun runMLabPing() { try { - Ping.cancellableStart(address = NetworkConstants.PING_TEST_ADDRESS, timeout = 1000) + MLabRunner.runTest(NDTTest.TestType.DOWNLOAD_AND_UPLOAD) .onStart { - Log.d(TAG, "isActive = true") + Log.d(TAG, "TCP RTT check started") _isMLabTestActive.value = true } .onCompletion { if (it != null) { - Log.d(TAG, "Error is ${it.message}") - _isMLabTestActive.value = false + Log.d(TAG, "Error during RTT check: ${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) - } + .collect { result -> + // Extract RTT (µs → ms) + val rttMicros = result.tcpInfo?.minRTT ?: 0L + val rttMs = rttMicros / 1000.0 + + Log.d(TAG, "Observed TCP MinRTT = $rttMs ms") + + // Update your PingResultState + if (rttMs > 0) { + _mLabPingResult.value = PingResultState.Success( + PingResult( + avg = rttMs.toString(), + min = null, + max = null, + mdev = null, + numLoss = "0", // TCP doesn’t report loss + error = PingError(PingErrorCase.OK, null) + ) + ) + } else { + _mLabPingResult.value = PingResultState.Error( + PingError(PingErrorCase.OTHER, "No RTT data available") + ) } } - } catch (e: IllegalArgumentException) { - _mLabPingResult.value = PingResultState.Error(PingError(PingErrorCase.OTHER, e.message)) - Log.e(TAG, "Ping Config error") + } catch (e: Exception) { + _mLabPingResult.value = + PingResultState.Error(PingError(PingErrorCase.OTHER, e.message)) + Log.e(TAG, "TCP RTT error: $e") } } + fun cancelMLabTest() { Log.d(TAG, "cancellation: the test job is $mlabTestJob") mlabTestJob?.cancel(CancellationException("Shit, cancel this test!!!")) From a405cabf996b6a86f57d69127ce77bec9f13d899 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Wed, 27 Aug 2025 16:18:31 -0700 Subject: [PATCH 04/19] Revert "feat: cursory attempts at replacing runMlabPing()" This reverts commit c4bec24844778d98af5bdb12f0085eddcefcc8a8. --- .../MainActivityViewModel.kt | 46 ++++++------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index 3180ae8..05fae3c 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -252,50 +252,32 @@ class MainActivityViewModel @Inject constructor( private suspend fun runMLabPing() { try { - MLabRunner.runTest(NDTTest.TestType.DOWNLOAD_AND_UPLOAD) + Ping.cancellableStart(address = NetworkConstants.PING_TEST_ADDRESS, timeout = 1000) .onStart { - Log.d(TAG, "TCP RTT check started") + Log.d(TAG, "isActive = true") _isMLabTestActive.value = true } .onCompletion { if (it != null) { - Log.d(TAG, "Error during RTT check: ${it.message}") + Log.d(TAG, "Error is ${it.message}") + _isMLabTestActive.value = false } - _isMLabTestActive.value = false } - .collect { result -> - // Extract RTT (µs → ms) - val rttMicros = result.tcpInfo?.minRTT ?: 0L - val rttMs = rttMicros / 1000.0 - - Log.d(TAG, "Observed TCP MinRTT = $rttMs ms") - - // Update your PingResultState - if (rttMs > 0) { - _mLabPingResult.value = PingResultState.Success( - PingResult( - avg = rttMs.toString(), - min = null, - max = null, - mdev = null, - numLoss = "0", // TCP doesn’t report loss - error = PingError(PingErrorCase.OK, null) - ) - ) - } else { - _mLabPingResult.value = PingResultState.Error( - PingError(PingErrorCase.OTHER, "No RTT data available") - ) + .collect { + _mLabPingResult.value = when(it.error.code) { + PingErrorCase.OK -> PingResultState.Success(it) + else -> { + _isMLabTestActive.value = false + PingResultState.Error(it.error) + } } } - } catch (e: Exception) { - _mLabPingResult.value = - PingResultState.Error(PingError(PingErrorCase.OTHER, e.message)) - Log.e(TAG, "TCP RTT error: $e") + } catch (e: IllegalArgumentException) { + _mLabPingResult.value = PingResultState.Error(PingError(PingErrorCase.OTHER, e.message)) + Log.e(TAG, "Ping Config error") } } - fun cancelMLabTest() { Log.d(TAG, "cancellation: the test job is $mlabTestJob") mlabTestJob?.cancel(CancellationException("Shit, cancel this test!!!")) From 214fe90f06f2f68443c8b1d4764f45889463c42d Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Wed, 27 Aug 2025 16:37:12 -0700 Subject: [PATCH 05/19] feat(networking): replace custom Ping with ndt7 TCPInfo.RTT --- .../MainActivityViewModel.kt | 69 ++++++------------- .../features/mlab/MLabResult.kt | 4 +- .../features/mlab/MLabRunner.kt | 9 ++- .../ndt7/android/models/ClientResponse.kt | 3 +- 4 files changed, 32 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index 05fae3c..0cf41d5 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -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 @@ -69,11 +64,11 @@ class MainActivityViewModel @Inject constructor( // Network Testing private val _isMLabTestActive = MutableStateFlow(false) - private var _mLabPingResult = MutableStateFlow(PingResultState()) + private var _mlabRttResult = MutableStateFlow(0.0) 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() @@ -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") @@ -300,6 +269,12 @@ class MainActivityViewModel @Inject constructor( .collect{ when(it.type) { NDTTest.TestType.UPLOAD -> { + // Extract RTT from TCPInfo if available during upload test + it.tcpInfo?.rtt?.let { rtt -> + _mlabRttResult.value = rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds + Log.d(TAG, "RTT from Upload test: ${_mlabRttResult.value} ms") + } + _mLabUploadResult.value = when(it.status) { MLabTestStatus.RUNNING -> { ConnectivityTestResult.Result(it.speed!!, Color.LightGray) } @@ -312,6 +287,12 @@ class MainActivityViewModel @Inject constructor( } } NDTTest.TestType.DOWNLOAD -> { + // Extract RTT from TCPInfo if available during download test + it.tcpInfo?.rtt?.let { rtt -> + _mlabRttResult.value = rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds + Log.d(TAG, "RTT from Download test: ${_mlabRttResult.value} ms") + } + _mLabDownloadResult.value = when(it.status) { MLabTestStatus.RUNNING -> { ConnectivityTestResult.Result(it.speed!!, Color.LightGray) } @@ -341,12 +322,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") @@ -361,7 +337,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() @@ -387,8 +363,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, // Use RTT from ndt7 TCPInfo + 0.0, // No packet loss information available from ndt7, defaulting to 0 ) saveToDB(signalStrengthReportModel, connectivityReportModel) @@ -446,7 +422,7 @@ class MainActivityViewModel @Inject constructor( } private fun resetMLabTestResult() { - _mLabPingResult.value = PingResultState.Error(PingError(PingErrorCase.OK, null)) + _mlabRttResult.value = 0.0 _mLabUploadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) _mLabDownloadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) } @@ -462,9 +438,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) \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt index 6273c80..d135c93 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt @@ -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 { diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index 1e79caf..f6237de 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -37,13 +37,15 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): override fun onDownloadProgress(clientResponse: ClientResponse) { val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client download is $speed") - channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING)) + val measurement = clientResponse.measurement + channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) } 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)) + val measurement = clientResponse.measurement + channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) } override fun onFinish( @@ -53,7 +55,8 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): ) { if (clientResponse != null) { Log.d(TAG, "client finish test $testType") - channel.trySend(MLabResult(DataConverter.convertToMbps(clientResponse), testType, null, MLabTestStatus.FINISHED)) + val measurement = clientResponse.measurement + channel.trySend(MLabResult(DataConverter.convertToMbps(clientResponse), testType, null, MLabTestStatus.FINISHED, measurement?.tcpInfo)) } else { channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR)) } diff --git a/libndt7/src/main/java/net/measurementlab/ndt7/android/models/ClientResponse.kt b/libndt7/src/main/java/net/measurementlab/ndt7/android/models/ClientResponse.kt index 4c5a8c9..73d99bc 100644 --- a/libndt7/src/main/java/net/measurementlab/ndt7/android/models/ClientResponse.kt +++ b/libndt7/src/main/java/net/measurementlab/ndt7/android/models/ClientResponse.kt @@ -7,7 +7,8 @@ import kotlinx.serialization.Serializable data class ClientResponse( @SerialName("AppInfo") val appInfo: AppInfo, @SerialName("Origin") val origin: String = "client", - @SerialName("Test") val test: String + @SerialName("Test") val test: String, + @SerialName("Measurement") val measurement: Measurement? = null ) @Serializable From 4a4e2d4a94981f6119f5a9b89a2693bcaa5395e2 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Wed, 27 Aug 2025 16:46:18 -0700 Subject: [PATCH 06/19] fix: remove references to in-house Ping from HomeScreen.kt --- .../lcl/lclmeasurementtool/ui/HomeScreen.kt | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt index 5e616ad..a1d6837 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt @@ -30,10 +30,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.lcl.lclmeasurementtool.BuildConfig import com.lcl.lclmeasurementtool.ConnectivityTestResult import com.lcl.lclmeasurementtool.MainActivityViewModel -import com.lcl.lclmeasurementtool.PingResultState import com.lcl.lclmeasurementtool.SignalStrengthResult -import com.lcl.lclmeasurementtool.features.ping.PingError -import com.lcl.lclmeasurementtool.features.ping.PingErrorCase import kotlinx.coroutines.cancel @Composable @@ -48,7 +45,7 @@ fun HomeScreen(modifier: Modifier = Modifier, isOffline: Boolean, mainActivityVi val snackbarHostState = remember { SnackbarHostState() } val isMLabTestActive = mainActivityViewModel.isMLabTestActive.collectAsStateWithLifecycle() - val mlabPingResult = mainActivityViewModel.mLabPingResult.collectAsStateWithLifecycle() + val mlabRttResult = mainActivityViewModel.mlabRttResult.collectAsStateWithLifecycle() val mlabUploadResult = mainActivityViewModel.mlabUploadResult.collectAsStateWithLifecycle() val mlabDownloadResult = mainActivityViewModel.mlabDownloadResult.collectAsStateWithLifecycle() val signalStrength = mainActivityViewModel.signalStrengthResult.collectAsStateWithLifecycle() @@ -64,7 +61,7 @@ fun HomeScreen(modifier: Modifier = Modifier, isOffline: Boolean, mainActivityVi ConnectivityCard( label = "MLab", modifier = modifier, - pingResult = mlabPingResult.value, + rttValue = mlabRttResult.value, uploadResult = mlabUploadResult.value, downloadResult = mlabDownloadResult.value ) @@ -150,7 +147,7 @@ private fun SignalStrengthCard( private fun ConnectivityCard( label: String, modifier: Modifier = Modifier, - pingResult: PingResultState, + rttValue: Double, uploadResult: ConnectivityTestResult, downloadResult: ConnectivityTestResult, ) { @@ -188,25 +185,12 @@ private fun ConnectivityCard( } Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) { - var pingNum = "0" - var pingLoss = "0" - when(pingResult) { - is PingResultState.Success -> { - pingNum = pingResult.result.avg!! - pingLoss = pingResult.result.numLoss!! - } - is PingResultState.Error -> { - pingNum = "0" - pingLoss = "0" - if (pingResult.error.code != PingErrorCase.OK) { - Log.d("HOMEScreen", pingResult.error.message ?: "Error occurred") - // TODO: show error message - } - } - } - - DataEntry(icon = Rounded.NetworkPing, text = "$pingNum ms") - DataEntry(icon = Rounded.Cancel, text = "$pingLoss % loss") + // Format RTT to one decimal place + val formattedRtt = if (rttValue > 0) String.format("%.1f", rttValue) else "0.0" + + DataEntry(icon = Rounded.NetworkPing, text = "$formattedRtt ms") + // We no longer have packet loss data from ndt7, so we'll remove this or set it to 0 + DataEntry(icon = Rounded.Cancel, text = "0 % loss") } } } @@ -233,13 +217,13 @@ fun ConnectivityCardPreview() { Column { ConnectivityCard(label = "IperfRunner", - pingResult = PingResultState.Error(PingError(PingErrorCase.OK, null)), + rttValue = 15.2, uploadResult = ConnectivityTestResult.Result("1", Color.Blue), downloadResult = ConnectivityTestResult.Result("1", Color.Green) ) ConnectivityCard(label = "MLab", - pingResult = PingResultState.Error(PingError(PingErrorCase.OK, null)), + rttValue = 23.7, uploadResult = ConnectivityTestResult.Result("1", Color.Blue), downloadResult = ConnectivityTestResult.Result("1", Color.Green) ) From 1a4ee136399d806446ecd3b82360d173e879aef6 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Mon, 1 Sep 2025 19:56:43 -0700 Subject: [PATCH 07/19] fix: tcpInfo is null --- .../MainActivityViewModel.kt | 42 +++++++++++++++++-- .../features/mlab/MLabRunner.kt | 22 ++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index 0cf41d5..8c398d3 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -20,6 +20,7 @@ import com.lcl.lclmeasurementtool.telephony.SignalStrengthMonitor import com.lcl.lclmeasurementtool.util.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.serialization.SerializationException import kotlinx.serialization.decodeFromString @@ -29,6 +30,7 @@ import net.measurementlab.ndt7.android.NDTTest import okhttp3.ResponseBody import retrofit2.HttpException import java.io.ByteArrayOutputStream +import java.net.InetAddress import java.security.SecureRandom import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey @@ -270,9 +272,12 @@ class MainActivityViewModel @Inject constructor( when(it.type) { NDTTest.TestType.UPLOAD -> { // Extract RTT from TCPInfo if available during upload test - it.tcpInfo?.rtt?.let { rtt -> - _mlabRttResult.value = rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds + if (it.tcpInfo?.rtt != null) { + _mlabRttResult.value = it.tcpInfo.rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds Log.d(TAG, "RTT from Upload test: ${_mlabRttResult.value} ms") + } else { + // Fallback to a default RTT measurement + estimateRttFromSpeedTest() } _mLabUploadResult.value = when(it.status) { @@ -288,9 +293,12 @@ class MainActivityViewModel @Inject constructor( } NDTTest.TestType.DOWNLOAD -> { // Extract RTT from TCPInfo if available during download test - it.tcpInfo?.rtt?.let { rtt -> - _mlabRttResult.value = rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds + if (it.tcpInfo?.rtt != null) { + _mlabRttResult.value = it.tcpInfo.rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds Log.d(TAG, "RTT from Download test: ${_mlabRttResult.value} ms") + } else { + // Fallback to a default RTT measurement + estimateRttFromSpeedTest() } _mLabDownloadResult.value = when(it.status) { @@ -426,6 +434,32 @@ class MainActivityViewModel @Inject constructor( _mLabUploadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) _mLabDownloadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) } + + /** + * Estimates RTT using a ping to Google's DNS server as a fallback + * This is used when TCPInfo is not available from the ndt7 library + */ + private fun estimateRttFromSpeedTest() { + viewModelScope.launch(Dispatchers.IO) { + try { + // Simple RTT calculation using java's InetAddress.isReachable + val start = System.currentTimeMillis() + val isReachable = InetAddress.getByName("8.8.8.8").isReachable(5000) + val rtt = System.currentTimeMillis() - start + + if (isReachable) { + _mlabRttResult.value = rtt.toDouble() + Log.d(TAG, "Fallback RTT measurement: ${_mlabRttResult.value} ms") + } else { + Log.d(TAG, "Fallback RTT measurement failed") + _mlabRttResult.value = 100.0 // Default value if measurement fails + } + } catch (e: Exception) { + Log.e(TAG, "Error in fallback RTT measurement", e) + _mlabRttResult.value = 100.0 // Default value if measurement fails + } + } + } } sealed interface MainActivityUiState { diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index f6237de..b0a9d0f 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -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 @@ -12,13 +14,29 @@ import java.util.concurrent.TimeUnit class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): NDTTest(httpClient) { override fun onDownloadProgress(clientResponse: ClientResponse) { super.onDownloadProgress(clientResponse) + Log.d(TAG, "onDownloadProgress - measurement: ${clientResponse.measurement}, tcpInfo: ${clientResponse.measurement?.tcpInfo}") callback.onDownloadProgress(clientResponse) } override fun onUploadProgress(clientResponse: ClientResponse) { super.onUploadProgress(clientResponse) + Log.d(TAG, "onUploadProgress - measurement: ${clientResponse.measurement}, tcpInfo: ${clientResponse.measurement?.tcpInfo}") callback.onUploadProgress(clientResponse) } + + override fun onMeasurementDownloadProgress(measurement: Measurement) { + super.onMeasurementDownloadProgress(measurement) + Log.d(TAG, "onMeasurementDownloadProgress - tcpInfo: ${measurement.tcpInfo}") + // Note: We don't need to override this method to get RTT. The measurement is passed along + // in the ClientResponse through the onDownloadProgress callback. + } + + override fun onMeasurementUploadProgress(measurement: Measurement) { + super.onMeasurementUploadProgress(measurement) + Log.d(TAG, "onMeasurementUploadProgress - tcpInfo: ${measurement.tcpInfo}") + // Note: We don't need to override this method to get RTT. The measurement is passed along + // in the ClientResponse through the onUploadProgress callback. + } override fun onFinished( clientResponse: ClientResponse?, @@ -26,6 +44,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): testType: TestType ) { super.onFinished(clientResponse, error, testType) + Log.d(TAG, "onFinished - type: $testType, measurement: ${clientResponse?.measurement}, tcpInfo: ${clientResponse?.measurement?.tcpInfo}") callback.onFinish(clientResponse, error, testType) } @@ -38,6 +57,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client download is $speed") val measurement = clientResponse.measurement + Log.d(TAG, "Download measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) } @@ -45,6 +65,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client upload is $speed") val measurement = clientResponse.measurement + Log.d(TAG, "Upload measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) } @@ -56,6 +77,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): if (clientResponse != null) { Log.d(TAG, "client finish test $testType") val measurement = clientResponse.measurement + Log.d(TAG, "Finish measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") channel.trySend(MLabResult(DataConverter.convertToMbps(clientResponse), testType, null, MLabTestStatus.FINISHED, measurement?.tcpInfo)) } else { channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR)) From 68340faf92b482bcf5c458b5b9edd2d8d0b7d9f5 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Mon, 1 Sep 2025 20:09:00 -0700 Subject: [PATCH 08/19] Revert "fix: tcpInfo is null" This reverts commit 1a4ee136399d806446ecd3b82360d173e879aef6. --- .../MainActivityViewModel.kt | 42 ++----------------- .../features/mlab/MLabRunner.kt | 22 ---------- 2 files changed, 4 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index 8c398d3..0cf41d5 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -20,7 +20,6 @@ import com.lcl.lclmeasurementtool.telephony.SignalStrengthMonitor import com.lcl.lclmeasurementtool.util.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.serialization.SerializationException import kotlinx.serialization.decodeFromString @@ -30,7 +29,6 @@ import net.measurementlab.ndt7.android.NDTTest import okhttp3.ResponseBody import retrofit2.HttpException import java.io.ByteArrayOutputStream -import java.net.InetAddress import java.security.SecureRandom import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey @@ -272,12 +270,9 @@ class MainActivityViewModel @Inject constructor( when(it.type) { NDTTest.TestType.UPLOAD -> { // Extract RTT from TCPInfo if available during upload test - if (it.tcpInfo?.rtt != null) { - _mlabRttResult.value = it.tcpInfo.rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds + it.tcpInfo?.rtt?.let { rtt -> + _mlabRttResult.value = rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds Log.d(TAG, "RTT from Upload test: ${_mlabRttResult.value} ms") - } else { - // Fallback to a default RTT measurement - estimateRttFromSpeedTest() } _mLabUploadResult.value = when(it.status) { @@ -293,12 +288,9 @@ class MainActivityViewModel @Inject constructor( } NDTTest.TestType.DOWNLOAD -> { // Extract RTT from TCPInfo if available during download test - if (it.tcpInfo?.rtt != null) { - _mlabRttResult.value = it.tcpInfo.rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds + it.tcpInfo?.rtt?.let { rtt -> + _mlabRttResult.value = rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds Log.d(TAG, "RTT from Download test: ${_mlabRttResult.value} ms") - } else { - // Fallback to a default RTT measurement - estimateRttFromSpeedTest() } _mLabDownloadResult.value = when(it.status) { @@ -434,32 +426,6 @@ class MainActivityViewModel @Inject constructor( _mLabUploadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) _mLabDownloadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) } - - /** - * Estimates RTT using a ping to Google's DNS server as a fallback - * This is used when TCPInfo is not available from the ndt7 library - */ - private fun estimateRttFromSpeedTest() { - viewModelScope.launch(Dispatchers.IO) { - try { - // Simple RTT calculation using java's InetAddress.isReachable - val start = System.currentTimeMillis() - val isReachable = InetAddress.getByName("8.8.8.8").isReachable(5000) - val rtt = System.currentTimeMillis() - start - - if (isReachable) { - _mlabRttResult.value = rtt.toDouble() - Log.d(TAG, "Fallback RTT measurement: ${_mlabRttResult.value} ms") - } else { - Log.d(TAG, "Fallback RTT measurement failed") - _mlabRttResult.value = 100.0 // Default value if measurement fails - } - } catch (e: Exception) { - Log.e(TAG, "Error in fallback RTT measurement", e) - _mlabRttResult.value = 100.0 // Default value if measurement fails - } - } - } } sealed interface MainActivityUiState { diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index b0a9d0f..f6237de 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -4,9 +4,7 @@ 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 @@ -14,29 +12,13 @@ import java.util.concurrent.TimeUnit class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): NDTTest(httpClient) { override fun onDownloadProgress(clientResponse: ClientResponse) { super.onDownloadProgress(clientResponse) - Log.d(TAG, "onDownloadProgress - measurement: ${clientResponse.measurement}, tcpInfo: ${clientResponse.measurement?.tcpInfo}") callback.onDownloadProgress(clientResponse) } override fun onUploadProgress(clientResponse: ClientResponse) { super.onUploadProgress(clientResponse) - Log.d(TAG, "onUploadProgress - measurement: ${clientResponse.measurement}, tcpInfo: ${clientResponse.measurement?.tcpInfo}") callback.onUploadProgress(clientResponse) } - - override fun onMeasurementDownloadProgress(measurement: Measurement) { - super.onMeasurementDownloadProgress(measurement) - Log.d(TAG, "onMeasurementDownloadProgress - tcpInfo: ${measurement.tcpInfo}") - // Note: We don't need to override this method to get RTT. The measurement is passed along - // in the ClientResponse through the onDownloadProgress callback. - } - - override fun onMeasurementUploadProgress(measurement: Measurement) { - super.onMeasurementUploadProgress(measurement) - Log.d(TAG, "onMeasurementUploadProgress - tcpInfo: ${measurement.tcpInfo}") - // Note: We don't need to override this method to get RTT. The measurement is passed along - // in the ClientResponse through the onUploadProgress callback. - } override fun onFinished( clientResponse: ClientResponse?, @@ -44,7 +26,6 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): testType: TestType ) { super.onFinished(clientResponse, error, testType) - Log.d(TAG, "onFinished - type: $testType, measurement: ${clientResponse?.measurement}, tcpInfo: ${clientResponse?.measurement?.tcpInfo}") callback.onFinish(clientResponse, error, testType) } @@ -57,7 +38,6 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client download is $speed") val measurement = clientResponse.measurement - Log.d(TAG, "Download measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) } @@ -65,7 +45,6 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client upload is $speed") val measurement = clientResponse.measurement - Log.d(TAG, "Upload measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) } @@ -77,7 +56,6 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): if (clientResponse != null) { Log.d(TAG, "client finish test $testType") val measurement = clientResponse.measurement - Log.d(TAG, "Finish measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") channel.trySend(MLabResult(DataConverter.convertToMbps(clientResponse), testType, null, MLabTestStatus.FINISHED, measurement?.tcpInfo)) } else { channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR)) From 59626ce251d74eaa41b113074ef95ae34bdf0e72 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Mon, 1 Sep 2025 21:22:20 -0700 Subject: [PATCH 09/19] fix: rectify problems with measurement --- .../features/mlab/MLabRunner.kt | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index f6237de..4ee4dc0 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -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 @@ -12,20 +14,37 @@ import java.util.concurrent.TimeUnit class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): NDTTest(httpClient) { override fun onDownloadProgress(clientResponse: ClientResponse) { super.onDownloadProgress(clientResponse) + Log.d(TAG, "onDownloadProgress - measurement: ${clientResponse.measurement}, tcpInfo: ${clientResponse.measurement?.tcpInfo}") callback.onDownloadProgress(clientResponse) } override fun onUploadProgress(clientResponse: ClientResponse) { super.onUploadProgress(clientResponse) + Log.d(TAG, "onUploadProgress - measurement: ${clientResponse.measurement}, tcpInfo: ${clientResponse.measurement?.tcpInfo}") callback.onUploadProgress(clientResponse) } + override fun onMeasurementDownloadProgress(measurement: Measurement) { + super.onMeasurementDownloadProgress(measurement) + Log.d(TAG, "onMeasurementDownloadProgress - tcpInfo: ${measurement.tcpInfo}") + // Note: We don't need to override this method to get RTT. The measurement is passed along + // in the ClientResponse through the onDownloadProgress callback. + } + + override fun onMeasurementUploadProgress(measurement: Measurement) { + super.onMeasurementUploadProgress(measurement) + Log.d(TAG, "onMeasurementUploadProgress - tcpInfo: ${measurement.tcpInfo}") + // Note: We don't need to override this method to get RTT. The measurement is passed along + // in the ClientResponse through the onUploadProgress callback. + } + override fun onFinished( clientResponse: ClientResponse?, error: Throwable?, testType: TestType ) { super.onFinished(clientResponse, error, testType) + Log.d(TAG, "onFinished - type: $testType, measurement: ${clientResponse?.measurement}, tcpInfo: ${clientResponse?.measurement?.tcpInfo}") callback.onFinish(clientResponse, error, testType) } @@ -38,6 +57,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client download is $speed") val measurement = clientResponse.measurement + Log.d(TAG, "Download measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) } @@ -45,6 +65,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client upload is $speed") val measurement = clientResponse.measurement + Log.d(TAG, "Upload measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) } @@ -56,6 +77,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): if (clientResponse != null) { Log.d(TAG, "client finish test $testType") val measurement = clientResponse.measurement + Log.d(TAG, "Finish measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") channel.trySend(MLabResult(DataConverter.convertToMbps(clientResponse), testType, null, MLabTestStatus.FINISHED, measurement?.tcpInfo)) } else { channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR)) @@ -84,4 +106,3 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): } } } - From 9961960d37abf23612fd176ba290b46b0087e5d6 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Mon, 1 Sep 2025 22:47:22 -0700 Subject: [PATCH 10/19] fix: get the ping actually working by fixing callbacks --- .../MainActivityViewModel.kt | 16 +++++---- .../features/mlab/MLabCallback.kt | 3 ++ .../features/mlab/MLabRunner.kt | 33 +++++++++---------- .../lcl/lclmeasurementtool/ui/HomeScreen.kt | 27 ++++++++++----- .../ndt7/android/models/ClientResponse.kt | 3 +- .../ndt7/android/models/Measurement.kt | 2 ++ 6 files changed, 48 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index 0cf41d5..ebc8915 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -64,7 +64,7 @@ class MainActivityViewModel @Inject constructor( // Network Testing private val _isMLabTestActive = MutableStateFlow(false) - private var _mlabRttResult = MutableStateFlow(0.0) + private var _mlabRttResult = MutableStateFlow(ConnectivityTestResult()) private var _mLabUploadResult = MutableStateFlow(ConnectivityTestResult()) private var _mLabDownloadResult = MutableStateFlow(ConnectivityTestResult()) @@ -271,8 +271,9 @@ class MainActivityViewModel @Inject constructor( NDTTest.TestType.UPLOAD -> { // Extract RTT from TCPInfo if available during upload test it.tcpInfo?.rtt?.let { rtt -> - _mlabRttResult.value = rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds - Log.d(TAG, "RTT from Upload test: ${_mlabRttResult.value} ms") + val rttMs = rtt.toDouble() / 1000.0 // microseconds → milliseconds + _mlabRttResult.value = ConnectivityTestResult.Result(rttMs.toString(), Color.Black) + Log.d(TAG, "RTT from Upload test: $rttMs ms") } _mLabUploadResult.value = when(it.status) { @@ -289,8 +290,9 @@ class MainActivityViewModel @Inject constructor( NDTTest.TestType.DOWNLOAD -> { // Extract RTT from TCPInfo if available during download test it.tcpInfo?.rtt?.let { rtt -> - _mlabRttResult.value = rtt.toDouble() / 1000.0 // Convert from microseconds to milliseconds - Log.d(TAG, "RTT from Download test: ${_mlabRttResult.value} ms") + val rttMs = rtt.toDouble() / 1000.0 // microseconds → milliseconds + _mlabRttResult.value = ConnectivityTestResult.Result(rttMs.toString(), Color.Black) + Log.d(TAG, "RTT from Upload test: $rttMs ms") } _mLabDownloadResult.value = when(it.status) { @@ -363,7 +365,7 @@ class MainActivityViewModel @Inject constructor( it.second.deviceID, (_mLabUploadResult.value as ConnectivityTestResult.Result).result.toDouble(), (_mLabDownloadResult.value as ConnectivityTestResult.Result).result.toDouble(), - _mlabRttResult.value, // Use RTT from ndt7 TCPInfo + (_mlabRttResult.value as ConnectivityTestResult.Result).result.toDouble(), 0.0, // No packet loss information available from ndt7, defaulting to 0 ) @@ -422,7 +424,7 @@ class MainActivityViewModel @Inject constructor( } private fun resetMLabTestResult() { - _mlabRttResult.value = 0.0 + _mlabRttResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) _mLabUploadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) _mLabDownloadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) } diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabCallback.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabCallback.kt index 7b6ff41..126b9da 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabCallback.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabCallback.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index 4ee4dc0..aac5e63 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -14,28 +14,22 @@ import java.util.concurrent.TimeUnit class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): NDTTest(httpClient) { override fun onDownloadProgress(clientResponse: ClientResponse) { super.onDownloadProgress(clientResponse) - Log.d(TAG, "onDownloadProgress - measurement: ${clientResponse.measurement}, tcpInfo: ${clientResponse.measurement?.tcpInfo}") callback.onDownloadProgress(clientResponse) } override fun onUploadProgress(clientResponse: ClientResponse) { super.onUploadProgress(clientResponse) - Log.d(TAG, "onUploadProgress - measurement: ${clientResponse.measurement}, tcpInfo: ${clientResponse.measurement?.tcpInfo}") callback.onUploadProgress(clientResponse) } override fun onMeasurementDownloadProgress(measurement: Measurement) { super.onMeasurementDownloadProgress(measurement) - Log.d(TAG, "onMeasurementDownloadProgress - tcpInfo: ${measurement.tcpInfo}") - // Note: We don't need to override this method to get RTT. The measurement is passed along - // in the ClientResponse through the onDownloadProgress callback. + callback.onMeasurementDownloadProgress(measurement) } override fun onMeasurementUploadProgress(measurement: Measurement) { super.onMeasurementUploadProgress(measurement) - Log.d(TAG, "onMeasurementUploadProgress - tcpInfo: ${measurement.tcpInfo}") - // Note: We don't need to override this method to get RTT. The measurement is passed along - // in the ClientResponse through the onUploadProgress callback. + callback.onMeasurementUploadProgress(measurement) } override fun onFinished( @@ -44,7 +38,6 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): testType: TestType ) { super.onFinished(clientResponse, error, testType) - Log.d(TAG, "onFinished - type: $testType, measurement: ${clientResponse?.measurement}, tcpInfo: ${clientResponse?.measurement?.tcpInfo}") callback.onFinish(clientResponse, error, testType) } @@ -56,17 +49,23 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): override fun onDownloadProgress(clientResponse: ClientResponse) { val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client download is $speed") - val measurement = clientResponse.measurement - Log.d(TAG, "Download measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") - channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) + channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING)) } override fun onUploadProgress(clientResponse: ClientResponse) { val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client upload is $speed") - val measurement = clientResponse.measurement - Log.d(TAG, "Upload measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") - channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement?.tcpInfo)) + 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.DOWNLOAD, null, MLabTestStatus.RUNNING, measurement.tcpInfo)) } override fun onFinish( @@ -76,9 +75,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): ) { if (clientResponse != null) { Log.d(TAG, "client finish test $testType") - val measurement = clientResponse.measurement - Log.d(TAG, "Finish measurement: $measurement, tcpInfo: ${measurement?.tcpInfo}") - channel.trySend(MLabResult(DataConverter.convertToMbps(clientResponse), testType, null, MLabTestStatus.FINISHED, measurement?.tcpInfo)) + channel.trySend(MLabResult(DataConverter.convertToMbps(clientResponse), testType, null, MLabTestStatus.FINISHED)) } else { channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR)) } diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt index a1d6837..5a7a65c 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt @@ -147,7 +147,7 @@ private fun SignalStrengthCard( private fun ConnectivityCard( label: String, modifier: Modifier = Modifier, - rttValue: Double, + rttValue: ConnectivityTestResult, uploadResult: ConnectivityTestResult, downloadResult: ConnectivityTestResult, ) { @@ -185,13 +185,20 @@ private fun ConnectivityCard( } Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) { - // Format RTT to one decimal place - val formattedRtt = if (rttValue > 0) String.format("%.1f", rttValue) else "0.0" - + val formattedRtt = when (rttValue) { + is ConnectivityTestResult.Result -> { + val numeric = rttValue.result.toDoubleOrNull() ?: 0.0 + if (numeric > 0) String.format("%.1f", numeric) else "0.0" + } + else -> { + "Err" // or rttValue.error if you want to display the error message + } + } + DataEntry(icon = Rounded.NetworkPing, text = "$formattedRtt ms") - // We no longer have packet loss data from ndt7, so we'll remove this or set it to 0 DataEntry(icon = Rounded.Cancel, text = "0 % loss") } + } } Box(modifier = Modifier.fillMaxWidth()) { @@ -216,14 +223,16 @@ fun DataEntry(icon: ImageVector, text: String) { fun ConnectivityCardPreview() { Column { - ConnectivityCard(label = "IperfRunner", - rttValue = 15.2, + ConnectivityCard( + label = "IperfRunner", + rttValue = ConnectivityTestResult.Result("1", Color.Red), uploadResult = ConnectivityTestResult.Result("1", Color.Blue), downloadResult = ConnectivityTestResult.Result("1", Color.Green) ) - ConnectivityCard(label = "MLab", - rttValue = 23.7, + ConnectivityCard( + label = "MLab", + rttValue = ConnectivityTestResult.Result("1", Color.Red), uploadResult = ConnectivityTestResult.Result("1", Color.Blue), downloadResult = ConnectivityTestResult.Result("1", Color.Green) ) diff --git a/libndt7/src/main/java/net/measurementlab/ndt7/android/models/ClientResponse.kt b/libndt7/src/main/java/net/measurementlab/ndt7/android/models/ClientResponse.kt index 73d99bc..4c5a8c9 100644 --- a/libndt7/src/main/java/net/measurementlab/ndt7/android/models/ClientResponse.kt +++ b/libndt7/src/main/java/net/measurementlab/ndt7/android/models/ClientResponse.kt @@ -7,8 +7,7 @@ import kotlinx.serialization.Serializable data class ClientResponse( @SerialName("AppInfo") val appInfo: AppInfo, @SerialName("Origin") val origin: String = "client", - @SerialName("Test") val test: String, - @SerialName("Measurement") val measurement: Measurement? = null + @SerialName("Test") val test: String ) @Serializable diff --git a/libndt7/src/main/java/net/measurementlab/ndt7/android/models/Measurement.kt b/libndt7/src/main/java/net/measurementlab/ndt7/android/models/Measurement.kt index 679b3d7..40991ea 100644 --- a/libndt7/src/main/java/net/measurementlab/ndt7/android/models/Measurement.kt +++ b/libndt7/src/main/java/net/measurementlab/ndt7/android/models/Measurement.kt @@ -81,5 +81,7 @@ data class TCPInfo( @SerialName("BytesRetrans") val bytesRetrans: Long?, @SerialName("DSackDups") val dSackDups: Long?, @SerialName("ReordSeen") val reordSeen: Long?, + @SerialName("RcvOooPack") val rcvOooPack: Long?, + @SerialName("SndWnd") val sndWnd: Long?, @SerialName("ElapsedTime") val elapsedTime: Long? ) From 1a47a73dd459433f05790b4985885a6c268edd97 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Tue, 2 Sep 2025 21:22:26 -0700 Subject: [PATCH 11/19] fix: get upload to work --- .../lcl/lclmeasurementtool/MainActivityViewModel.kt | 10 +--------- .../lcl/lclmeasurementtool/features/mlab/MLabRunner.kt | 2 +- .../java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index ebc8915..a037b30 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -269,18 +269,10 @@ class MainActivityViewModel @Inject constructor( .collect{ when(it.type) { NDTTest.TestType.UPLOAD -> { - // Extract RTT from TCPInfo if available during upload 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 Upload test: $rttMs ms") - } - _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!!) @@ -292,7 +284,7 @@ class MainActivityViewModel @Inject constructor( 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 Upload test: $rttMs ms") + Log.d(TAG, "RTT from Download test: $rttMs ms") } _mLabDownloadResult.value = when(it.status) { diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index aac5e63..c7e6b17 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -65,7 +65,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): override fun onMeasurementUploadProgress(measurement: Measurement) { Log.d(TAG, "on measurement upload") - channel.trySend(MLabResult(null, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, measurement.tcpInfo)) +// channel.trySend(MLabResult(null, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement.tcpInfo)) } override fun onFinish( diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt index 5a7a65c..d17a8a0 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt @@ -191,7 +191,7 @@ private fun ConnectivityCard( if (numeric > 0) String.format("%.1f", numeric) else "0.0" } else -> { - "Err" // or rttValue.error if you want to display the error message + "0.0" // or rttValue.error if you want to display the error message } } From 7e355c06c86345c4337e3e9289c5ee2ca57e2c74 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Tue, 2 Sep 2025 21:47:51 -0700 Subject: [PATCH 12/19] fix: make sure upload results update on UI correctly --- .../MainActivityViewModel.kt | 68 ++++++++++++++----- .../features/mlab/MLabRunner.kt | 21 ++++-- 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index a037b30..080e768 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -256,27 +256,43 @@ 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 -> { @@ -287,22 +303,36 @@ class MainActivityViewModel @Inject constructor( Log.d(TAG, "RTT from Download test: $rttMs ms") } - _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!!) + 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 } } @@ -416,9 +446,11 @@ class MainActivityViewModel @Inject constructor( } private fun resetMLabTestResult() { + 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 } } diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index c7e6b17..fe5bd56 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -54,8 +54,9 @@ 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) { @@ -65,7 +66,7 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): override fun onMeasurementUploadProgress(measurement: Measurement) { Log.d(TAG, "on measurement upload") -// channel.trySend(MLabResult(null, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement.tcpInfo)) + channel.trySend(MLabResult(null, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement.tcpInfo)) } override fun onFinish( @@ -74,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() + } } } @@ -94,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) From eec3d2b938263ec83cb8e1c73e1be15d8d71010e Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Wed, 3 Sep 2025 21:06:41 -0700 Subject: [PATCH 13/19] feat: read the data from the database and generate the csv file when user taps export in the settings --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 2 +- .../model/viewmodels/SettingsViewModel.kt | 104 ++++++++++- .../lcl/lclmeasurementtool/ui/SettingsView.kt | 48 +++++- .../lclmeasurementtool/util/CsvExporter.kt | 162 ++++++++++++++++++ app/src/main/res/xml/file_paths.xml | 8 + 6 files changed, 313 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt create mode 100644 app/src/main/res/xml/file_paths.xml diff --git a/app/build.gradle b/app/build.gradle index 14401c5..c86fc17 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 261d468..380670d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,7 +52,7 @@ android:grantUriPermissions="true"> + android:resource="@xml/file_paths" /> = userDataRepository .userData @@ -25,6 +36,9 @@ class SettingsViewModel @Inject constructor( initialValue = false ) + // Holds the URI of the exported file after a successful export + private var _exportedFileUri: Uri? = null + val exportedFileUri get() = _exportedFileUri fun toggleShowData(showData: Boolean) { viewModelScope.launch { @@ -32,10 +46,94 @@ class SettingsViewModel @Inject constructor( } } - fun logout() { + fun logout() { viewModelScope.launch { userDataRepository.logout() } } + /** + * Export signal strength data to a CSV file + */ + fun exportSignalStrengthData(onComplete: (Uri?) -> Unit) { + viewModelScope.launch { + try { + val signalData = signalStrengthRepository.getAll().first() + if (signalData.isEmpty()) { + showToast("No signal strength data to export") + onComplete(null) + return@launch + } + + val uri = CsvExporter.exportSignalStrengthToCsv(getApplication(), signalData) + _exportedFileUri = uri + + if (uri != null) { + showToast("Signal strength data exported successfully") + shareFile(uri, "Signal Strength Data", "text/csv") + } else { + showToast("Failed to export signal strength data") + } + + onComplete(uri) + } catch (e: Exception) { + showToast("Error exporting signal strength data: ${e.message}") + onComplete(null) + } + } + } + + /** + * Export connectivity data to a CSV file + */ + fun exportConnectivityData(onComplete: (Uri?) -> Unit) { + viewModelScope.launch { + try { + val connectivityData = connectivityRepository.getAll().first() + if (connectivityData.isEmpty()) { + showToast("No connectivity data to export") + onComplete(null) + return@launch + } + + val uri = CsvExporter.exportConnectivityToCsv(getApplication(), connectivityData) + _exportedFileUri = uri + + if (uri != null) { + showToast("Connectivity data exported successfully") + shareFile(uri, "Speed Test Data", "text/csv") + } else { + showToast("Failed to export connectivity data") + } + + onComplete(uri) + } catch (e: Exception) { + showToast("Error exporting connectivity data: ${e.message}") + onComplete(null) + } + } + } + + /** + * Share a file using Android's share functionality + */ + private fun shareFile(uri: Uri, subject: String, mimeType: String) { + val context = getApplication() + val shareIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + putExtra(Intent.EXTRA_SUBJECT, subject) + type = mimeType + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + val chooserIntent = Intent.createChooser(shareIntent, "Share $subject") + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(chooserIntent) + } + + private fun showToast(message: String) { + Toast.makeText(getApplication(), message, Toast.LENGTH_SHORT).show() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt index 0b31e92..8aa4b77 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt @@ -34,13 +34,33 @@ fun SettingsDialog( onDismiss: () -> Unit, viewModel: SettingsViewModel = hiltViewModel() ) { - + val context = LocalContext.current val showData = viewModel.shouldShowData.collectAsStateWithLifecycle() + + // Export functions that handle the result + val exportSignalStrength: () -> Unit = { + viewModel.exportSignalStrengthData { uri -> + uri?.let { + // Optionally show a share intent or handle the URI + } + } + } + + val exportConnectivity: () -> Unit = { + viewModel.exportConnectivityData { uri -> + uri?.let { + // Optionally show a share intent or handle the URI + } + } + } + SettingDialog( onDismiss = onDismiss, toggleShowData = viewModel::toggleShowData, logout = viewModel::logout, - showData = showData.value + showData = showData.value, + exportSignalStrength = exportSignalStrength, + exportConnectivity = exportConnectivity ) } @@ -49,7 +69,9 @@ fun SettingDialog( onDismiss: () -> Unit, toggleShowData: (Boolean) -> Unit, logout: () -> Unit, - showData: Boolean + showData: Boolean, + exportSignalStrength: () -> Unit = {}, + exportConnectivity: () -> Unit = {} ) { AlertDialog( onDismissRequest = { onDismiss() }, @@ -62,7 +84,13 @@ fun SettingDialog( text = { Divider() Column(Modifier.verticalScroll(rememberScrollState())) { - SettingsPanel(onSelectShowData = {toggleShowData(!showData)}, onLogoutClicked = {logout()}, showData = showData) + SettingsPanel( + onSelectShowData = {toggleShowData(!showData)}, + onLogoutClicked = {logout()}, + showData = showData, + exportSignalStrength = exportSignalStrength, + exportConnectivity = exportConnectivity + ) Divider(Modifier.padding(top = 8.dp)) LinksPanel() VersionInfo() @@ -85,7 +113,9 @@ fun SettingDialog( private fun SettingsPanel( onSelectShowData: (Boolean) -> Unit, onLogoutClicked: () -> Unit, - showData: Boolean + showData: Boolean, + exportSignalStrength: () -> Unit = {}, + exportConnectivity: () -> Unit = {} ) { SettingsDialogSectionTitle(text = "General") Column(Modifier.fillMaxWidth(), @@ -102,8 +132,8 @@ private fun SettingsPanel( SettingsDialogSectionTitle(text = "Data Management") Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - ClickableRow(text = "Export Signal Strength Data", icon = Icons.Rounded.Download, onClick = {}) - ClickableRow(text = "Export Speed Test Data", icon = Icons.Rounded.Download, onClick = {}) + ClickableRow(text = "Export Signal Strength Data", icon = Icons.Rounded.Download, onClick = exportSignalStrength) + ClickableRow(text = "Export Speed Test Data", icon = Icons.Rounded.Download, onClick = exportConnectivity) } SettingsDialogSectionTitle(text = "Help") Column(Modifier.fillMaxWidth(), @@ -207,7 +237,9 @@ private fun PreviewSettingsDialog() { onDismiss = {}, toggleShowData = {}, logout = {}, - showData = false + showData = false, + exportSignalStrength = {}, + exportConnectivity = {} ) } // diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt b/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt new file mode 100644 index 0000000..589cd07 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt @@ -0,0 +1,162 @@ +package com.lcl.lclmeasurementtool.util + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import com.lcl.lclmeasurementtool.model.datamodel.ConnectivityReportModel +import com.lcl.lclmeasurementtool.model.datamodel.SignalStrengthReportModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.FileOutputStream +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* + +object CsvExporter { + + /** + * Export signal strength data to a CSV file + */ + suspend fun exportSignalStrengthToCsv( + context: Context, + data: List + ): Uri? = withContext(Dispatchers.IO) { + try { + val fileName = "signal_strength_export_${getCurrentDateTime()}.csv" + val csvContent = buildSignalStrengthCsv(data) + return@withContext saveToFile(context, fileName, csvContent) + } catch (e: IOException) { + e.printStackTrace() + return@withContext null + } + } + + /** + * Export connectivity data to a CSV file + */ + suspend fun exportConnectivityToCsv( + context: Context, + data: List + ): Uri? = withContext(Dispatchers.IO) { + try { + val fileName = "connectivity_export_${getCurrentDateTime()}.csv" + val csvContent = buildConnectivityCsv(data) + return@withContext saveToFile(context, fileName, csvContent) + } catch (e: IOException) { + e.printStackTrace() + return@withContext null + } + } + + /** + * Build CSV content for signal strength data + */ + private fun buildSignalStrengthCsv(data: List): String { + val csvBuilder = StringBuilder() + + // Add CSV header + csvBuilder.append("Timestamp,Latitude,Longitude,Cell ID,Device ID,Signal Strength (dBm),Signal Level\n") + + // Add data rows + data.forEach { signal -> + csvBuilder.append("${signal.timestamp},${signal.latitude},${signal.longitude},") + csvBuilder.append("${signal.cellId},${signal.deviceId},${signal.dbm},${signal.levelCode}\n") + } + + return csvBuilder.toString() + } + + /** + * Build CSV content for connectivity data + */ + private fun buildConnectivityCsv(data: List): String { + val csvBuilder = StringBuilder() + + // Add CSV header + csvBuilder.append("Timestamp,Latitude,Longitude,Cell ID,Device ID,Download Speed,Upload Speed,Ping,Packet Loss\n") + + // Add data rows + data.forEach { connectivity -> + csvBuilder.append("${connectivity.timestamp},${connectivity.latitude},${connectivity.longitude},") + csvBuilder.append("${connectivity.cellId},${connectivity.deviceId},${connectivity.downloadSpeed},") + csvBuilder.append("${connectivity.uploadSpeed},${connectivity.ping},${connectivity.packetLoss}\n") + } + + return csvBuilder.toString() + } + + /** + * Save CSV content to a file in the Downloads directory + * + * This approach uses ContentResolver for compatibility across Android versions + */ + private fun saveToFile(context: Context, fileName: String, content: String): Uri? { + return try { + // Use ContentValues and ContentResolver to create a file + val contentValues = android.content.ContentValues().apply { + put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, fileName) + put(android.provider.MediaStore.MediaColumns.MIME_TYPE, "text/csv") + + // For API 29+, use the Downloads collection + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, "Download") + put(android.provider.MediaStore.Downloads.IS_PENDING, 1) + } + } + + // Choose the right content URI based on Android version + val contentUri = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI + } else { + android.provider.MediaStore.Files.getContentUri("external") + } + + val uri = context.contentResolver.insert(contentUri, contentValues) + + uri?.let { + context.contentResolver.openOutputStream(it)?.use { outputStream -> + outputStream.write(content.toByteArray()) + outputStream.flush() + } + + // For API 29+, update IS_PENDING to 0 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + contentValues.clear() + contentValues.put(android.provider.MediaStore.Downloads.IS_PENDING, 0) + context.contentResolver.update(uri, contentValues, null, null) + } + } + + uri + } catch (e: Exception) { + e.printStackTrace() + + // Use a simpler fallback method if the MediaStore approach fails + try { + val downloadsDir = context.getExternalFilesDir(android.os.Environment.DIRECTORY_DOWNLOADS) + val file = java.io.File(downloadsDir, fileName) + + java.io.FileWriter(file).use { writer -> + writer.write(content) + } + + androidx.core.content.FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + file + ) + } catch (e2: Exception) { + e2.printStackTrace() + null + } + } + } + + /** + * Get current date and time formatted for filenames + */ + private fun getCurrentDateTime(): String { + val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + return dateFormat.format(Date()) + } +} diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..7a105c2 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,8 @@ + + + + + + + + From eb0e9d3438b1012932a9c491c4b4b0ae6f9461e1 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Sun, 7 Sep 2025 13:21:07 -0700 Subject: [PATCH 14/19] refactor: store specific metrics in MLabResult instead of whole TCPInfo --- .../MainActivityViewModel.kt | 14 +++++++-- .../features/mlab/MLabResult.kt | 5 ++-- .../features/mlab/MLabRunner.kt | 30 +++++++++++++++---- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index 080e768..c90a9d6 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -296,13 +296,21 @@ class MainActivityViewModel @Inject constructor( } } NDTTest.TestType.DOWNLOAD -> { - // Extract RTT from TCPInfo if available during download test - it.tcpInfo?.rtt?.let { rtt -> - val rttMs = rtt.toDouble() / 1000.0 // microseconds → milliseconds + // Use RTT directly from MLabResult if available + it.rttMs?.let { rttMs -> _mlabRttResult.value = ConnectivityTestResult.Result(rttMs.toString(), Color.Black) Log.d(TAG, "RTT from Download test: $rttMs ms") } + // Log additional metrics if available + it.minRttMs?.let { minRttMs -> + Log.d(TAG, "Min RTT from Download test: $minRttMs ms") + } + + it.packetLossPercent?.let { packetLoss -> + Log.d(TAG, "Packet loss from Download test: $packetLoss%") + } + if (it.speed != null) { Log.d(TAG, "Download speed update: ${it.speed}, status: ${it.status}") _mLabDownloadResult.value = when(it.status) { diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt index d135c93..bc0c0f5 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt @@ -1,14 +1,15 @@ 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 tcpInfo: TCPInfo? = null + val rttMs: Double? = null, + val minRttMs: Double? = null, + val packetLossPercent: Double? = null ) enum class MLabTestStatus { diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index fe5bd56..c0d15a7 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -49,24 +49,42 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): override fun onDownloadProgress(clientResponse: ClientResponse) { val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client download is $speed") - channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING)) + channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, null, null, null)) } override fun onUploadProgress(clientResponse: ClientResponse) { val speed = DataConverter.convertToMbps(clientResponse) if (speed != null && speed != "0.0") { - channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING)) + channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING, null, null, null)) } } override fun onMeasurementDownloadProgress(measurement: Measurement) { Log.d(TAG, "on measurement download") - channel.trySend(MLabResult(null, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, measurement.tcpInfo)) + val tcpInfo = measurement.tcpInfo + val rttMs = tcpInfo?.rtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds + val minRttMs = tcpInfo?.minRtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds + + // Calculate packet loss percentage if possible + val packetLossPercent = if (tcpInfo?.segsOut != null && tcpInfo.totalRetrans != null && tcpInfo.segsOut > 0) { + (tcpInfo.totalRetrans.toDouble() / tcpInfo.segsOut.toDouble()) * 100.0 + } else null + + channel.trySend(MLabResult(null, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, rttMs, minRttMs, packetLossPercent)) } override fun onMeasurementUploadProgress(measurement: Measurement) { Log.d(TAG, "on measurement upload") - channel.trySend(MLabResult(null, TestType.UPLOAD, null, MLabTestStatus.RUNNING, measurement.tcpInfo)) + val tcpInfo = measurement.tcpInfo + val rttMs = tcpInfo?.rtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds + val minRttMs = tcpInfo?.minRtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds + + // Calculate packet loss percentage if possible + val packetLossPercent = if (tcpInfo?.segsOut != null && tcpInfo.totalRetrans != null && tcpInfo.segsOut > 0) { + (tcpInfo.totalRetrans.toDouble() / tcpInfo.segsOut.toDouble()) * 100.0 + } else null + + channel.trySend(MLabResult(null, TestType.UPLOAD, null, MLabTestStatus.RUNNING, rttMs, minRttMs, packetLossPercent)) } override fun onFinish( @@ -77,10 +95,10 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): if (clientResponse != null) { val speed = DataConverter.convertToMbps(clientResponse) Log.d(TAG, "client finish test $testType with speed $speed") - channel.trySend(MLabResult(speed, testType, null, MLabTestStatus.FINISHED)) + channel.trySend(MLabResult(speed, testType, null, MLabTestStatus.FINISHED, null, null, null)) } else { Log.e(TAG, "Error during $testType test: ${error?.message}") - channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR)) + channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR, null, null, null)) } // Only close the channel after both download and upload tests are complete From 2fb2a96e8cc9da2e39c2f55b3a0f716fc5d3e6f2 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Sun, 7 Sep 2025 13:30:50 -0700 Subject: [PATCH 15/19] fix: remove smart casting --- .../features/mlab/MLabRunner.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index c0d15a7..4a74c94 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -66,8 +66,12 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): val minRttMs = tcpInfo?.minRtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds // Calculate packet loss percentage if possible - val packetLossPercent = if (tcpInfo?.segsOut != null && tcpInfo.totalRetrans != null && tcpInfo.segsOut > 0) { - (tcpInfo.totalRetrans.toDouble() / tcpInfo.segsOut.toDouble()) * 100.0 + val packetLossPercent = if (tcpInfo != null) { + val segsOut = tcpInfo.segsOut + val totalRetrans = tcpInfo.totalRetrans + if (segsOut != null && totalRetrans != null && segsOut > 0) { + (totalRetrans.toDouble() / segsOut.toDouble()) * 100.0 + } else null } else null channel.trySend(MLabResult(null, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, rttMs, minRttMs, packetLossPercent)) @@ -80,8 +84,12 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): val minRttMs = tcpInfo?.minRtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds // Calculate packet loss percentage if possible - val packetLossPercent = if (tcpInfo?.segsOut != null && tcpInfo.totalRetrans != null && tcpInfo.segsOut > 0) { - (tcpInfo.totalRetrans.toDouble() / tcpInfo.segsOut.toDouble()) * 100.0 + val packetLossPercent = if (tcpInfo != null) { + val segsOut = tcpInfo.segsOut + val totalRetrans = tcpInfo.totalRetrans + if (segsOut != null && totalRetrans != null && segsOut > 0) { + (totalRetrans.toDouble() / segsOut.toDouble()) * 100.0 + } else null } else null channel.trySend(MLabResult(null, TestType.UPLOAD, null, MLabTestStatus.RUNNING, rttMs, minRttMs, packetLossPercent)) From 3dea4e24519b82338fc2b49d9850bb1ed5ce5bf9 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Sun, 7 Sep 2025 13:34:23 -0700 Subject: [PATCH 16/19] refactor: min version is 30 so remove checks --- .../lclmeasurementtool/util/CsvExporter.kt | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt b/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt index 589cd07..e43e2bf 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt @@ -97,19 +97,13 @@ object CsvExporter { put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, fileName) put(android.provider.MediaStore.MediaColumns.MIME_TYPE, "text/csv") - // For API 29+, use the Downloads collection - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { - put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, "Download") - put(android.provider.MediaStore.Downloads.IS_PENDING, 1) - } + // Using Downloads collection (available since our min SDK is 33) + put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, "Download") + put(android.provider.MediaStore.Downloads.IS_PENDING, 1) } - // Choose the right content URI based on Android version - val contentUri = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { - android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI - } else { - android.provider.MediaStore.Files.getContentUri("external") - } + // Use Downloads collection content URI (available since our min SDK is 33) + val contentUri = android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI val uri = context.contentResolver.insert(contentUri, contentValues) @@ -119,12 +113,10 @@ object CsvExporter { outputStream.flush() } - // For API 29+, update IS_PENDING to 0 - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { - contentValues.clear() - contentValues.put(android.provider.MediaStore.Downloads.IS_PENDING, 0) - context.contentResolver.update(uri, contentValues, null, null) - } + // Update IS_PENDING to 0 after writing is complete + contentValues.clear() + contentValues.put(android.provider.MediaStore.Downloads.IS_PENDING, 0) + context.contentResolver.update(uri, contentValues, null, null) } uri From 78a17d67cfa2a6d02c001bef2efd622322d03c2e Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Sun, 7 Sep 2025 13:35:37 -0700 Subject: [PATCH 17/19] refactor: replace stack trace with logging --- .../main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt b/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt index e43e2bf..ca9d2b4 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/CsvExporter.kt @@ -2,6 +2,7 @@ package com.lcl.lclmeasurementtool.util import android.content.Context import android.net.Uri +import android.util.Log import androidx.documentfile.provider.DocumentFile import com.lcl.lclmeasurementtool.model.datamodel.ConnectivityReportModel import com.lcl.lclmeasurementtool.model.datamodel.SignalStrengthReportModel @@ -121,7 +122,7 @@ object CsvExporter { uri } catch (e: Exception) { - e.printStackTrace() + Log.e("CsvExporter", "Error saving file with MediaStore approach: ${e.message}", e) // Use a simpler fallback method if the MediaStore approach fails try { @@ -138,7 +139,7 @@ object CsvExporter { file ) } catch (e2: Exception) { - e2.printStackTrace() + Log.e("CsvExporter", "Error saving file with fallback method: ${e2.message}", e2) null } } From 4306449fe40ee9cef5936792eb9d6549b94fa917 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Sun, 7 Sep 2025 13:52:54 -0700 Subject: [PATCH 18/19] refactor: remove the wrapping around the imported functions from SettingsViewModel --- .../1.json | 150 ++++++++++++++++++ .../lcl/lclmeasurementtool/ui/SettingsView.kt | 18 +-- 2 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 app/schemas/com.lcl.lclmeasurementtool.database.db.AppDatabase/1.json diff --git a/app/schemas/com.lcl.lclmeasurementtool.database.db.AppDatabase/1.json b/app/schemas/com.lcl.lclmeasurementtool.database.db.AppDatabase/1.json new file mode 100644 index 0000000..6174b29 --- /dev/null +++ b/app/schemas/com.lcl.lclmeasurementtool.database.db.AppDatabase/1.json @@ -0,0 +1,150 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "0c7a8a929af48ae178cd610468177570", + "entities": [ + { + "tableName": "signal_strength_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `time_stamp` TEXT NOT NULL, `cellId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `signal_strength` INTEGER NOT NULL, `signal_strength_level` INTEGER NOT NULL, `reported` INTEGER NOT NULL, PRIMARY KEY(`time_stamp`))", + "fields": [ + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "time_stamp", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellId", + "columnName": "cellId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dbm", + "columnName": "signal_strength", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "levelCode", + "columnName": "signal_strength_level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reported", + "columnName": "reported", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time_stamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "connectivity_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `time_stamp` TEXT NOT NULL, `cellId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `upload_speed` REAL NOT NULL, `download_speed` REAL NOT NULL, `ping` REAL NOT NULL, `package_loss` REAL NOT NULL, `reported` INTEGER NOT NULL, PRIMARY KEY(`time_stamp`))", + "fields": [ + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "time_stamp", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellId", + "columnName": "cellId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploadSpeed", + "columnName": "upload_speed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "downloadSpeed", + "columnName": "download_speed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "ping", + "columnName": "ping", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "packetLoss", + "columnName": "package_loss", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "reported", + "columnName": "reported", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time_stamp" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0c7a8a929af48ae178cd610468177570')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt index 8aa4b77..af12cb5 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt @@ -38,22 +38,10 @@ fun SettingsDialog( val showData = viewModel.shouldShowData.collectAsStateWithLifecycle() // Export functions that handle the result - val exportSignalStrength: () -> Unit = { - viewModel.exportSignalStrengthData { uri -> - uri?.let { - // Optionally show a share intent or handle the URI - } - } - } - - val exportConnectivity: () -> Unit = { - viewModel.exportConnectivityData { uri -> - uri?.let { - // Optionally show a share intent or handle the URI - } - } - } + val exportSignalStrength = { viewModel.exportSignalStrengthData { _ -> } } + val exportConnectivity = { viewModel.exportConnectivityData { _ -> } } + SettingDialog( onDismiss = onDismiss, toggleShowData = viewModel::toggleShowData, From 25c02de6d8370700d989beffce9158415f9dcf63 Mon Sep 17 00:00:00 2001 From: ananyaa06 Date: Sun, 7 Sep 2025 14:09:09 -0700 Subject: [PATCH 19/19] refactor: make onDownloadProgress and onUploadProgress more consistent --- .../MainActivityViewModel.kt | 9 ---- .../features/mlab/MLabResult.kt | 4 +- .../features/mlab/MLabRunner.kt | 41 +++++++------------ 3 files changed, 15 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt index c90a9d6..c88143e 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -302,15 +302,6 @@ class MainActivityViewModel @Inject constructor( Log.d(TAG, "RTT from Download test: $rttMs ms") } - // Log additional metrics if available - it.minRttMs?.let { minRttMs -> - Log.d(TAG, "Min RTT from Download test: $minRttMs ms") - } - - it.packetLossPercent?.let { packetLoss -> - Log.d(TAG, "Packet loss from Download test: $packetLoss%") - } - if (it.speed != null) { Log.d(TAG, "Download speed update: ${it.speed}, status: ${it.status}") _mLabDownloadResult.value = when(it.status) { diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt index bc0c0f5..cf3d9cb 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt @@ -7,9 +7,7 @@ data class MLabResult( val type: NDTTest.TestType, val errorMsg: String?, val status: MLabTestStatus, - val rttMs: Double? = null, - val minRttMs: Double? = null, - val packetLossPercent: Double? = null + val rttMs: Double? = null ) enum class MLabTestStatus { diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt index 4a74c94..56d1ea6 100644 --- a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -48,14 +48,19 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): val callback = object : MLabCallback { override fun onDownloadProgress(clientResponse: ClientResponse) { val speed = DataConverter.convertToMbps(clientResponse) + val speedValue = speed?.toFloatOrNull() ?: 0f Log.d(TAG, "client download is $speed") - channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, null, null, null)) + if (speedValue > 0.1f) { + channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, null)) + } } override fun onUploadProgress(clientResponse: ClientResponse) { val speed = DataConverter.convertToMbps(clientResponse) - if (speed != null && speed != "0.0") { - channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING, null, null, null)) + val speedValue = speed?.toFloatOrNull() ?: 0f + Log.d(TAG, "client upload is $speed") + if (speedValue > 0.1f) { + channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING, null)) } } @@ -63,36 +68,16 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): Log.d(TAG, "on measurement download") val tcpInfo = measurement.tcpInfo val rttMs = tcpInfo?.rtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds - val minRttMs = tcpInfo?.minRtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds - - // Calculate packet loss percentage if possible - val packetLossPercent = if (tcpInfo != null) { - val segsOut = tcpInfo.segsOut - val totalRetrans = tcpInfo.totalRetrans - if (segsOut != null && totalRetrans != null && segsOut > 0) { - (totalRetrans.toDouble() / segsOut.toDouble()) * 100.0 - } else null - } else null - channel.trySend(MLabResult(null, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, rttMs, minRttMs, packetLossPercent)) + channel.trySend(MLabResult(null, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING, rttMs)) } override fun onMeasurementUploadProgress(measurement: Measurement) { Log.d(TAG, "on measurement upload") val tcpInfo = measurement.tcpInfo val rttMs = tcpInfo?.rtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds - val minRttMs = tcpInfo?.minRtt?.toDouble()?.div(1000.0) // Convert microseconds to milliseconds - - // Calculate packet loss percentage if possible - val packetLossPercent = if (tcpInfo != null) { - val segsOut = tcpInfo.segsOut - val totalRetrans = tcpInfo.totalRetrans - if (segsOut != null && totalRetrans != null && segsOut > 0) { - (totalRetrans.toDouble() / segsOut.toDouble()) * 100.0 - } else null - } else null - channel.trySend(MLabResult(null, TestType.UPLOAD, null, MLabTestStatus.RUNNING, rttMs, minRttMs, packetLossPercent)) + channel.trySend(MLabResult(null, TestType.UPLOAD, null, MLabTestStatus.RUNNING, rttMs)) } override fun onFinish( @@ -102,11 +87,13 @@ class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): ) { if (clientResponse != null) { val speed = DataConverter.convertToMbps(clientResponse) + val speedValue = speed?.toFloatOrNull() ?: 0f Log.d(TAG, "client finish test $testType with speed $speed") - channel.trySend(MLabResult(speed, testType, null, MLabTestStatus.FINISHED, null, null, null)) + // For finished tests, we report all results regardless of value + channel.trySend(MLabResult(speed, testType, null, MLabTestStatus.FINISHED, null)) } else { Log.e(TAG, "Error during $testType test: ${error?.message}") - channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR, null, null, null)) + channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR, null)) } // Only close the channel after both download and upload tests are complete