Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import net.measurementlab.ndt7.android.NDTTest
import okhttp3.ResponseBody
import retrofit2.HttpException
import retrofit2.Response
import com.lcl.lclmeasurementtool.constants.PingConstants
import com.lcl.lclmeasurementtool.features.ping.Ping
import com.lcl.lclmeasurementtool.features.ping.PingErrorCase
import com.lcl.lclmeasurementtool.sync.UploadWorker
import java.io.ByteArrayOutputStream
import java.security.SecureRandom
Expand Down Expand Up @@ -91,6 +94,11 @@ class MainActivityViewModel @Inject constructor(
private var _mLabUploadResult = MutableStateFlow(ConnectivityTestResult())
private var _mLabDownloadResult = MutableStateFlow(ConnectivityTestResult())

private val _pingPacketLoss = MutableStateFlow("-- %")
private val _pingRttResult = MutableStateFlow("-- ms")
val pingPacketLoss = _pingPacketLoss.asStateFlow()
val pingRttResult = _pingRttResult.asStateFlow()

var mlabRttResult = _mlabRttResult.asStateFlow()
var mlabUploadResult = _mLabUploadResult.asStateFlow()
var mlabDownloadResult = _mLabDownloadResult.asStateFlow()
Expand Down Expand Up @@ -288,7 +296,7 @@ class MainActivityViewModel @Inject constructor(
.onCompletion {
if (it != null) {
Log.e(TAG, "Error in MLab test: ${it.message}", it)
_isMLabTestActive.value = false
_isMLabTestActive.value = false
} else {
Log.d(TAG, "MLab test completed normally")
}
Expand All @@ -315,7 +323,7 @@ class MainActivityViewModel @Inject constructor(
} 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
_isMLabTestActive.value = false
}
}
NDTTest.TestType.DOWNLOAD -> {
Expand Down Expand Up @@ -344,7 +352,7 @@ class MainActivityViewModel @Inject constructor(
} 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
_isMLabTestActive.value = false
}
}
else -> {
Expand All @@ -354,7 +362,7 @@ class MainActivityViewModel @Inject constructor(
}
} catch (e: Exception) {
Log.e(TAG, "Exception during MLab test", e)
_isMLabTestActive.value = false
_isMLabTestActive.value = false
}
}

Expand Down Expand Up @@ -382,6 +390,22 @@ class MainActivityViewModel @Inject constructor(

ensureActive()

// Run ping alongside MLab to get packet loss
val pingResult = try {
Ping.start(
address = PingConstants.PING_SERVER_ADDRESS,
times = PingConstants.PING_TIMES,
timeout = PingConstants.PING_TIMEOUT_MS,
)
} catch (e: Exception) { null }
if (pingResult != null && pingResult.error.code == PingErrorCase.OK) {
_pingRttResult.value = "${pingResult.avg ?: "--"} ms"
_pingPacketLoss.value = "${pingResult.numLoss ?: "--"} %"
} else {
_pingRttResult.value = "-- ms"
_pingPacketLoss.value = "-- %"
}

_isMLabTestActive.value = false
Log.d(TAG, "upload, download are finished. isMLabTestActive.value=${isMLabTestActive.value}")
val curTime = TimeUtil.getCurrentTime()
Expand Down Expand Up @@ -418,7 +442,7 @@ class MainActivityViewModel @Inject constructor(
(_mLabUploadResult.value as ConnectivityTestResult.Result).result.toDouble(),
(_mLabDownloadResult.value as ConnectivityTestResult.Result).result.toDouble(),
(_mlabRttResult.value as ConnectivityTestResult.Result).result.toDouble(),
0.0, // No packet loss information available from ndt7, defaulting to 0
_pingPacketLoss.value.replace(" %", "").toDoubleOrNull() ?: 0.0,
)

saveToDB(signalStrengthReportModel, connectivityReportModel)
Expand Down Expand Up @@ -495,6 +519,8 @@ class MainActivityViewModel @Inject constructor(
_mlabRttResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray)
_mLabUploadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray)
_mLabDownloadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray)
_pingPacketLoss.value = "-- %"
_pingRttResult.value = "-- ms"
_isMLabTestActive.value = false
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.lcl.lclmeasurementtool.constants

object PingConstants {
// Keep google.com as default host while preserving explicit host:port parsing.
const val PING_SERVER_ADDRESS = "google.com:443"
const val PING_TIMES = 5
const val PING_TIMEOUT_MS = 3000L
}
217 changes: 166 additions & 51 deletions app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.Icons.Rounded
import androidx.compose.material.icons.filled.NetworkCheck
Expand All @@ -21,7 +22,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
Expand All @@ -33,6 +33,14 @@ import com.lcl.lclmeasurementtool.MainActivityViewModel
import com.lcl.lclmeasurementtool.SignalStrengthResult
import kotlinx.coroutines.cancel

private val DashboardSurface = Color(0xFFF5F7FB)
private val DashboardTile = Color(0xFFFFFFFF)
private val DashboardBorder = Color(0xFFE3E8F3)
private val DashboardTextMuted = Color(0xFF677085)
private val DashboardTextStrong = Color(0xFF1C2434)
private val DashboardLive = Color(0xFF1FA56A)
private val DashboardPending = Color(0xFF7A8090)

@Composable
fun HomeRoute(isOffline: Boolean, mainActivityViewModel: MainActivityViewModel) {
HomeScreen(isOffline = isOffline, mainActivityViewModel = mainActivityViewModel)
Expand All @@ -48,8 +56,9 @@ fun HomeScreen(modifier: Modifier = Modifier, isOffline: Boolean, mainActivityVi
val mlabRttResult = mainActivityViewModel.mlabRttResult.collectAsStateWithLifecycle()
val mlabUploadResult = mainActivityViewModel.mlabUploadResult.collectAsStateWithLifecycle()
val mlabDownloadResult = mainActivityViewModel.mlabDownloadResult.collectAsStateWithLifecycle()
val pingPacketLoss = mainActivityViewModel.pingPacketLoss.collectAsStateWithLifecycle()
val pingRttResult = mainActivityViewModel.pingRttResult.collectAsStateWithLifecycle()
val signalStrength = mainActivityViewModel.signalStrengthResult.collectAsStateWithLifecycle()
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()

Box(modifier = Modifier.fillMaxSize()) {
Expand All @@ -59,6 +68,8 @@ fun HomeScreen(modifier: Modifier = Modifier, isOffline: Boolean, mainActivityVi
) {
SignalStrengthCard(modifier = modifier, signalStrengthResult = signalStrength.value)
ConnectivityCard(
packetLoss = pingPacketLoss.value,
pingRtt = pingRttResult.value,
label = "MLab",
modifier = modifier,
rttValue = mlabRttResult.value,
Expand Down Expand Up @@ -113,25 +124,28 @@ private fun SignalStrengthCard(

val (dbm, level) = signalStrengthResult

Card(colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceVariant),
Card(
colors = CardDefaults.cardColors(DashboardSurface),
border = androidx.compose.foundation.BorderStroke(1.dp, DashboardBorder),
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.padding(horizontal = 10.dp, vertical = 10.dp)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.padding(20.dp)
modifier = modifier.padding(horizontal = 16.dp, vertical = 18.dp)
) {

Icon(modifier = modifier,
imageVector = Filled.SignalCellularAlt,
contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text(text = "Signal Strength:", fontSize = fontSize)
Text(text = "Signal Strength:", fontSize = fontSize, color = DashboardTextStrong)
Spacer(modifier = Modifier.width(4.dp))
Text(text = "$dbm", fontWeight = FontWeight.Bold, fontSize = fontSize)
Text(text = "$dbm", fontWeight = FontWeight.Bold, fontSize = fontSize, color = DashboardTextStrong)
Spacer(modifier = Modifier.width(4.dp))
Text(text = "dBm", fontSize = fontSize)
Text(text = "dBm", fontSize = fontSize, color = DashboardTextMuted)
Spacer(modifier = Modifier.width(20.dp))
Box(modifier = Modifier
.size(10.dp)
Expand All @@ -150,61 +164,162 @@ private fun ConnectivityCard(
rttValue: ConnectivityTestResult,
uploadResult: ConnectivityTestResult,
downloadResult: ConnectivityTestResult,
packetLoss: String = "-- %",
pingRtt: String = "-- ms",
) {
Card(colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceVariant),
val uploadText = when (uploadResult) {
is ConnectivityTestResult.Result -> "${uploadResult.result} Mbps"
else -> "--"
}
val downloadText = when (downloadResult) {
is ConnectivityTestResult.Result -> "${downloadResult.result} Mbps"
else -> "--"
}
val mlabRttText = when (rttValue) {
is ConnectivityTestResult.Result -> {
val numeric = rttValue.result.toDoubleOrNull() ?: 0.0
if (numeric > 0) "${String.format("%.1f", numeric)} ms" else "--"
}
else -> "--"
}
val hasLivePing = pingRtt != "-- ms" && packetLoss != "-- %"

Card(
colors = CardDefaults.cardColors(DashboardSurface),
border = androidx.compose.foundation.BorderStroke(1.dp, DashboardBorder),
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.padding(horizontal = 10.dp, vertical = 10.dp)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.padding(start = 12.dp, end = 12.dp, top = 12.dp)
Column(
modifier = modifier.padding(14.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {

Icon(modifier = modifier.padding(end = 12.dp),
imageVector = Filled.NetworkCheck,
contentDescription = null)
Column {
Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
when (uploadResult) {
is ConnectivityTestResult.Result -> {
DataEntry(icon = Rounded.CloudUpload, text = "${uploadResult.result} Mbps")
}
else -> DataEntry(icon = Rounded.CloudUpload, text = "0.0 Mbps")
}


when (downloadResult) {
is ConnectivityTestResult.Result -> {
DataEntry(icon = Rounded.CloudDownload, text = "${downloadResult.result} Mbps")
}
else -> {
DataEntry(icon = Rounded.CloudDownload, text = "0.0 Mbps")
}
}

Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Filled.NetworkCheck,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = "Mobile Connectivity",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = DashboardTextStrong
)
}
Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
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 -> {
"0.0" // or rttValue.error if you want to display the error message
}
}

DataEntry(icon = Rounded.NetworkPing, text = "$formattedRtt ms")
DataEntry(icon = Rounded.Cancel, text = "0 % loss")
Surface(
shape = RoundedCornerShape(999.dp),
color = if (hasLivePing) DashboardLive.copy(alpha = 0.16f) else DashboardPending.copy(alpha = 0.16f)
) {
Text(
text = if (hasLivePing) "Live" else "Pending",
style = MaterialTheme.typography.labelMedium,
color = if (hasLivePing) DashboardLive else DashboardPending,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 3.dp)
)
}
}

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
DashboardMetricTile(
icon = Rounded.CloudDownload,
label = "Download",
value = downloadText,
modifier = Modifier.weight(1f)
)
DashboardMetricTile(
icon = Rounded.CloudUpload,
label = "Upload",
value = uploadText,
modifier = Modifier.weight(1f)
)
}

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
DashboardMetricTile(
icon = Rounded.NetworkPing,
label = "MLab Latency",
value = mlabRttText,
modifier = Modifier.weight(1f)
)
DashboardMetricTile(
icon = Rounded.Cancel,
label = "Packet Loss",
value = packetLoss,
modifier = Modifier.weight(1f)
)
}

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
DashboardMetricTile(
icon = Rounded.NetworkPing,
label = "Custom Ping",
value = pingRtt,
modifier = Modifier.fillMaxWidth(0.49f),
)
}

Text(
text = "Powered by $label",
fontWeight = FontWeight.Thin,
fontSize = 10.sp,
color = DashboardTextMuted,
modifier = Modifier
.align(Alignment.End)
.padding(end = 10.dp, bottom = 4.dp)
)
}
Box(modifier = Modifier.fillMaxWidth()) {
Text(text = "Powered by $label", fontWeight = FontWeight.Thin, fontSize = 10.sp, modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 10.dp, bottom = 4.dp))
}
}

@Composable
private fun DashboardMetricTile(
icon: ImageVector,
label: String,
value: String,
modifier: Modifier = Modifier,
) {
Card(
colors = CardDefaults.cardColors(DashboardTile),
border = androidx.compose.foundation.BorderStroke(1.dp, DashboardBorder),
shape = RoundedCornerShape(12.dp),
modifier = modifier
) {
Column(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = DashboardTextMuted
)
}
Text(
text = value,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
color = DashboardTextStrong
)
}
}
}
Expand Down