diff --git a/networksurvey/build.gradle b/networksurvey/build.gradle index 80d255f9..dac9e521 100644 --- a/networksurvey/build.gradle +++ b/networksurvey/build.gradle @@ -133,6 +133,7 @@ dependencies { debugImplementation "androidx.compose.ui:ui-tooling" implementation "androidx.compose.ui:ui-tooling-preview" implementation "androidx.lifecycle:lifecycle-viewmodel-compose" + implementation "androidx.lifecycle:lifecycle-runtime-compose" implementation "com.google.accompanist:accompanist-themeadapter-material:0.28.0" // Only include firebase in the google play build diff --git a/networksurvey/src/main/java/com/craxiom/networksurvey/fragments/MyWifiNetworkRecyclerViewAdapter.java b/networksurvey/src/main/java/com/craxiom/networksurvey/fragments/MyWifiNetworkRecyclerViewAdapter.java index 061a0d0c..ab42ed84 100644 --- a/networksurvey/src/main/java/com/craxiom/networksurvey/fragments/MyWifiNetworkRecyclerViewAdapter.java +++ b/networksurvey/src/main/java/com/craxiom/networksurvey/fragments/MyWifiNetworkRecyclerViewAdapter.java @@ -17,6 +17,7 @@ import com.craxiom.networksurvey.constants.WifiBeaconMessageConstants; import com.craxiom.networksurvey.model.WifiNetwork; import com.craxiom.networksurvey.model.WifiRecordWrapper; +import com.craxiom.networksurvey.util.ColorUtils; import timber.log.Timber; @@ -69,9 +70,9 @@ public void onBindViewHolder(final ViewHolder holder, int position) if (data.hasSignalStrength()) { - final float signalStrength = data.getSignalStrength().getValue(); + final int signalStrength = (int) data.getSignalStrength().getValue(); holder.signalStrength.setText(context.getString(R.string.dbm_value, String.valueOf(signalStrength))); - holder.signalStrength.setTextColor(context.getResources().getColor(getColorForSignalStrength(signalStrength), null)); + holder.signalStrength.setTextColor(context.getResources().getColor(ColorUtils.getColorForWifiSignalStrength(signalStrength), null)); } else { holder.signalStrength.setText(""); @@ -92,33 +93,6 @@ public int getItemCount() return wifiRecords.size(); } - /** - * @param signalStrength The signal strength value in dBm. - * @return The resource ID for the color that should be used for the signal strength text. - */ - private int getColorForSignalStrength(float signalStrength) - { - final int colorResourceId; - if (signalStrength > -60) - { - colorResourceId = R.color.rssi_green; - } else if (signalStrength > -70) - { - colorResourceId = R.color.rssi_yellow; - } else if (signalStrength > -80) - { - colorResourceId = R.color.rssi_orange; - } else if (signalStrength > -90) - { - colorResourceId = R.color.rssi_red; - } else - { - colorResourceId = R.color.rssi_deep_red; - } - - return colorResourceId; - } - /** * Navigates to the Wi-Fi details screen for the selected Wi-Fi network. */ @@ -158,6 +132,7 @@ class ViewHolder extends RecyclerView.ViewHolder passpoint = view.findViewById(R.id.wifi_passpoint); capabilities = view.findViewById(R.id.wifi_capabilities); + // FIXME Make a click anywhere on the row navigate to the details screen, even when clicking on text mView.setOnClickListener(v -> { Float signalStrength = null; WifiBeaconRecordData data = wifiRecord.getData(); diff --git a/networksurvey/src/main/java/com/craxiom/networksurvey/fragments/WifiDetailsFragment.kt b/networksurvey/src/main/java/com/craxiom/networksurvey/fragments/WifiDetailsFragment.kt index d6c78ebb..5fd56813 100644 --- a/networksurvey/src/main/java/com/craxiom/networksurvey/fragments/WifiDetailsFragment.kt +++ b/networksurvey/src/main/java/com/craxiom/networksurvey/fragments/WifiDetailsFragment.kt @@ -12,6 +12,7 @@ import com.craxiom.networksurvey.listeners.IWifiSurveyRecordListener import com.craxiom.networksurvey.model.WifiNetwork import com.craxiom.networksurvey.model.WifiRecordWrapper import com.craxiom.networksurvey.services.NetworkSurveyService +import com.craxiom.networksurvey.ui.wifi.UNKNOWN_RSSI import com.craxiom.networksurvey.ui.wifi.WifiDetailsScreen import com.craxiom.networksurvey.ui.wifi.WifiDetailsViewModel import com.craxiom.networksurvey.util.NsTheme @@ -36,7 +37,9 @@ class WifiDetailsFragment : AServiceDataFragment(), IWifiSurveyRecordListener { setContent { viewModel = viewModel() viewModel.wifiNetwork = wifiNetwork - if (wifiNetwork.signalStrength != null) { + if (wifiNetwork.signalStrength == null) { + viewModel.addInitialRssi(UNKNOWN_RSSI) + } else { viewModel.addInitialRssi(wifiNetwork.signalStrength!!) } NsTheme { @@ -70,6 +73,7 @@ class WifiDetailsFragment : AServiceDataFragment(), IWifiSurveyRecordListener { if (targetWifiRecordWrapper == null) { Timber.i("No wifi record found for ${wifiNetwork.bssid} in the wifi scan results") + viewModel.addNewRssi(UNKNOWN_RSSI) return } @@ -77,6 +81,8 @@ class WifiDetailsFragment : AServiceDataFragment(), IWifiSurveyRecordListener { viewModel.addNewRssi(targetWifiRecordWrapper.wifiBeaconRecord.data.signalStrength.value) } else { Timber.i("No signal strength present for ${wifiNetwork.bssid} in the wifi beacon record") + viewModel.addNewRssi(UNKNOWN_RSSI) + } } } \ No newline at end of file diff --git a/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiDetailsScreen.kt b/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiDetailsScreen.kt index b42c5a9d..22014bb3 100644 --- a/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiDetailsScreen.kt +++ b/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiDetailsScreen.kt @@ -4,7 +4,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState @@ -13,9 +17,16 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.craxiom.networksurvey.R +import com.craxiom.networksurvey.constants.WifiBeaconMessageConstants.HIDDEN_SSID_PLACEHOLDER +import com.craxiom.networksurvey.util.ColorUtils /** * A Compose screen that shows the details of a single WiFi network. The main purpose for this @@ -26,58 +37,163 @@ import androidx.compose.ui.unit.dp internal fun WifiDetailsScreen( viewModel: WifiDetailsViewModel ) { + val context = LocalContext.current + val rssi by viewModel.rssiFlow.collectAsStateWithLifecycle() + val colorId = ColorUtils.getColorForWifiSignalStrength(rssi) + val colorResource = Color(context.getColor(colorId)) + LazyColumn( state = rememberLazyListState(), contentPadding = PaddingValues(padding), verticalArrangement = Arrangement.spacedBy(padding), ) { - chartItems(viewModel) + chartItems(viewModel, colorResource, rssi) } } private fun LazyListScope.chartItems( viewModel: WifiDetailsViewModel, + signalStrengthColor: Color, + rssi: Float ) { + val hiddenSsid = viewModel.wifiNetwork.ssid.isEmpty() +// TODO Make text selectable item { - Card(shape = MaterialTheme.shapes.large, colors = CardDefaults.elevatedCardColors()) { - Column(modifier = Modifier.padding(padding)) { - Text( - text = "BSSID: ${viewModel.wifiNetwork.bssid}", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = "SSID: ${if (viewModel.wifiNetwork.ssid.isEmpty()) "Hidden Network" else viewModel.wifiNetwork.ssid}", - style = MaterialTheme.typography.titleMedium - ) - // TODO Update the signal strength with every scan - //Text(text = "Signal Strength: ${viewModel.wifiNetwork.signalStrength?.toString() ?: "Unknown"} dBm", style = MaterialTheme.typography.titleMedium) - Text( - text = "Frequency: ${viewModel.wifiNetwork.frequency?.toString() ?: "Unknown"} MHz", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = "Channel: ${viewModel.wifiNetwork.channel?.toString() ?: "Unknown"}", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = "Encryption: ${viewModel.wifiNetwork.encryptionType}", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = "Passpoint: ${if (viewModel.wifiNetwork.passpoint == true) "Yes" else "No"}", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = "Capabilities: ${viewModel.wifiNetwork.capabilities}", - style = MaterialTheme.typography.titleMedium - ) + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceAround + ) { + Card( + modifier = Modifier + .fillMaxWidth(), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.elevatedCardColors() + ) { + Row( + modifier = Modifier + .padding(vertical = padding / 2) + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = if (hiddenSsid) HIDDEN_SSID_PLACEHOLDER else viewModel.wifiNetwork.ssid, + style = MaterialTheme.typography.titleMedium.copy( + color = Color( + LocalContext.current.getColor( + if (hiddenSsid) R.color.red else R.color.colorAccent + ) + ) + ) + ) + Text( + text = "SSID", + style = MaterialTheme.typography.labelMedium + ) + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = if (rssi == UNKNOWN_RSSI) "Unknown" else "${rssi.toInt()} dBm", + style = MaterialTheme.typography.titleMedium.copy(color = signalStrengthColor) + ) + Text( + text = "Signal Strength", + style = MaterialTheme.typography.labelMedium + ) + } + } + + Row( + modifier = Modifier + .padding(horizontal = padding, vertical = padding / 2) + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text( + text = "BSSID: ${viewModel.wifiNetwork.bssid}", + style = MaterialTheme.typography.titleMedium + ) + } + } + + Row( + modifier = Modifier + .padding(start = padding, end = padding, bottom = padding / 2) + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = viewModel.wifiNetwork.encryptionType, + style = MaterialTheme.typography.titleMedium + ) + } + + Row( + modifier = Modifier + .padding(start = padding, end = padding, bottom = padding / 2) + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.Start + ) { + Text( + text = "Channel: ${viewModel.wifiNetwork.channel?.toString() ?: "Unknown"}", + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.width(padding * 2)) + Text( + text = "${viewModel.wifiNetwork.frequency?.toString() ?: "Unknown"} MHz", + style = MaterialTheme.typography.titleMedium + ) + } + + if (viewModel.wifiNetwork.passpoint != null && viewModel.wifiNetwork.passpoint == true) { + + Row( + modifier = Modifier + .padding(start = padding, end = padding, bottom = padding / 2) + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.Start + ) { + Text( + text = "Passpoint", + style = MaterialTheme.typography.titleMedium.copy( + color = Color( + LocalContext.current.getColor(R.color.colorAccent) + ) + ) + ) + } + } } } } - cardItem { WifiRssiChart(viewModel.modelProducer) } + cardItem { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Signal Strength (Last 2 Minutes)", + style = MaterialTheme.typography.titleMedium + ) + WifiRssiChart(viewModel.modelProducer) + } + } } + private fun LazyListScope.cardItem(content: @Composable () -> Unit) { item { Card(shape = MaterialTheme.shapes.large, colors = CardDefaults.elevatedCardColors()) { @@ -89,6 +205,3 @@ private fun LazyListScope.cardItem(content: @Composable () -> Unit) { } private val padding = 16.dp - -private const val COLOR_1_CODE = 0xffa485e0 -private val color1 = Color(COLOR_1_CODE) diff --git a/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiDetailsViewModel.kt b/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiDetailsViewModel.kt index a90bb5c1..d4f6352b 100644 --- a/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiDetailsViewModel.kt +++ b/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiDetailsViewModel.kt @@ -9,11 +9,18 @@ import com.patrykandpatrick.vico.core.model.LineCartesianLayerModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import timber.log.Timber import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty +const val UNKNOWN_RSSI = -200f +const val MAX_WIFI_RSSI = -20f +const val MIN_WIFI_RSSI = -100f private const val CHART_WIDTH = 60 private const val UPDATE_FREQUENCY = 2000L @@ -26,20 +33,29 @@ internal class WifiDetailsViewModel : ViewModel() { lateinit var wifiNetwork: WifiNetwork + // This is the RSSI that is displayed in the details header. It is not necessarily the same + // as the RSSI that is displayed in the chart because on the chart we have to limit the range + // of the RSSI values. + private val _rssi = MutableStateFlow(UNKNOWN_RSSI) + val rssiFlow = _rssi.asStateFlow() + private val xValueQueue: ArrayDeque by dequeLimiter(CHART_WIDTH) private val rssiQueue: ArrayDeque by dequeLimiter(CHART_WIDTH) - private var latestRssi: Float = 0f + + private val _latestChartRssi = MutableStateFlow(UNKNOWN_RSSI) + private val latestChartRssi: StateFlow = _latestChartRssi.asStateFlow() + + private var unknownRssiCount = 0 private val lastXValue: Int get() = xValueQueue.lastOrNull() ?: 0 init { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.Main) { while (currentCoroutineContext().isActive) { // This coroutine will make sure that the chart is updated every n seconds, even // if there are not any new values coming in. - val lastValue = latestRssi - if (lastValue != 0f) { - addRssiToChart(lastValue) + if (!rssiQueue.isEmpty()) { + addRssiToChart(latestChartRssi.value) } delay(UPDATE_FREQUENCY) @@ -51,12 +67,15 @@ internal class WifiDetailsViewModel : ViewModel() { * Adds the initial RSSI value to the chart. This is used to make sure that the chart is * populated with something when the screen is first shown. */ + @Synchronized fun addInitialRssi(rssi: Float) { - if (rssiQueue.isNotEmpty()) return - if (rssi == 0f) return + if (rssiQueue.isNotEmpty()) { + Timber.e("The initial RSSI value is being added to the chart, but the chart is not empty") + return + } for (i in 0 until CHART_WIDTH) { - addRssiToChart(-200f) + addRssiToChart(UNKNOWN_RSSI) } // Add it two times to trigger something to show on the chart. A single value won't be @@ -66,12 +85,39 @@ internal class WifiDetailsViewModel : ViewModel() { addRssiToChart(rssi) } + @Synchronized fun addNewRssi(rssi: Float) { - if (rssi == 0f) return - // TODO If the rssi is out of range, bring it in range - latestRssi = rssi + var rssiToChart = rssi + + if (rssi == UNKNOWN_RSSI && latestChartRssi.value != UNKNOWN_RSSI) { + unknownRssiCount++ + + if (unknownRssiCount <= 2) { + // Ignore the first couple times the RSSI is missing from the scan results since it + // appears to be a common occurrence where a network is not found in a scan result. + // even though it is close to the device. + Timber.i("Ignoring the RSSI value of $rssi for ${wifiNetwork.ssid} since it is missing from the scan results") + return + } else { + unknownRssiCount = 0 + Timber.i("Resetting the unknown RSSI count for ${wifiNetwork.ssid}") + } + } + + if (rssi != UNKNOWN_RSSI) { + if (rssi < MIN_WIFI_RSSI) { + rssiToChart = MIN_WIFI_RSSI + } else if (rssi > MAX_WIFI_RSSI) { + rssiToChart = MAX_WIFI_RSSI + } + } + + _latestChartRssi.value = rssiToChart + // Display the actual RSSI value in the header, not the limited value that is used on the chart + _rssi.value = rssi } + @Synchronized private fun addRssiToChart(rssi: Float) { rssiQueue.add(rssi) xValueQueue.add(lastXValue + 1) @@ -92,8 +138,7 @@ fun dequeLimiter(limit: Int): ReadWriteProperty> = private fun applyLimit() { while (deque.size > limit) { - val removed = deque.removeFirst() - println("dequeLimiter removed $removed") + deque.removeFirst() } } diff --git a/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiRssiChart.kt b/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiRssiChart.kt index eecdd85c..24f37686 100644 --- a/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiRssiChart.kt +++ b/networksurvey/src/main/java/com/craxiom/networksurvey/ui/wifi/WifiRssiChart.kt @@ -14,7 +14,6 @@ import com.patrykandpatrick.vico.compose.chart.layout.fullWidth import com.patrykandpatrick.vico.compose.chart.rememberCartesianChart import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec import com.patrykandpatrick.vico.compose.component.shape.shader.color -import com.patrykandpatrick.vico.compose.dimensions.dimensionsOf import com.patrykandpatrick.vico.compose.style.ProvideChartStyle import com.patrykandpatrick.vico.core.axis.AxisItemPlacer import com.patrykandpatrick.vico.core.axis.vertical.VerticalAxis @@ -41,6 +40,12 @@ private fun ComposeChart(modelProducer: CartesianChartModelProducer) { ProvideChartStyle(rememberChartStyle(chartColors)) { //val defaultLines = currentChartStyle.lineLayer.lines CartesianChartHost( + modelProducer = modelProducer, + marker = rememberMarker(), + runInitialAnimation = false, + diffAnimationSpec = snap(), + horizontalLayout = horizontalLayout, + chartScrollSpec = rememberChartScrollSpec(isScrollEnabled = false), chart = rememberCartesianChart( rememberLineCartesianLayer( @@ -66,22 +71,14 @@ private fun ComposeChart(modelProducer: CartesianChartModelProducer) { title = stringResource(R.string.y_axis_time), ),*/ fadingEdges = rememberFadingEdges(), - ), - modelProducer = modelProducer, - marker = rememberMarker(), - runInitialAnimation = false, - diffAnimationSpec = snap(), - horizontalLayout = horizontalLayout, - chartScrollSpec = rememberChartScrollSpec(isScrollEnabled = false) + ) ) } } -private const val COLOR_1_CODE = 0xffffbb00 private const val COLOR_2_CODE = 0xff9db591 private val lineColor = Color(0xFF03A9F4) -private val color1 = Color(COLOR_1_CODE) private val color2 = Color(COLOR_2_CODE) private val chartColors = listOf(lineColor, color2) private val lineSpec = listOf( @@ -92,14 +89,7 @@ private val lineSpec = listOf( ), ) private val axisValueOverrider = AxisValueOverrider.fixed( - maxY = -30f, - minY = -110f, + maxY = MAX_WIFI_RSSI, + minY = MIN_WIFI_RSSI, ) -private val axisTitleHorizontalPaddingValue = 8.dp -private val axisTitleVerticalPaddingValue = 2.dp -private val axisTitlePadding = - dimensionsOf(axisTitleHorizontalPaddingValue, axisTitleVerticalPaddingValue) -private val axisTitleMarginValue = 4.dp -private val startAxisTitleMargins = dimensionsOf(end = axisTitleMarginValue) -private val bottomAxisTitleMargins = dimensionsOf(top = axisTitleMarginValue) private val horizontalLayout = HorizontalLayout.fullWidth() diff --git a/networksurvey/src/main/java/com/craxiom/networksurvey/util/ColorUtils.java b/networksurvey/src/main/java/com/craxiom/networksurvey/util/ColorUtils.java index 020ed2eb..28f5a762 100644 --- a/networksurvey/src/main/java/com/craxiom/networksurvey/util/ColorUtils.java +++ b/networksurvey/src/main/java/com/craxiom/networksurvey/util/ColorUtils.java @@ -4,6 +4,8 @@ import androidx.annotation.ColorInt; +import com.craxiom.networksurvey.R; + /** * Utility methods for working with colors in this app. * @@ -105,4 +107,31 @@ public static int getSignalColorForValue(int signalStrength, int maxValue) return COLOR_BINS[index]; } + + /** + * @param signalStrength The Wi-Fi signal strength value in dBm. + * @return The resource ID for the color that should be used for the signal strength text. + */ + public static int getColorForWifiSignalStrength(float signalStrength) + { + final int colorResourceId; + if (signalStrength > -60) + { + colorResourceId = R.color.rssi_green; + } else if (signalStrength > -70) + { + colorResourceId = R.color.rssi_yellow; + } else if (signalStrength > -80) + { + colorResourceId = R.color.rssi_orange; + } else if (signalStrength > -90) + { + colorResourceId = R.color.rssi_red; + } else + { + colorResourceId = R.color.rssi_deep_red; + } + + return colorResourceId; + } }