mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
chore(deps): bump deps to take advantage of new functionality (#4658)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
46b32f1cce
commit
145cde9393
11 changed files with 230 additions and 237 deletions
|
|
@ -67,13 +67,28 @@ import dagger.hilt.EntryPoint
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.DateFormatter
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.air_utilization
|
||||
import org.meshtastic.core.resources.battery
|
||||
import org.meshtastic.core.resources.channel_utilization
|
||||
import org.meshtastic.core.resources.getStringSuspend
|
||||
import org.meshtastic.core.resources.connecting
|
||||
import org.meshtastic.core.resources.device_sleeping
|
||||
import org.meshtastic.core.resources.disconnected
|
||||
import org.meshtastic.core.resources.local_stats_bad
|
||||
import org.meshtastic.core.resources.local_stats_diagnostics_prefix
|
||||
import org.meshtastic.core.resources.local_stats_dropped
|
||||
import org.meshtastic.core.resources.local_stats_heap
|
||||
import org.meshtastic.core.resources.local_stats_heap_value
|
||||
import org.meshtastic.core.resources.local_stats_noise
|
||||
import org.meshtastic.core.resources.local_stats_relays
|
||||
import org.meshtastic.core.resources.local_stats_traffic
|
||||
import org.meshtastic.core.resources.local_stats_updated_at
|
||||
import org.meshtastic.core.resources.meshtastic_app_name
|
||||
import org.meshtastic.core.resources.nodes
|
||||
import org.meshtastic.core.resources.powered
|
||||
import org.meshtastic.core.resources.refresh
|
||||
import org.meshtastic.core.resources.updated
|
||||
import org.meshtastic.core.resources.uptime
|
||||
|
|
@ -129,11 +144,11 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
titleBar = {
|
||||
TitleBar(
|
||||
startIcon = ImageProvider(com.geeksville.mesh.R.drawable.app_icon),
|
||||
title = state.appName,
|
||||
title = stringResource(Res.string.meshtastic_app_name),
|
||||
actions = {
|
||||
CircleIconButton(
|
||||
imageProvider = ImageProvider(com.geeksville.mesh.R.drawable.ic_refresh),
|
||||
contentDescription = state.refreshLabel,
|
||||
contentDescription = stringResource(Res.string.refresh),
|
||||
onClick = actionRunCallback<RefreshLocalStatsAction>(),
|
||||
backgroundColor = null,
|
||||
)
|
||||
|
|
@ -154,6 +169,7 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
private fun FullStatsContent(state: LocalStatsWidgetUiState) {
|
||||
val size = LocalSize.current
|
||||
val isNarrow = size.width < 160.dp
|
||||
|
|
@ -168,13 +184,20 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
state.nodeColors?.let { colors -> NodeChip(shortName = name, colors = colors) }
|
||||
}
|
||||
Spacer(GlanceModifier.width(8.dp))
|
||||
StatRow(
|
||||
label = state.batteryLabel,
|
||||
value = state.batteryValue,
|
||||
progress = state.batteryProgress,
|
||||
isSmall = isSmall,
|
||||
modifier = GlanceModifier.defaultWeight(),
|
||||
)
|
||||
if (state.hasBattery) {
|
||||
val isPowered = state.batteryLevel > 100
|
||||
val batteryValue =
|
||||
if (isPowered) stringResource(Res.string.powered) else "${state.batteryLevel}%"
|
||||
StatRow(
|
||||
label = stringResource(Res.string.battery),
|
||||
value = batteryValue,
|
||||
progress = state.batteryProgress,
|
||||
isSmall = isSmall,
|
||||
modifier = GlanceModifier.defaultWeight(),
|
||||
)
|
||||
} else {
|
||||
Spacer(GlanceModifier.defaultWeight())
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(GlanceModifier.height(2.dp))
|
||||
|
|
@ -183,15 +206,15 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
|
||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
StatRow(
|
||||
label = state.channelUtilizationLabel,
|
||||
value = state.channelUtilizationValue,
|
||||
label = stringResource(Res.string.channel_utilization),
|
||||
value = "%.1f%%".format(state.channelUtilization),
|
||||
progress = state.channelUtilizationProgress,
|
||||
isSmall = isSmall,
|
||||
modifier = GlanceModifier.defaultWeight().padding(end = 4.dp),
|
||||
)
|
||||
StatRow(
|
||||
label = state.airUtilizationLabel,
|
||||
value = state.airUtilizationValue,
|
||||
label = stringResource(Res.string.air_utilization),
|
||||
value = "%.1f%%".format(state.airUtilization),
|
||||
progress = state.airUtilizationProgress,
|
||||
isSmall = isSmall,
|
||||
modifier = GlanceModifier.defaultWeight().padding(start = 4.dp),
|
||||
|
|
@ -201,17 +224,53 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
// Detailed Traffic/Relay Stats
|
||||
Spacer(GlanceModifier.height(2.dp))
|
||||
Column(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
state.trafficText?.let { StatText(it, isSmall) }
|
||||
state.relayText?.let { StatText(it, isSmall) }
|
||||
state.diagnosticsText?.let { StatText(it, isSmall) }
|
||||
state.heapText?.let {
|
||||
if (state.hasStats) {
|
||||
StatText(
|
||||
stringResource(
|
||||
Res.string.local_stats_traffic,
|
||||
state.numPacketsTx,
|
||||
state.numPacketsRx,
|
||||
state.numRxDupe,
|
||||
),
|
||||
isSmall,
|
||||
)
|
||||
if (state.numTxRelay > 0 || state.numTxRelayCanceled > 0) {
|
||||
StatText(
|
||||
stringResource(
|
||||
Res.string.local_stats_relays,
|
||||
state.numTxRelay,
|
||||
state.numTxRelayCanceled,
|
||||
),
|
||||
isSmall,
|
||||
)
|
||||
}
|
||||
|
||||
val diag = mutableListOf<String>()
|
||||
if (state.noiseFloor != 0) {
|
||||
diag.add(stringResource(Res.string.local_stats_noise, state.noiseFloor))
|
||||
}
|
||||
if (state.numPacketsRxBad > 0) {
|
||||
diag.add(stringResource(Res.string.local_stats_bad, state.numPacketsRxBad))
|
||||
}
|
||||
if (state.numTxDropped > 0) {
|
||||
diag.add(stringResource(Res.string.local_stats_dropped, state.numTxDropped))
|
||||
}
|
||||
if (diag.isNotEmpty()) {
|
||||
StatText(
|
||||
stringResource(Res.string.local_stats_diagnostics_prefix, diag.joinToString(" | ")),
|
||||
isSmall,
|
||||
)
|
||||
}
|
||||
|
||||
val heapProgress =
|
||||
if (state.heapTotalBytes > 0) {
|
||||
state.heapFreeBytes.toFloat() / state.heapTotalBytes
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
StatRow(it, state.heapValue, heapProgress, isSmall)
|
||||
val heapValue =
|
||||
stringResource(Res.string.local_stats_heap_value, state.heapFreeBytes, state.heapTotalBytes)
|
||||
StatRow(stringResource(Res.string.local_stats_heap), heapValue, heapProgress, isSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -246,8 +305,15 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
modifier = GlanceModifier.size(32.dp),
|
||||
)
|
||||
}
|
||||
val statusText =
|
||||
when (state.connectionState) {
|
||||
is ConnectionState.Disconnected -> stringResource(Res.string.disconnected)
|
||||
is ConnectionState.Connecting -> stringResource(Res.string.connecting)
|
||||
is ConnectionState.DeviceSleep -> stringResource(Res.string.device_sleeping)
|
||||
is ConnectionState.Connected -> ""
|
||||
}
|
||||
Text(
|
||||
text = state.statusText,
|
||||
text = statusText,
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onSurfaceVariant,
|
||||
|
|
@ -258,6 +324,7 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun Footer(state: LocalStatsWidgetUiState) {
|
||||
Column(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
|
|
@ -267,11 +334,11 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
) {
|
||||
Column(modifier = GlanceModifier.defaultWeight(), horizontalAlignment = Alignment.Start) {
|
||||
Text(
|
||||
text = state.nodesLabel,
|
||||
text = stringResource(Res.string.nodes),
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = 10.sp),
|
||||
)
|
||||
Text(
|
||||
text = state.nodeCountText,
|
||||
text = "${state.onlineNodes}/${state.totalNodes}",
|
||||
maxLines = 1,
|
||||
style =
|
||||
TextStyle(
|
||||
|
|
@ -283,11 +350,11 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
}
|
||||
Column(modifier = GlanceModifier.defaultWeight(), horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = state.uptimeLabel,
|
||||
text = stringResource(Res.string.uptime),
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = 10.sp),
|
||||
)
|
||||
Text(
|
||||
text = state.uptimeText,
|
||||
text = formatUptime(state.uptimeSecs.toInt()),
|
||||
maxLines = 1,
|
||||
style =
|
||||
TextStyle(
|
||||
|
|
@ -299,11 +366,17 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
}
|
||||
}
|
||||
Row(modifier = GlanceModifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
val updatedLabel = stringResource(Res.string.updated)
|
||||
val updatedText =
|
||||
stringResource(
|
||||
Res.string.local_stats_updated_at,
|
||||
DateFormatter.formatShortDate(state.updateTimeMillis),
|
||||
)
|
||||
val footerText =
|
||||
if (state.updatedLabel.isNotEmpty()) {
|
||||
"${state.updatedLabel} ${state.updatedText}"
|
||||
if (updatedLabel.isNotEmpty()) {
|
||||
"$updatedLabel $updatedText"
|
||||
} else {
|
||||
state.updatedText
|
||||
updatedText
|
||||
}
|
||||
Text(
|
||||
text = footerText,
|
||||
|
|
@ -386,27 +459,24 @@ class LocalStatsWidget : GlanceAppWidget() {
|
|||
}
|
||||
}
|
||||
|
||||
internal suspend fun createMockWidgetState() = LocalStatsWidgetUiState(
|
||||
internal fun createMockWidgetState() = LocalStatsWidgetUiState(
|
||||
connectionState = ConnectionState.Connected,
|
||||
showContent = true,
|
||||
appName = getStringSuspend(Res.string.meshtastic_app_name),
|
||||
nodesLabel = getStringSuspend(Res.string.nodes),
|
||||
uptimeLabel = getStringSuspend(Res.string.uptime),
|
||||
updatedLabel = getStringSuspend(Res.string.updated),
|
||||
refreshLabel = getStringSuspend(Res.string.refresh),
|
||||
nodeShortName = "ME",
|
||||
nodeColors = 0xFFFFFFFF.toInt() to 0xFF000000.toInt(),
|
||||
batteryLabel = getStringSuspend(Res.string.battery),
|
||||
batteryValue = "85%",
|
||||
batteryLevel = 85,
|
||||
hasBattery = true,
|
||||
batteryProgress = 0.85f,
|
||||
channelUtilizationLabel = getStringSuspend(Res.string.channel_utilization),
|
||||
channelUtilizationValue = "18.5%",
|
||||
channelUtilization = 18.5f,
|
||||
channelUtilizationProgress = 0.185f,
|
||||
airUtilizationLabel = getStringSuspend(Res.string.air_utilization),
|
||||
airUtilizationValue = "3.2%",
|
||||
airUtilization = 3.2f,
|
||||
airUtilizationProgress = 0.032f,
|
||||
trafficText = "TX: 145 | RX: 892 | D: 42",
|
||||
nodeCountText = "2/3",
|
||||
uptimeText = "2d 0h",
|
||||
updatedText = "5m ago",
|
||||
hasStats = true,
|
||||
numPacketsTx = 145,
|
||||
numPacketsRx = 892,
|
||||
numRxDupe = 42,
|
||||
totalNodes = 3,
|
||||
onlineNodes = 2,
|
||||
uptimeSecs = 172800L,
|
||||
updateTimeMillis = System.currentTimeMillis() - 300000L,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,35 +27,10 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.meshtastic.core.common.util.DateFormatter
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.air_utilization
|
||||
import org.meshtastic.core.resources.battery
|
||||
import org.meshtastic.core.resources.channel_utilization
|
||||
import org.meshtastic.core.resources.connecting
|
||||
import org.meshtastic.core.resources.device_sleeping
|
||||
import org.meshtastic.core.resources.disconnected
|
||||
import org.meshtastic.core.resources.getStringSuspend
|
||||
import org.meshtastic.core.resources.local_stats_bad
|
||||
import org.meshtastic.core.resources.local_stats_diagnostics_prefix
|
||||
import org.meshtastic.core.resources.local_stats_dropped
|
||||
import org.meshtastic.core.resources.local_stats_heap
|
||||
import org.meshtastic.core.resources.local_stats_heap_value
|
||||
import org.meshtastic.core.resources.local_stats_noise
|
||||
import org.meshtastic.core.resources.local_stats_relays
|
||||
import org.meshtastic.core.resources.local_stats_traffic
|
||||
import org.meshtastic.core.resources.local_stats_updated_at
|
||||
import org.meshtastic.core.resources.meshtastic_app_name
|
||||
import org.meshtastic.core.resources.nodes
|
||||
import org.meshtastic.core.resources.powered
|
||||
import org.meshtastic.core.resources.refresh
|
||||
import org.meshtastic.core.resources.updated
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.LocalStats
|
||||
|
|
@ -64,48 +39,42 @@ import javax.inject.Singleton
|
|||
|
||||
data class LocalStatsWidgetUiState(
|
||||
val connectionState: ConnectionState = ConnectionState.Disconnected,
|
||||
// Rendering data
|
||||
val statusText: String = "",
|
||||
val isConnecting: Boolean = false,
|
||||
val showContent: Boolean = false,
|
||||
|
||||
// Static Strings (Resolved in provider for Glance stability)
|
||||
val appName: String = "",
|
||||
val nodesLabel: String = "",
|
||||
val uptimeLabel: String = "",
|
||||
val updatedLabel: String = "",
|
||||
val refreshLabel: String = "",
|
||||
|
||||
// Node Identity
|
||||
val nodeShortName: String? = null,
|
||||
val nodeColors: Pair<Int, Int>? = null,
|
||||
|
||||
// Battery
|
||||
val batteryLabel: String = "",
|
||||
val batteryValue: String = "",
|
||||
val batteryLevel: Int = 0,
|
||||
val hasBattery: Boolean = false,
|
||||
val batteryProgress: Float = 0f,
|
||||
|
||||
// Utilization
|
||||
val channelUtilizationLabel: String = "",
|
||||
val channelUtilizationValue: String = "",
|
||||
val channelUtilization: Float = 0f,
|
||||
val channelUtilizationProgress: Float = 0f,
|
||||
val airUtilizationLabel: String = "",
|
||||
val airUtilizationValue: String = "",
|
||||
val airUtilization: Float = 0f,
|
||||
val airUtilizationProgress: Float = 0f,
|
||||
|
||||
// Packet Stats Lines
|
||||
val trafficText: String? = null,
|
||||
val relayText: String? = null,
|
||||
val diagnosticsText: String? = null,
|
||||
// Stats
|
||||
val hasStats: Boolean = false,
|
||||
val numPacketsTx: Int = 0,
|
||||
val numPacketsRx: Int = 0,
|
||||
val numRxDupe: Int = 0,
|
||||
val numTxRelay: Int = 0,
|
||||
val numTxRelayCanceled: Int = 0,
|
||||
val noiseFloor: Int = 0,
|
||||
val numPacketsRxBad: Int = 0,
|
||||
val numTxDropped: Int = 0,
|
||||
val heapFreeBytes: Int = 0,
|
||||
val heapTotalBytes: Int = 0,
|
||||
val heapValue: String? = null,
|
||||
val heapText: String? = null,
|
||||
|
||||
// Footer
|
||||
val nodeCountText: String = "",
|
||||
val uptimeText: String = "",
|
||||
val updatedText: String = "",
|
||||
val totalNodes: Int = 0,
|
||||
val onlineNodes: Int = 0,
|
||||
val uptimeSecs: Long = 0,
|
||||
val updateTimeMillis: Long = 0,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
|
|
@ -151,101 +120,49 @@ constructor(
|
|||
)
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber")
|
||||
private suspend fun mapToUiState(
|
||||
private fun mapToUiState(
|
||||
connectionState: ConnectionState,
|
||||
totalNodes: Int,
|
||||
onlineNodes: Int,
|
||||
stats: LocalStats,
|
||||
localNode: Node?,
|
||||
): LocalStatsWidgetUiState {
|
||||
val statusText =
|
||||
when (connectionState) {
|
||||
is ConnectionState.Disconnected -> getStringSuspend(Res.string.disconnected)
|
||||
is ConnectionState.Connecting -> getStringSuspend(Res.string.connecting)
|
||||
is ConnectionState.DeviceSleep -> getStringSuspend(Res.string.device_sleeping)
|
||||
is ConnectionState.Connected -> ""
|
||||
}
|
||||
|
||||
val metrics = localNode?.deviceMetrics
|
||||
val batteryLevel = metrics?.battery_level ?: 0
|
||||
val isPowered = batteryLevel > 100
|
||||
val batteryValue = if (isPowered) getStringSuspend(Res.string.powered) else "$batteryLevel%"
|
||||
|
||||
val hasStats = stats.uptime_seconds != 0
|
||||
val channelUtil = if (hasStats) stats.channel_utilization else metrics?.channel_utilization ?: 0f
|
||||
val airUtilTx = if (hasStats) stats.air_util_tx else metrics?.air_util_tx ?: 0f
|
||||
|
||||
val diag = mutableListOf<String>()
|
||||
if (hasStats) {
|
||||
if (stats.noise_floor != 0) {
|
||||
diag.add(getStringSuspend(Res.string.local_stats_noise, stats.noise_floor))
|
||||
}
|
||||
if (stats.num_packets_rx_bad > 0) {
|
||||
diag.add(getStringSuspend(Res.string.local_stats_bad, stats.num_packets_rx_bad))
|
||||
}
|
||||
if (stats.num_tx_dropped > 0) {
|
||||
diag.add(getStringSuspend(Res.string.local_stats_dropped, stats.num_tx_dropped))
|
||||
}
|
||||
}
|
||||
|
||||
val uptimeSecs = if (hasStats) stats.uptime_seconds.toLong() else metrics?.uptime_seconds?.toLong() ?: 0L
|
||||
|
||||
return LocalStatsWidgetUiState(
|
||||
connectionState = connectionState,
|
||||
statusText = statusText,
|
||||
isConnecting = connectionState is ConnectionState.Connecting,
|
||||
showContent = connectionState is ConnectionState.Connected,
|
||||
appName = getStringSuspend(Res.string.meshtastic_app_name),
|
||||
nodesLabel = getStringSuspend(Res.string.nodes),
|
||||
uptimeLabel = getStringSuspend(Res.string.uptime),
|
||||
updatedLabel = getStringSuspend(Res.string.updated),
|
||||
refreshLabel = getStringSuspend(Res.string.refresh),
|
||||
nodeShortName = localNode?.user?.short_name,
|
||||
nodeColors = localNode?.colors,
|
||||
batteryLabel = getStringSuspend(Res.string.battery),
|
||||
batteryValue = batteryValue,
|
||||
batteryLevel = batteryLevel,
|
||||
hasBattery = metrics?.battery_level != null,
|
||||
batteryProgress = (batteryLevel / 100f).coerceIn(0f, 1f),
|
||||
channelUtilizationLabel = getStringSuspend(Res.string.channel_utilization),
|
||||
channelUtilizationValue = "%.1f%%".format(channelUtil),
|
||||
channelUtilization = channelUtil,
|
||||
channelUtilizationProgress = (channelUtil / 100f).coerceIn(0f, 1f),
|
||||
airUtilizationLabel = getStringSuspend(Res.string.air_utilization),
|
||||
airUtilizationValue = "%.1f%%".format(airUtilTx),
|
||||
airUtilization = airUtilTx,
|
||||
airUtilizationProgress = (airUtilTx / 100f).coerceIn(0f, 1f),
|
||||
trafficText =
|
||||
if (hasStats) {
|
||||
getStringSuspend(
|
||||
Res.string.local_stats_traffic,
|
||||
stats.num_packets_tx,
|
||||
stats.num_packets_rx,
|
||||
stats.num_rx_dupe,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
relayText =
|
||||
stats
|
||||
.takeIf { hasStats && (it.num_tx_relay > 0 || it.num_tx_relay_canceled > 0) }
|
||||
?.let {
|
||||
getStringSuspend(Res.string.local_stats_relays, it.num_tx_relay, it.num_tx_relay_canceled)
|
||||
},
|
||||
diagnosticsText =
|
||||
if (diag.isNotEmpty()) {
|
||||
getStringSuspend(Res.string.local_stats_diagnostics_prefix, diag.joinToString(" | "))
|
||||
} else {
|
||||
null
|
||||
},
|
||||
heapFreeBytes = if (hasStats) stats.heap_free_bytes else 0,
|
||||
heapTotalBytes = if (hasStats) stats.heap_total_bytes else 0,
|
||||
heapValue =
|
||||
if (hasStats) {
|
||||
getStringSuspend(Res.string.local_stats_heap_value, stats.heap_free_bytes, stats.heap_total_bytes)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
heapText = if (hasStats) getStringSuspend(Res.string.local_stats_heap) else null,
|
||||
nodeCountText = "$onlineNodes/$totalNodes",
|
||||
uptimeText = formatUptime(uptimeSecs.toInt()),
|
||||
updatedText = getStringSuspend(Res.string.local_stats_updated_at, DateFormatter.formatShortDate(nowMillis)),
|
||||
hasStats = hasStats,
|
||||
numPacketsTx = stats.num_packets_tx,
|
||||
numPacketsRx = stats.num_packets_rx,
|
||||
numRxDupe = stats.num_rx_dupe,
|
||||
numTxRelay = stats.num_tx_relay,
|
||||
numTxRelayCanceled = stats.num_tx_relay_canceled,
|
||||
noiseFloor = stats.noise_floor,
|
||||
numPacketsRxBad = stats.num_packets_rx_bad,
|
||||
numTxDropped = stats.num_tx_dropped,
|
||||
heapFreeBytes = stats.heap_free_bytes,
|
||||
heapTotalBytes = stats.heap_total_bytes,
|
||||
totalNodes = totalNodes,
|
||||
onlineNodes = onlineNodes,
|
||||
uptimeSecs = uptimeSecs,
|
||||
updateTimeMillis = nowMillis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.network.di
|
||||
|
||||
import android.content.Context
|
||||
|
|
@ -26,11 +25,6 @@ import dagger.Provides
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
|
@ -71,20 +65,5 @@ interface GoogleNetworkModule {
|
|||
)
|
||||
.eventListenerFactory(eventListenerFactory = DatadogEventListener.Factory())
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHttpClient(okHttpClient: OkHttpClient): HttpClient = HttpClient(engineFactory = OkHttp) {
|
||||
engine { preconfigured = okHttpClient }
|
||||
|
||||
install(plugin = ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.network.di
|
||||
|
||||
import android.content.Context
|
||||
|
|
@ -31,6 +30,11 @@ import dagger.Provides
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import org.meshtastic.core.network.BuildConfig
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -49,7 +53,7 @@ class NetworkModule {
|
|||
return ImageLoader.Builder(context = application)
|
||||
.components {
|
||||
add(OkHttpNetworkFetcherFactory(callFactory = { sharedOkHttp }))
|
||||
add(SvgDecoder.Factory())
|
||||
add(SvgDecoder.Factory(scaleToDensity = true))
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build()
|
||||
|
|
@ -59,4 +63,19 @@ class NetworkModule {
|
|||
.crossfade(enable = true)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHttpClient(okHttpClient: OkHttpClient): HttpClient = HttpClient(engineFactory = OkHttp) {
|
||||
engine { preconfigured = okHttpClient }
|
||||
|
||||
install(plugin = ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ fun getString(stringResource: StringResource): String = runBlocking { composeGet
|
|||
|
||||
/** Retrieves a formatted string from the [StringResource] in a blocking manner. */
|
||||
fun getString(stringResource: StringResource, vararg formatArgs: Any): String = runBlocking {
|
||||
getStringSuspend(stringResource, *formatArgs)
|
||||
composeGetString(stringResource, *formatArgs)
|
||||
}
|
||||
|
||||
/** Retrieves a string from the [StringResource] in a suspending manner. */
|
||||
|
|
@ -37,7 +37,6 @@ suspend fun getStringSuspend(stringResource: StringResource, vararg formatArgs:
|
|||
formatArgs
|
||||
.map { arg ->
|
||||
if (arg is StringResource) {
|
||||
// Resolve nested StringResources recursively
|
||||
getStringSuspend(arg)
|
||||
} else {
|
||||
arg
|
||||
|
|
@ -45,9 +44,6 @@ suspend fun getStringSuspend(stringResource: StringResource, vararg formatArgs:
|
|||
}
|
||||
.toTypedArray()
|
||||
|
||||
// Compose Multiplatform doesn't fully support complex formatting like %.2f
|
||||
// Fetch the raw string and format it using standard Java String.format.
|
||||
val rawString = composeGetString(stringResource)
|
||||
@Suppress("SpreadOperator")
|
||||
return String.format(java.util.Locale.getDefault(), rawString, *resolvedArgs)
|
||||
return composeGetString(stringResource, *resolvedArgs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,8 @@ import android.widget.Toast
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
suspend fun Context.showToast(stringResource: StringResource) {
|
||||
showToast(getString(stringResource))
|
||||
}
|
||||
|
||||
suspend fun Context.showToast(stringResource: StringResource, vararg formatArgs: Any) {
|
||||
Toast.makeText(this, getString(stringResource, formatArgs), Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(this, getString(stringResource, *formatArgs), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
suspend fun Context.showToast(text: String) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ dependencies {
|
|||
implementation(projects.core.datastore)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.network)
|
||||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.service)
|
||||
|
|
@ -48,6 +49,7 @@ dependencies {
|
|||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.ktor.client.core)
|
||||
|
||||
implementation(libs.nordic.client.android)
|
||||
implementation(libs.nordic.dfu)
|
||||
|
|
|
|||
|
|
@ -20,12 +20,17 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.head
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.http.contentLength
|
||||
import io.ktor.http.isSuccess
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
|
@ -45,7 +50,7 @@ class FirmwareFileHandler
|
|||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val client: OkHttpClient,
|
||||
private val client: HttpClient,
|
||||
) {
|
||||
private val tempDir = File(context.cacheDir, "firmware_update")
|
||||
|
||||
|
|
@ -60,10 +65,9 @@ constructor(
|
|||
}
|
||||
|
||||
suspend fun checkUrlExists(url: String): Boolean = withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder().url(url).head().build()
|
||||
try {
|
||||
client.newCall(request).execute().use { response -> response.isSuccessful }
|
||||
} catch (e: IOException) {
|
||||
client.head(url).status.isSuccess()
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||
Logger.w(e) { "Failed to check URL existence: $url" }
|
||||
false
|
||||
}
|
||||
|
|
@ -71,28 +75,27 @@ constructor(
|
|||
|
||||
suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): File? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response =
|
||||
try {
|
||||
client.newCall(request).execute()
|
||||
} catch (e: IOException) {
|
||||
client.get(url)
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||
Logger.w(e) { "Download failed for $url" }
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
Logger.w { "Download failed: ${response.code} for $url" }
|
||||
if (!response.status.isSuccess()) {
|
||||
Logger.w { "Download failed: ${response.status.value} for $url" }
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val body = response.body ?: return@withContext null
|
||||
val contentLength = body.contentLength()
|
||||
val body = response.bodyAsChannel()
|
||||
val contentLength = response.contentLength() ?: -1L
|
||||
|
||||
if (!tempDir.exists()) tempDir.mkdirs()
|
||||
|
||||
val targetFile = File(tempDir, fileName)
|
||||
|
||||
body.byteStream().use { input ->
|
||||
body.toInputStream().use { input ->
|
||||
FileOutputStream(targetFile).use { output ->
|
||||
val buffer = ByteArray(DOWNLOAD_BUFFER_SIZE)
|
||||
var bytesRead: Int
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package org.meshtastic.feature.map.component
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
|
|
@ -32,6 +31,7 @@ import com.google.maps.android.clustering.view.DefaultClusterRenderer
|
|||
import com.google.maps.android.compose.Circle
|
||||
import com.google.maps.android.compose.MapsComposeExperimentalApi
|
||||
import com.google.maps.android.compose.clustering.Clustering
|
||||
import com.google.maps.android.compose.clustering.ClusteringMarkerProperties
|
||||
import org.meshtastic.feature.map.BaseMapViewModel
|
||||
import org.meshtastic.feature.map.model.NodeClusterItem
|
||||
|
||||
|
|
@ -63,10 +63,19 @@ fun NodeClusterMarkers(
|
|||
}
|
||||
}
|
||||
|
||||
if (mapFilterState.showPrecisionCircle) {
|
||||
nodeClusterItems.forEach { clusterItem ->
|
||||
key(clusterItem.node.num) {
|
||||
// Add a stable key for each circle
|
||||
Clustering(
|
||||
items = nodeClusterItems,
|
||||
onClusterClick = onClusterClick,
|
||||
onClusterItemInfoWindowClick = { item ->
|
||||
navigateToNodeDetails(item.node.num)
|
||||
false
|
||||
},
|
||||
clusterItemContent = { clusterItem -> PulsingNodeChip(node = clusterItem.node) },
|
||||
onClusterManager = { clusterManager ->
|
||||
(clusterManager.renderer as DefaultClusterRenderer).minClusterSize = 10
|
||||
},
|
||||
clusterItemDecoration = { clusterItem ->
|
||||
if (mapFilterState.showPrecisionCircle) {
|
||||
clusterItem.getPrecisionMeters()?.let { precisionMeters ->
|
||||
if (precisionMeters > 0) {
|
||||
Circle(
|
||||
|
|
@ -80,18 +89,7 @@ fun NodeClusterMarkers(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Clustering(
|
||||
items = nodeClusterItems,
|
||||
onClusterClick = onClusterClick,
|
||||
onClusterItemInfoWindowClick = { item ->
|
||||
navigateToNodeDetails(item.node.num)
|
||||
false
|
||||
},
|
||||
clusterItemContent = { clusterItem -> PulsingNodeChip(node = clusterItem.node) },
|
||||
onClusterManager = { clusterManager ->
|
||||
(clusterManager.renderer as DefaultClusterRenderer).minClusterSize = 10
|
||||
ClusteringMarkerProperties(zIndex = 1f)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,11 +81,14 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.format
|
||||
import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.common.util.toInstant
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.debug_clear
|
||||
import org.meshtastic.core.resources.debug_decoded_payload
|
||||
|
|
@ -113,8 +116,7 @@ import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog
|
|||
import java.io.IOException
|
||||
import java.io.OutputStreamWriter
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.time.Instant.Companion.fromEpochMilliseconds
|
||||
|
||||
private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE)
|
||||
|
||||
|
|
@ -201,8 +203,18 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel = hiltViewMo
|
|||
filterMode = filterMode,
|
||||
onFilterModeChange = { filterMode = it },
|
||||
onExportLogs = {
|
||||
val format =
|
||||
LocalDateTime.Format {
|
||||
year()
|
||||
monthNumber()
|
||||
day()
|
||||
char('_')
|
||||
hour()
|
||||
minute()
|
||||
second()
|
||||
}
|
||||
val timestamp =
|
||||
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(nowMillis.toInstant().toDate())
|
||||
fromEpochMilliseconds(nowMillis).toLocalDateTime(TimeZone.UTC).format(format)
|
||||
val fileName = "meshtastic_debug_$timestamp.txt"
|
||||
exportLogsLauncher.launch(fileName)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ mockk = "1.14.9"
|
|||
testRetry = "1.6.4"
|
||||
|
||||
# Compose Multiplatform
|
||||
compose-multiplatform = "1.10.1"
|
||||
compose-multiplatform = "1.11.0-alpha03"
|
||||
|
||||
# Google
|
||||
hilt = "2.59.2"
|
||||
|
|
@ -165,6 +165,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
|
|||
|
||||
# Networking
|
||||
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
|
||||
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version = "5.3.2" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue