Skip to content

Commit

Permalink
First pass at a Wi-Fi Spectrum chart. Only added 2.4 GHz, and Vico do…
Browse files Browse the repository at this point in the history
…es not yet support custom data labels
  • Loading branch information
christianrowlands committed Jan 25, 2024
1 parent 1a037ed commit e14975a
Show file tree
Hide file tree
Showing 18 changed files with 814 additions and 8 deletions.
4 changes: 2 additions & 2 deletions networksurvey/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.3.2'

implementation 'app.futured.donut:donut:2.2.3'
implementation 'com.patrykandpatrick.vico:core:2.0.0-alpha.5'
implementation 'com.patrykandpatrick.vico:compose-m3:2.0.0-alpha.5'
implementation 'com.patrykandpatrick.vico:core:2.0.0-alpha.6'
implementation 'com.patrykandpatrick.vico:compose-m3:2.0.0-alpha.6'

implementation 'com.github.yuriy-budiyev:code-scanner:2.1.2'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ class WifiDetailsFragment : AServiceDataFragment(), IWifiSurveyRecordListener {
private val preferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == NetworkSurveyConstants.PROPERTY_WIFI_SCAN_INTERVAL_SECONDS) {
val bluetoothScanRateMs = PreferenceUtils.getScanRatePreferenceMs(
val wifiScanRateMs = PreferenceUtils.getScanRatePreferenceMs(
NetworkSurveyConstants.PROPERTY_WIFI_SCAN_INTERVAL_SECONDS,
NetworkSurveyConstants.DEFAULT_WIFI_SCAN_INTERVAL_SECONDS,
context
)
viewModel.setScanRateSeconds(bluetoothScanRateMs / 1_000)
viewModel.setScanRateSeconds(wifiScanRateMs / 1_000)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
import android.os.Looper;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LifecycleOwner;
Expand Down Expand Up @@ -52,7 +56,7 @@
*
* @since 0.1.2
*/
public class WifiNetworksFragment extends Fragment implements IWifiSurveyRecordListener
public class WifiNetworksFragment extends Fragment implements IWifiSurveyRecordListener, MenuProvider
{
private FragmentWifiNetworksListBinding binding;
private SortedList<WifiRecordWrapper> wifiRecordSortedList;
Expand Down Expand Up @@ -134,6 +138,12 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
if (paused) lastScanTime = System.currentTimeMillis();
});

FragmentActivity activity = getActivity();
if (activity != null)
{
activity.addMenuProvider(this, getViewLifecycleOwner());
}

return binding.getRoot();
}

Expand Down Expand Up @@ -207,6 +217,23 @@ public void onWifiBeaconSurveyRecords(List<WifiRecordWrapper> wifiBeaconRecords)
});
}

@Override
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater)
{
menuInflater.inflate(R.menu.wifi_networks_menu, menu);
}

@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem)
{
if (menuItem.getItemId() == R.id.action_open_spectrum)
{
navigateToWifiSpectrumScreen();
return true;
}
return false;
}

/**
* Navigates to the Wi-Fi details screen for the selected Wi-Fi network.
*/
Expand All @@ -220,6 +247,18 @@ public void navigateToWifiDetails(WifiNetwork wifiNetwork)
wifiNetwork));
}

/**
* Navigates to the Wi-Fi spectrum screen.
*/
public void navigateToWifiSpectrumScreen()
{
FragmentActivity activity = getActivity();
if (activity == null) return;

Navigation.findNavController(activity, getId())
.navigate(WifiNetworksFragmentDirections.actionWifiListFragmentToWifiSpectrumFragment());
}

/**
* Updates the view with the information stored in the view model.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.craxiom.networksurvey.fragments

import android.content.SharedPreferences
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.craxiom.networksurvey.constants.NetworkSurveyConstants
import com.craxiom.networksurvey.listeners.IWifiSurveyRecordListener
import com.craxiom.networksurvey.model.WifiRecordWrapper
import com.craxiom.networksurvey.services.NetworkSurveyService
import com.craxiom.networksurvey.ui.wifi.WifiSpectrumChartViewModel
import com.craxiom.networksurvey.ui.wifi.WifiSpectrumScreen
import com.craxiom.networksurvey.util.NsTheme
import com.craxiom.networksurvey.util.PreferenceUtils

/**
* The fragment that displays the details of a single Wifi network from the scan results.
*/
class WifiSpectrumFragment : AServiceDataFragment(), IWifiSurveyRecordListener {
private lateinit var viewModel: WifiSpectrumChartViewModel

private lateinit var sharedPreferences: SharedPreferences
private val preferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == NetworkSurveyConstants.PROPERTY_WIFI_SCAN_INTERVAL_SECONDS) {
val wifiScanRateMs = PreferenceUtils.getScanRatePreferenceMs(
NetworkSurveyConstants.PROPERTY_WIFI_SCAN_INTERVAL_SECONDS,
NetworkSurveyConstants.DEFAULT_WIFI_SCAN_INTERVAL_SECONDS,
context
)
viewModel.setScanRateSeconds(wifiScanRateMs / 1_000)
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val composeView = ComposeView(requireContext())

composeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
viewModel = viewModel()

sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
sharedPreferences.registerOnSharedPreferenceChangeListener(
preferenceChangeListener
)
val scanRateMs = PreferenceUtils.getScanRatePreferenceMs(
NetworkSurveyConstants.PROPERTY_WIFI_SCAN_INTERVAL_SECONDS,
NetworkSurveyConstants.DEFAULT_WIFI_SCAN_INTERVAL_SECONDS,
context
)
viewModel.setScanRateSeconds(scanRateMs / 1_000)
viewModel.initializeCharts()

NsTheme {
WifiSpectrumScreen(
viewModel = viewModel,
wifiSpectrumFragment = this@WifiSpectrumFragment
)
}
}
}

return composeView
}

override fun onResume() {
super.onResume()

startAndBindToService()
}

override fun onPause() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)

super.onPause()
}

override fun onSurveyServiceConnected(service: NetworkSurveyService?) {
if (service == null) return
service.registerWifiSurveyRecordListener(this)
}

override fun onSurveyServiceDisconnecting(service: NetworkSurveyService?) {
if (service == null) return
service.unregisterWifiSurveyRecordListener(this)
}

override fun onWifiBeaconSurveyRecords(wifiBeaconRecords: MutableList<WifiRecordWrapper>?) {
val wifiNetworkInfoList: List<WifiNetworkInfo> = wifiBeaconRecords
?.filter { it.wifiBeaconRecord.data.hasSignalStrength() && it.wifiBeaconRecord.data.ssid != null && it.wifiBeaconRecord.data.hasChannel() }
?.map {
WifiNetworkInfo(
it.wifiBeaconRecord.data.ssid!!,
it.wifiBeaconRecord.data.signalStrength.value.toInt(),
it.wifiBeaconRecord.data.channel.value
)
}
?: emptyList()

viewModel.onWifiScanResults(wifiNetworkInfoList)
}


/**
* Navigates to the Settings UI (primarily for the user to change the scan rate)
*/
fun navigateToSettings() {
findNavController().navigate(WifiDetailsFragmentDirections.actionWifiDetailsToSettings())
}
}

data class WifiNetworkInfo(
val ssid: String,
val signalStrength: Int,
val channel: Int
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.craxiom.networksurvey.ui.cellular


import android.os.Build
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import com.patrykandpatrick.vico.core.chart.values.ChartValues
import com.patrykandpatrick.vico.core.extension.appendCompat
import com.patrykandpatrick.vico.core.extension.transformToSpannable
import com.patrykandpatrick.vico.core.marker.Marker
import com.patrykandpatrick.vico.core.marker.MarkerLabelFormatter

Expand All @@ -30,3 +30,36 @@ class CustomMarkerLabelFormatter(private val colorCode: Boolean = true, private
}
}
}

internal fun SpannableStringBuilder.appendCompat(
text: CharSequence,
what: Any,
flags: Int,
): SpannableStringBuilder =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
append(text, what, flags)
} else {
append(text, 0, text.length)
setSpan(what, length - text.length, length, flags)
this
}

internal fun <T> Iterable<T>.transformToSpannable(
separator: CharSequence = ", ",
prefix: CharSequence = "",
postfix: CharSequence = "",
limit: Int = -1,
truncated: CharSequence = "…",
transform: SpannableStringBuilder.(T) -> Unit,
): Spannable {
val buffer = SpannableStringBuilder()
buffer.append(prefix)
var count = 0
for (element in this) {
if (++count > 1) buffer.append(separator)
if (limit < 0 || count <= limit) buffer.transform(element) else break
}
if (limit in 0..<count) buffer.append(truncated)
buffer.append(postfix)
return buffer
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.craxiom.networksurvey.ui.wifi

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import com.patrykandpatrick.vico.compose.component.shape.shader.color
import com.patrykandpatrick.vico.compose.style.ChartStyle
import com.patrykandpatrick.vico.core.DefaultColors
import com.patrykandpatrick.vico.core.DefaultDimens
import com.patrykandpatrick.vico.core.chart.layer.LineCartesianLayer
import com.patrykandpatrick.vico.core.component.shape.LineComponent
import com.patrykandpatrick.vico.core.component.shape.Shapes
import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders

@Composable
internal fun rememberSpectrumChartStyle(
columnLayerColors: List<Color>,
lineLayerColors: List<Color>,
): ChartStyle {
val isSystemInDarkTheme = isSystemInDarkTheme()
// TODO Use the textComponent for the SSID labels when support is added to the library
/*val textComponent = rememberTextComponent(
color = Color(if (isSystemInDarkTheme) DefaultColors.Dark.axisLabelColor else DefaultColors.Light.axisLabelColor),
)*/

return remember(columnLayerColors, lineLayerColors, isSystemInDarkTheme) {
val defaultColors = if (isSystemInDarkTheme) DefaultColors.Dark else DefaultColors.Light
ChartStyle(
ChartStyle.Axis(
axisLabelColor = Color(defaultColors.axisLabelColor),
axisGuidelineColor = Color(defaultColors.axisGuidelineColor),
axisLineColor = Color(defaultColors.axisLineColor),
),
ChartStyle.ColumnLayer(
columnLayerColors.map { columnChartColor ->
LineComponent(
columnChartColor.toArgb(),
DefaultDimens.COLUMN_WIDTH,
Shapes.roundedCornerShape(DefaultDimens.COLUMN_ROUNDNESS_PERCENT),
)
},
),
ChartStyle.LineLayer(
lineLayerColors.map { lineChartColor ->
LineCartesianLayer.LineSpec(
pointConnector = SpectrumPointConnector(),
//dataLabel = textComponent,
thicknessDp = 3f,
shader = DynamicShaders.color(lineChartColor),
//backgroundShader = null,
)
},
),
ChartStyle.Marker(),
Color(defaultColors.elevationOverlayColor),
)
}
}

@Composable
internal fun rememberSpectrumChartStyle(chartColors: List<Color>) =
rememberSpectrumChartStyle(columnLayerColors = chartColors, lineLayerColors = chartColors)
Loading

0 comments on commit e14975a

Please sign in to comment.