Skip to content

Commit

Permalink
Adds a display overlay on the map with current serving cell information
Browse files Browse the repository at this point in the history
- Need to handle SIM swaps while on the map and also signal info from multiple subscriptions
  • Loading branch information
christianrowlands committed Jul 25, 2024
1 parent da5a6a6 commit 74b1cee
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
package com.craxiom.networksurvey.model;

import com.craxiom.messaging.GsmRecord;
import com.craxiom.messaging.GsmRecordData;
import com.craxiom.messaging.LteRecord;
import com.craxiom.messaging.LteRecordData;
import com.craxiom.messaging.NrRecord;
import com.craxiom.messaging.NrRecordData;
import com.craxiom.messaging.UmtsRecord;
import com.craxiom.messaging.UmtsRecordData;
import com.google.protobuf.GeneratedMessageV3;

import java.util.Objects;

/**
* Wraps the various cellular records so that we can include a variable that specifies which record type it is.
*
Expand All @@ -11,10 +21,61 @@ public class CellularRecordWrapper
{
public final CellularProtocol cellularProtocol;
public final GeneratedMessageV3 cellularRecord;
private final int hash;
private final String comparableString;

public CellularRecordWrapper(CellularProtocol cellularProtocol, GeneratedMessageV3 cellularRecord)
{
this.cellularProtocol = cellularProtocol;
this.cellularRecord = cellularRecord;

comparableString = getComparableString(this);
hash = Objects.hash(cellularProtocol, comparableString);
}

@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

CellularRecordWrapper that = (CellularRecordWrapper) o;

if (cellularProtocol != that.cellularProtocol) return false;
return comparableString.equals(that.comparableString);
}

@Override
public int hashCode()
{
return hash;
}

private static String getComparableString(CellularRecordWrapper wrapper)
{
return switch (wrapper.cellularProtocol)
{
case GSM ->
{
GsmRecordData gsmData = ((GsmRecord) wrapper.cellularRecord).getData();
yield "" + gsmData.getMcc().getValue() + gsmData.getMnc().getValue() + gsmData.getLac().getValue() + gsmData.getCi().getValue();
}
case UMTS ->
{
UmtsRecordData umtsData = ((UmtsRecord) wrapper.cellularRecord).getData();
yield "" + umtsData.getMcc().getValue() + umtsData.getMnc().getValue() + umtsData.getLac().getValue() + umtsData.getCid().getValue();
}
case LTE ->
{
LteRecordData lteData = ((LteRecord) wrapper.cellularRecord).getData();
yield "" + lteData.getMcc().getValue() + lteData.getMnc().getValue() + lteData.getTac().getValue() + lteData.getEci().getValue();
}
case NR ->
{
NrRecordData nrData = ((NrRecord) wrapper.cellularRecord).getData();
yield "" + nrData.getMcc().getValue() + nrData.getMnc().getValue() + nrData.getTac().getValue() + nrData.getNci().getValue();
}
default -> "";
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
Expand All @@ -38,14 +39,22 @@ import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.craxiom.messaging.CdmaRecord
import com.craxiom.messaging.GsmRecord
import com.craxiom.messaging.LteRecord
import com.craxiom.messaging.NrRecord
import com.craxiom.messaging.UmtsRecord
import com.craxiom.networksurvey.BuildConfig
import com.craxiom.networksurvey.R
import com.craxiom.networksurvey.model.CellularProtocol
import com.craxiom.networksurvey.ui.cellular.model.CustomLocationOverlay
import com.craxiom.networksurvey.ui.cellular.model.FollowMyLocationChangeListener
import com.craxiom.networksurvey.ui.cellular.model.ServingCellInfo
import com.craxiom.networksurvey.ui.cellular.model.ServingSignalInfo
import com.craxiom.networksurvey.ui.cellular.model.TowerMapViewModel
import com.craxiom.networksurvey.ui.cellular.model.TowerMarker
import com.google.gson.annotations.SerializedName
import com.google.protobuf.GeneratedMessageV3
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
Expand Down Expand Up @@ -96,6 +105,10 @@ internal fun TowerMapScreen(viewModel: TowerMapViewModel = viewModel()) {

val missingApiKey = BuildConfig.NS_API_KEY.isEmpty()

val servingCells by viewModel.servingCells.collectAsStateWithLifecycle()
var selectedSimIndex by remember { mutableIntStateOf(-1) }
val servingCellSignals by viewModel.servingSignals.collectAsStateWithLifecycle()

val options = listOf(
CellularProtocol.GSM.name,
CellularProtocol.CDMA.name,
Expand Down Expand Up @@ -203,6 +216,38 @@ internal fun TowerMapScreen(viewModel: TowerMapViewModel = viewModel()) {
})
}
}

Column(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(16.dp)
) {
if (servingCells.size > 1) {
// Only show the drop down if there is more than one option
SimCardDropdown(servingCells, selectedSimIndex) { newIndex ->
selectedSimIndex = newIndex
}
}

// Display the serving cell info for the selected SIM card
if (servingCells.isNotEmpty()) {
if (servingCells.size == 1) {
ServingCellInfoDisplay(
servingCells.values.first(),
servingCellSignals
)
} else {
if (selectedSimIndex == -1) {
// Default to the first key if a SIM card has not been selected
selectedSimIndex = servingCells.keys.first()
}
ServingCellInfoDisplay(
servingCells[selectedSimIndex],
servingCellSignals
)
}
}
}
}
}

Expand Down Expand Up @@ -310,6 +355,73 @@ internal fun OsmdroidMapView(
)
}

@Composable
fun SimCardDropdown(
servingCells: HashMap<Int, ServingCellInfo>,
selectedSimIndex: Int,
onSimSelected: (Int) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
val simOptions = servingCells.keys.toList() // Get SIM card indices

// Dropdown button for selecting SIM card
Button(onClick = { expanded = true }) {
Text(text = "SIM Card $selectedSimIndex")
}

DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
simOptions.forEachIndexed { _, simIndex ->
DropdownMenuItem(
text = { Text(text = "SIM Card $simIndex") },
onClick = {
onSimSelected(simIndex)
expanded = false
}
)
}
}
}

@Composable
fun ServingCellInfoDisplay(cellInfo: ServingCellInfo?, servingSignalInfo: ServingSignalInfo) {
Column(
modifier = Modifier
.background(Color(0x80EEEEEE))
.padding(16.dp),
horizontalAlignment = Alignment.Start
) {
Text(
text = "Serving Cell Info",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))

if (cellInfo != null) {
val servingCell = cellInfo.servingCell ?: return Text(
"No serving cell found",
color = MaterialTheme.colorScheme.primary
)
val record = servingCell.cellularRecord

// Display technology and signal strengths based on CellularRecord
Text(
"Technology: ${servingCell.cellularProtocol}",
color = MaterialTheme.colorScheme.primary
)
Text("$servingSignalInfo", color = MaterialTheme.colorScheme.primary)

val servingCellDisplayString = getServingCellDisplayString(record)
Text(servingCellDisplayString, color = MaterialTheme.colorScheme.primary)
} else {
Text("No serving cell info available", color = MaterialTheme.colorScheme.primary)
}
}
}

/**
* Moves the map view to the user's current location.
*/
Expand Down Expand Up @@ -519,6 +631,35 @@ private fun calculateArea(bounds: BoundingBox): Double {
return width * height // area in square meters
}

private fun getServingCellDisplayString(message: GeneratedMessageV3): String {
return when (message) {
is GsmRecord -> {
"MCC: ${message.data.mcc.value}\nMNC: ${message.data.mnc.value}\nLAC: ${message.data.lac.value}\nCellId: ${message.data.ci.value}"
}

is CdmaRecord -> {
"SID: ${message.data.sid.value}\nNID: ${message.data.nid.value}\nBSID: ${message.data.bsid.value}"
}

is UmtsRecord -> {
"MCC: ${message.data.mcc.value}\nMNC: ${message.data.mnc.value}\nLAC: ${message.data.lac.value}\nCellId: ${message.data.cid.value}"
}

is LteRecord -> {
"MCC: ${message.data.mcc.value}\nMNC: ${message.data.mnc.value}\nTAC: ${message.data.tac.value}\nECI: ${message.data.eci.value}"
}

is NrRecord -> {
"MCC: ${message.data.mcc.value}\nMNC: ${message.data.mnc.value}\nTAC: ${message.data.tac.value}\nNCI: ${message.data.nci.value}"
}

else -> {
"Unknown Protocol"
}
}
}


@Composable
fun CircleButtonWithLine(
isFollowing: Boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.craxiom.networksurvey.ui.cellular.model

import com.craxiom.networksurvey.model.CellularProtocol
import java.io.Serializable

data class ServingSignalInfo(
val cellularProtocol: CellularProtocol,
val signalOne: Int,
val signalTwo: Int,
) : Serializable {
override fun toString(): String {
val protocolSpecificIdentifier = when (cellularProtocol) {
CellularProtocol.GSM -> "RSSI: $signalOne"
CellularProtocol.CDMA -> "ECIO: $signalOne"
CellularProtocol.UMTS -> "RSSI: $signalOne\nRSCP: $signalTwo"
CellularProtocol.LTE -> "RSRP: $signalOne\nRSRQ: $signalTwo"
CellularProtocol.NR -> "SS-RSRP: $signalOne\nSS-RSRQ: $signalTwo"
else -> "Unknown: $signalOne"
}
return protocolSpecificIdentifier
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.craxiom.networksurvey.ui.ASignalChartViewModel
import com.craxiom.networksurvey.util.CellularUtils
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.osmdroid.bonuspack.clustering.RadiusMarkerClusterer
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
Expand All @@ -16,6 +17,7 @@ import org.osmdroid.views.overlay.FolderOverlay
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
import timber.log.Timber
import java.util.Objects

/**
* The view model for the Tower Map screen.
Expand All @@ -29,6 +31,9 @@ internal class TowerMapViewModel : ASignalChartViewModel() {
val subIdToServingCellLocations = HashMap<Int, GeoPoint>()
var myLocation: Location? = null

private var _servingSignals = MutableStateFlow(ServingSignalInfo(CellularProtocol.NONE, 0, 0))
val servingSignals = _servingSignals.asStateFlow()

lateinit var mapView: MapView
lateinit var gpsMyLocationProvider: GpsMyLocationProvider
private var hasMapLocationBeenSet = false
Expand Down Expand Up @@ -110,13 +115,27 @@ internal class TowerMapViewModel : ASignalChartViewModel() {
it?.cellularRecord != null && CellularUtils.isServingCell(it.cellularRecord)

}

updateServingCellSignals(servingCellRecord, subscriptionId)

// No need to update the serving cell if it is the same as the current serving cell. This
// prevents a map refresh which is expensive.
val currentServingCell = _servingCells.value[subscriptionId]
if (Objects.equals(currentServingCell?.servingCell, servingCellRecord)) return

if (servingCellRecord == null) {
_servingCells.value.remove(subscriptionId)
return
_servingCells.update { map ->
map.remove(subscriptionId)
map
}
} else {
_servingCells.update { oldMap ->
val newMap = HashMap(oldMap)
newMap[subscriptionId] = ServingCellInfo(servingCellRecord, subscriptionId)
newMap
}
}

_servingCells.value[subscriptionId] = ServingCellInfo(servingCellRecord, subscriptionId)

recreateOverlaysFromTowerData(mapView, false)
}

Expand Down Expand Up @@ -193,4 +212,17 @@ internal class TowerMapViewModel : ASignalChartViewModel() {
polyline.setPoints(pathPoints)
}
}

private fun updateServingCellSignals(
servingCellRecord: CellularRecordWrapper?,
subscriptionId: Int
) {
if (servingCellRecord == null) {
_servingSignals.value = ServingSignalInfo(CellularProtocol.NONE, 0, 0)
return
}

val servingSignalInfo = CellularUtils.getSignalInfo(servingCellRecord)
_servingSignals.value = servingSignalInfo
}
}
Loading

0 comments on commit 74b1cee

Please sign in to comment.