mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-30 23:54:32 -04:00
refactor(ui): compose resources, domain layer (#4628)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
1 parent
96adc70401
commit
2676a51647
322 files changed
+3031
-2790
No files matched your search
@@ -32,13 +32,13 @@ This file serves as a comprehensive guide for AI agents and developers working o
|
||||
- **Material 3:** The app uses Material 3. Look for ways to use **Material 3 Expressive** components where appropriate.
|
||||
- **Strings:**
|
||||
- Do **not** use `app/src/main/res/values/strings.xml` for UI strings.
|
||||
- Use the **Compose Multiplatform Resource** library in `core/strings`.
|
||||
- **Definition:** Add strings to `core/strings/src/commonMain/composeResources/values/strings.xml`.
|
||||
- Use the **Compose Multiplatform Resource** library in `core:resources`.
|
||||
- **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`.
|
||||
- **Usage:**
|
||||
```kotlin
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.your_string_key
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.your_string_key
|
||||
|
||||
Text(text = stringResource(Res.string.your_string_key))
|
||||
```
|
||||
@@ -102,7 +102,7 @@ This file serves as a comprehensive guide for AI agents and developers working o
|
||||
1. **Explore First:** Before making changes, read `gradle/libs.versions.toml` and the relevant `build.gradle.kts` to understand the environment.
|
||||
2. **Plan:** Identify which modules (`core` or `feature`) need modification.
|
||||
3. **Implement:**
|
||||
- If adding a string, modify `core/strings`.
|
||||
- If adding a string, modify `core:resources`.
|
||||
- If adding a dependency, modify `libs.versions.toml` first.
|
||||
4. **Verify:**
|
||||
- Run `./gradlew spotlessApply` (Essential!).
|
||||
@@ -118,8 +118,14 @@ This file serves as a comprehensive guide for AI agents and developers working o
|
||||
|
||||
## 7. Troubleshooting
|
||||
|
||||
- **Missing Strings:** If `Res.string.xyz` is unresolved, ensure you have imported `org.meshtastic.core.strings.Res` and the specific string property, and that you have run a build to generate the resources.
|
||||
- **Missing Strings:** If `Res.string.xyz` is unresolved, ensure you have imported `org.meshtastic.core.resources.Res` and the specific string property, and that you have run a build to generate the resources.
|
||||
- **Build Errors:** Check `gradle/libs.versions.toml` for version conflicts. Use `build-logic` conventions to ensure plugins are applied correctly.
|
||||
|
||||
---
|
||||
*Refer to `CONTRIBUTING.md` for human-centric processes like Code of Conduct and Pull Request etiquette.*
|
||||
|
||||
### E. Resources and Assets
|
||||
- **Centralization:** All global app resources (Strings, Drawables, Fonts, raw files) should be placed in `:core:resources`.
|
||||
- **Module Path:** `core/resources/src/commonMain/composeResources/`
|
||||
- **Decentralization:** Feature-specific strings and assets can (and should) be housed in their respective feature module's `composeResources` directory to maintain modular boundaries and clean architectural dependency graphs. Crowdin localization handles globbing `/**/composeResources/values/strings.xml` perfectly.
|
||||
- **Drawables:** Use `painterResource(Res.drawable.your_icon)` to access cross-platform drawables. Name them consistently (`ic_` for icons, `img_` for artwork). Avoid putting standard Drawables or Vectors in legacy Android `res/drawable` folders unless strictly required by a legacy library (like `OsmDroid` map markers) or the OS layer (like `app_icon.xml`).
|
||||
+4
-4
@@ -19,14 +19,14 @@ Thank you for your interest in contributing to Meshtastic-Android! We welcome co
|
||||
- Write clear, descriptive variable and function names.
|
||||
- Add comments where necessary, especially for complex logic.
|
||||
- Keep methods and classes focused and concise.
|
||||
- **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:strings`.
|
||||
- **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:resources`.
|
||||
- Do **not** use the legacy `app/src/main/res/values/strings.xml`.
|
||||
- **Definition:** Add strings to `core/strings/src/commonMain/composeResources/values/strings.xml`.
|
||||
- **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`.
|
||||
- **Usage:**
|
||||
```kotlin
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.your_string_key
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.your_string_key
|
||||
|
||||
Text(text = stringResource(Res.string.your_string_key))
|
||||
```
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ graph TB
|
||||
:app -.-> :core:prefs
|
||||
:app -.-> :core:proto
|
||||
:app -.-> :core:service
|
||||
:app -.-> :core:strings
|
||||
:app -.-> :core:resources
|
||||
:app -.-> :core:ui
|
||||
:app -.-> :core:barcode
|
||||
:app -.-> :feature:intro
|
||||
|
||||
@@ -188,11 +188,9 @@ secrets {
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().withBuildType("debug")) { variant ->
|
||||
variant.flavorName?.let { flavor ->
|
||||
variant.applicationId = "com.geeksville.mesh.$flavor.debug"
|
||||
}
|
||||
variant.flavorName?.let { flavor -> variant.applicationId = "com.geeksville.mesh.$flavor.debug" }
|
||||
}
|
||||
|
||||
|
||||
onVariants(selector().withBuildType("release")) { variant ->
|
||||
if (variant.flavorName == "google") {
|
||||
val variantNameCapped = variant.name.replaceFirstChar { it.uppercase() }
|
||||
@@ -226,7 +224,7 @@ dependencies {
|
||||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.service)
|
||||
implementation(projects.core.strings)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.barcode)
|
||||
implementation(projects.feature.intro)
|
||||
|
||||
@@ -50,8 +50,8 @@ import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
|
||||
import no.nordicsemi.kotlin.ble.environment.android.compose.LocalEnvironmentOwner
|
||||
import org.meshtastic.core.model.util.dispatchMeshtasticUri
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.channel_invalid
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_invalid
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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 com.geeksville.mesh.domain.usecase
|
||||
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import com.geeksville.mesh.model.DeviceListEntry
|
||||
import com.geeksville.mesh.model.getMeshtasticShortName
|
||||
import com.geeksville.mesh.repository.network.NetworkRepository
|
||||
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||
import org.meshtastic.core.datastore.model.RecentAddress
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.meshtastic
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
data class DiscoveredDevices(
|
||||
val bleDevices: List<DeviceListEntry>,
|
||||
val usbDevices: List<DeviceListEntry>,
|
||||
val discoveredTcpDevices: List<DeviceListEntry>,
|
||||
val recentTcpDevices: List<DeviceListEntry>,
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class GetDiscoveredDevicesUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val bluetoothRepository: BluetoothRepository,
|
||||
private val networkRepository: NetworkRepository,
|
||||
private val recentAddressesDataSource: RecentAddressesDataSource,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val databaseManager: DatabaseManager,
|
||||
private val usbRepository: UsbRepository,
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
private val usbManagerLazy: dagger.Lazy<UsbManager>,
|
||||
) {
|
||||
private val suffixLength = 4
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
fun invoke(showMock: Boolean): Flow<DiscoveredDevices> {
|
||||
val nodeDb = nodeRepository.nodeDBbyNum
|
||||
|
||||
val bondedBleFlow = bluetoothRepository.state.map { ble -> ble.bondedDevices.map { DeviceListEntry.Ble(it) } }
|
||||
|
||||
val processedTcpFlow =
|
||||
combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) {
|
||||
tcpServices,
|
||||
recentList,
|
||||
->
|
||||
val recentMap = recentList.associateBy({ it.address }) { it.name }
|
||||
tcpServices
|
||||
.map { service ->
|
||||
val address = "t${service.toAddressString()}"
|
||||
val txtRecords = service.attributes
|
||||
val shortNameBytes = txtRecords["shortname"]
|
||||
val idBytes = txtRecords["id"]
|
||||
|
||||
val shortName =
|
||||
shortNameBytes?.let { String(it, Charsets.UTF_8) } ?: getString(Res.string.meshtastic)
|
||||
val deviceId = idBytes?.let { String(it, Charsets.UTF_8) }?.replace("!", "")
|
||||
var displayName = recentMap[address] ?: shortName
|
||||
if (deviceId != null && (displayName.split("_").none { it == deviceId })) {
|
||||
displayName += "_$deviceId"
|
||||
}
|
||||
DeviceListEntry.Tcp(displayName, address)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
}
|
||||
|
||||
val usbDevicesFlow =
|
||||
usbRepository.serialDevices.map { usb ->
|
||||
usb.map { (_, d) -> DeviceListEntry.Usb(radioInterfaceService, usbManagerLazy.get(), d) }
|
||||
}
|
||||
|
||||
return combine(
|
||||
nodeDb,
|
||||
bondedBleFlow,
|
||||
processedTcpFlow,
|
||||
usbDevicesFlow,
|
||||
networkRepository.resolvedList,
|
||||
recentAddressesDataSource.recentAddresses,
|
||||
) { args: Array<Any> ->
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
val db = args[0] as Map<Int, Node>
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
val bondedBle = args[1] as List<DeviceListEntry.Ble>
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
val processedTcp = args[2] as List<DeviceListEntry.Tcp>
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
val usbDevices = args[3] as List<DeviceListEntry.Usb>
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
val resolved = args[4] as List<NsdServiceInfo>
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
val recentList = args[5] as List<RecentAddress>
|
||||
|
||||
val bleForUi =
|
||||
bondedBle
|
||||
.map { entry ->
|
||||
val matchingNode =
|
||||
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
|
||||
db.values.find { node ->
|
||||
val suffix = entry.peripheral.getMeshtasticShortName()?.lowercase(Locale.ROOT)
|
||||
suffix != null && node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
entry.copy(node = matchingNode)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
|
||||
val usbForUi =
|
||||
(usbDevices + if (showMock) listOf(DeviceListEntry.Mock("Demo Mode")) else emptyList()).map { entry ->
|
||||
val matchingNode =
|
||||
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
|
||||
db.values.find { node ->
|
||||
val suffix = entry.name.split("_").lastOrNull()?.lowercase(Locale.ROOT)
|
||||
suffix != null &&
|
||||
suffix.length >= suffixLength &&
|
||||
node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
entry.copy(node = matchingNode)
|
||||
}
|
||||
|
||||
val discoveredTcpForUi =
|
||||
processedTcp.map { entry ->
|
||||
val matchingNode =
|
||||
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
|
||||
val resolvedService = resolved.find { "t${it.toAddressString()}" == entry.fullAddress }
|
||||
val deviceId = resolvedService?.attributes?.get("id")?.let { String(it, Charsets.UTF_8) }
|
||||
db.values.find { node ->
|
||||
node.user.id == deviceId || (deviceId != null && node.user.id == "!$deviceId")
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
entry.copy(node = matchingNode)
|
||||
}
|
||||
|
||||
val discoveredTcpAddresses = processedTcp.map { it.fullAddress }.toSet()
|
||||
val recentTcpForUi =
|
||||
recentList
|
||||
.filterNot { discoveredTcpAddresses.contains(it.address) }
|
||||
.map { DeviceListEntry.Tcp(it.name, it.address) }
|
||||
.map { entry ->
|
||||
val matchingNode =
|
||||
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
|
||||
val suffix = entry.name.split("_").lastOrNull()?.lowercase(Locale.ROOT)
|
||||
db.values.find { node ->
|
||||
suffix != null &&
|
||||
suffix.length >= suffixLength &&
|
||||
node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
entry.copy(node = matchingNode)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
|
||||
DiscoveredDevices(
|
||||
bleDevices = bleForUi,
|
||||
usbDevices = usbForUi,
|
||||
discoveredTcpDevices = discoveredTcpForUi,
|
||||
recentTcpDevices = recentTcpForUi,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,13 +53,13 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.TracerouteMapAvailability
|
||||
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
|
||||
import org.meshtastic.core.model.util.dispatchMeshtasticUri
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.client_notification
|
||||
import org.meshtastic.core.resources.compromised_keys
|
||||
import org.meshtastic.core.service.IMeshService
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.service.TracerouteResponse
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.client_notification
|
||||
import org.meshtastic.core.strings.compromised_keys
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import org.meshtastic.core.ui.util.ComposableContent
|
||||
|
||||
@@ -46,16 +46,16 @@ import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.device
|
||||
import org.meshtastic.core.strings.environment
|
||||
import org.meshtastic.core.strings.host
|
||||
import org.meshtastic.core.strings.neighbor_info
|
||||
import org.meshtastic.core.strings.pax
|
||||
import org.meshtastic.core.strings.position_log
|
||||
import org.meshtastic.core.strings.power
|
||||
import org.meshtastic.core.strings.signal
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.device
|
||||
import org.meshtastic.core.resources.environment
|
||||
import org.meshtastic.core.resources.host
|
||||
import org.meshtastic.core.resources.neighbor_info
|
||||
import org.meshtastic.core.resources.pax
|
||||
import org.meshtastic.core.resources.position_log
|
||||
import org.meshtastic.core.resources.power
|
||||
import org.meshtastic.core.resources.signal
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.feature.map.node.NodeMapScreen
|
||||
import org.meshtastic.feature.map.node.NodeMapViewModel
|
||||
|
||||
@@ -36,14 +36,14 @@ import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.connected_count
|
||||
import org.meshtastic.core.resources.connecting
|
||||
import org.meshtastic.core.resources.device_sleeping
|
||||
import org.meshtastic.core.resources.disconnected
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.connected_count
|
||||
import org.meshtastic.core.strings.connecting
|
||||
import org.meshtastic.core.strings.device_sleeping
|
||||
import org.meshtastic.core.strings.disconnected
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.Telemetry
|
||||
|
||||
@@ -43,15 +43,15 @@ import org.meshtastic.core.model.util.SfppHasher
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.core.model.util.toOneLiner
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.critical_alert
|
||||
import org.meshtastic.core.resources.error_duty_cycle
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.unknown_username
|
||||
import org.meshtastic.core.resources.waypoint_received
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.service.filter.MessageFilterService
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.critical_alert
|
||||
import org.meshtastic.core.strings.error_duty_cycle
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.core.strings.waypoint_received
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Paxcount
|
||||
|
||||
@@ -21,10 +21,10 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.unknown_username
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import java.util.Locale
|
||||
|
||||
@@ -107,7 +107,19 @@ class MeshService : Service() {
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
try {
|
||||
super.onCreate()
|
||||
} catch (e: IllegalStateException) {
|
||||
// Hilt can throw IllegalStateException in tests if the component is not created.
|
||||
// This can happen if the service is started by the system (e.g. after a crash or on boot)
|
||||
// before the test rule has a chance to create the component.
|
||||
if (e.message?.contains("HiltAndroidRule") == true) {
|
||||
Logger.w(e) { "MeshService created before Hilt component was ready in test. Stopping service." }
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
throw e
|
||||
}
|
||||
Logger.i { "Creating mesh service" }
|
||||
serviceNotifications.initChannels()
|
||||
|
||||
|
||||
@@ -53,27 +53,27 @@ import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.client_notification
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.low_battery_message
|
||||
import org.meshtastic.core.resources.low_battery_title
|
||||
import org.meshtastic.core.resources.mark_as_read
|
||||
import org.meshtastic.core.resources.meshtastic_alerts_notifications
|
||||
import org.meshtastic.core.resources.meshtastic_app_name
|
||||
import org.meshtastic.core.resources.meshtastic_broadcast_notifications
|
||||
import org.meshtastic.core.resources.meshtastic_low_battery_notifications
|
||||
import org.meshtastic.core.resources.meshtastic_low_battery_temporary_remote_notifications
|
||||
import org.meshtastic.core.resources.meshtastic_messages_notifications
|
||||
import org.meshtastic.core.resources.meshtastic_new_nodes_notifications
|
||||
import org.meshtastic.core.resources.meshtastic_service_notifications
|
||||
import org.meshtastic.core.resources.meshtastic_waypoints_notifications
|
||||
import org.meshtastic.core.resources.new_node_seen
|
||||
import org.meshtastic.core.resources.no_local_stats
|
||||
import org.meshtastic.core.resources.reply
|
||||
import org.meshtastic.core.resources.you
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.SERVICE_NOTIFY_ID
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.client_notification
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.low_battery_message
|
||||
import org.meshtastic.core.strings.low_battery_title
|
||||
import org.meshtastic.core.strings.mark_as_read
|
||||
import org.meshtastic.core.strings.meshtastic_alerts_notifications
|
||||
import org.meshtastic.core.strings.meshtastic_app_name
|
||||
import org.meshtastic.core.strings.meshtastic_broadcast_notifications
|
||||
import org.meshtastic.core.strings.meshtastic_low_battery_notifications
|
||||
import org.meshtastic.core.strings.meshtastic_low_battery_temporary_remote_notifications
|
||||
import org.meshtastic.core.strings.meshtastic_messages_notifications
|
||||
import org.meshtastic.core.strings.meshtastic_new_nodes_notifications
|
||||
import org.meshtastic.core.strings.meshtastic_service_notifications
|
||||
import org.meshtastic.core.strings.meshtastic_waypoints_notifications
|
||||
import org.meshtastic.core.strings.new_node_seen
|
||||
import org.meshtastic.core.strings.no_local_stats
|
||||
import org.meshtastic.core.strings.reply
|
||||
import org.meshtastic.core.strings.you
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.LocalStats
|
||||
|
||||
@@ -26,14 +26,14 @@ import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.model.getFullTracerouteResponse
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.traceroute_duration
|
||||
import org.meshtastic.core.resources.traceroute_route_back_to_us
|
||||
import org.meshtastic.core.resources.traceroute_route_towards_dest
|
||||
import org.meshtastic.core.resources.unknown_username
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.service.TracerouteResponse
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.traceroute_duration
|
||||
import org.meshtastic.core.strings.traceroute_route_back_to_us
|
||||
import org.meshtastic.core.strings.traceroute_route_towards_dest
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -106,26 +106,26 @@ import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.app_too_old
|
||||
import org.meshtastic.core.resources.bottom_nav_settings
|
||||
import org.meshtastic.core.resources.connected
|
||||
import org.meshtastic.core.resources.connecting
|
||||
import org.meshtastic.core.resources.connections
|
||||
import org.meshtastic.core.resources.conversations
|
||||
import org.meshtastic.core.resources.device_sleeping
|
||||
import org.meshtastic.core.resources.disconnected
|
||||
import org.meshtastic.core.resources.firmware_old
|
||||
import org.meshtastic.core.resources.firmware_too_old
|
||||
import org.meshtastic.core.resources.map
|
||||
import org.meshtastic.core.resources.must_update
|
||||
import org.meshtastic.core.resources.nodes
|
||||
import org.meshtastic.core.resources.okay
|
||||
import org.meshtastic.core.resources.should_update
|
||||
import org.meshtastic.core.resources.should_update_firmware
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.view_on_map
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.app_too_old
|
||||
import org.meshtastic.core.strings.bottom_nav_settings
|
||||
import org.meshtastic.core.strings.connected
|
||||
import org.meshtastic.core.strings.connecting
|
||||
import org.meshtastic.core.strings.connections
|
||||
import org.meshtastic.core.strings.conversations
|
||||
import org.meshtastic.core.strings.device_sleeping
|
||||
import org.meshtastic.core.strings.disconnected
|
||||
import org.meshtastic.core.strings.firmware_old
|
||||
import org.meshtastic.core.strings.firmware_too_old
|
||||
import org.meshtastic.core.strings.map
|
||||
import org.meshtastic.core.strings.must_update
|
||||
import org.meshtastic.core.strings.nodes
|
||||
import org.meshtastic.core.strings.okay
|
||||
import org.meshtastic.core.strings.should_update
|
||||
import org.meshtastic.core.strings.should_update_firmware
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.view_on_map
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.icon.Conversations
|
||||
|
||||
@@ -64,17 +64,17 @@ import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.connected
|
||||
import org.meshtastic.core.resources.connected_device
|
||||
import org.meshtastic.core.resources.connected_sleeping
|
||||
import org.meshtastic.core.resources.connecting
|
||||
import org.meshtastic.core.resources.connections
|
||||
import org.meshtastic.core.resources.must_set_region
|
||||
import org.meshtastic.core.resources.no_device_selected
|
||||
import org.meshtastic.core.resources.not_connected
|
||||
import org.meshtastic.core.resources.set_your_region
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.connected
|
||||
import org.meshtastic.core.strings.connected_device
|
||||
import org.meshtastic.core.strings.connected_sleeping
|
||||
import org.meshtastic.core.strings.connecting
|
||||
import org.meshtastic.core.strings.connections
|
||||
import org.meshtastic.core.strings.must_set_region
|
||||
import org.meshtastic.core.strings.no_device_selected
|
||||
import org.meshtastic.core.strings.not_connected
|
||||
import org.meshtastic.core.strings.set_your_region
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
|
||||
@@ -18,16 +18,13 @@ package com.geeksville.mesh.ui.connections
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.RemoteException
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import co.touchlab.kermit.Severity
|
||||
import com.geeksville.mesh.domain.usecase.GetDiscoveredDevicesUseCase
|
||||
import com.geeksville.mesh.model.DeviceListEntry
|
||||
import com.geeksville.mesh.model.getMeshtasticShortName
|
||||
import com.geeksville.mesh.repository.network.NetworkRepository
|
||||
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
@@ -36,25 +33,18 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||
import org.meshtastic.core.datastore.model.RecentAddress
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.meshtastic
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -66,12 +56,9 @@ constructor(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val bluetoothRepository: BluetoothRepository,
|
||||
private val usbRepository: UsbRepository,
|
||||
private val usbManagerLazy: dagger.Lazy<UsbManager>,
|
||||
private val networkRepository: NetworkRepository,
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
private val recentAddressesDataSource: RecentAddressesDataSource,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val databaseManager: DatabaseManager,
|
||||
private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase,
|
||||
) : ViewModel() {
|
||||
private val context: Context
|
||||
get() = application.applicationContext
|
||||
@@ -81,142 +68,32 @@ constructor(
|
||||
private val _errorText = MutableStateFlow<String?>(null)
|
||||
val errorText: StateFlow<String?> = _errorText.asStateFlow()
|
||||
|
||||
private val nodeDb: StateFlow<Map<Int, Node>> = nodeRepository.nodeDBbyNum
|
||||
|
||||
private val bondedBleDevicesFlow: StateFlow<List<DeviceListEntry.Ble>> =
|
||||
bluetoothRepository.state
|
||||
.map { ble -> ble.bondedDevices.map { DeviceListEntry.Ble(it) } }
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
// Flow for discovered TCP devices, using recent addresses for potential name enrichment
|
||||
private val processedDiscoveredTcpDevicesFlow: StateFlow<List<DeviceListEntry.Tcp>> =
|
||||
combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) { tcpServices, recentList ->
|
||||
val recentMap = recentList.associateBy({ it.address }) { it.name }
|
||||
tcpServices
|
||||
.map { service ->
|
||||
val address = "t${service.toAddressString()}"
|
||||
val txtRecords = service.attributes // Map<String, ByteArray?>
|
||||
val shortNameBytes = txtRecords["shortname"]
|
||||
val idBytes = txtRecords["id"]
|
||||
|
||||
val shortName =
|
||||
shortNameBytes?.let { String(it, Charsets.UTF_8) } ?: getString(Res.string.meshtastic)
|
||||
val deviceId = idBytes?.let { String(it, Charsets.UTF_8) }?.replace("!", "")
|
||||
var displayName = recentMap[address] ?: shortName
|
||||
if (deviceId != null && (displayName.split("_").none { it == deviceId })) {
|
||||
displayName += "_$deviceId"
|
||||
}
|
||||
DeviceListEntry.Tcp(displayName, address)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||
private val discoveredDevicesFlow =
|
||||
showMockInterface
|
||||
.flatMapLatest { showMock -> getDiscoveredDevicesUseCase.invoke(showMock) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||
|
||||
/** A combined list of bonded BLE devices for the UI. */
|
||||
val bleDevicesForUi: StateFlow<List<DeviceListEntry>> =
|
||||
combine(bondedBleDevicesFlow, nodeDb) { bonded, db ->
|
||||
bonded
|
||||
.map { entry: DeviceListEntry.Ble ->
|
||||
val matchingNode =
|
||||
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
|
||||
db.values.find { node ->
|
||||
val suffix = entry.peripheral.getMeshtasticShortName()?.lowercase(Locale.ROOT)
|
||||
suffix != null && node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
entry.copy(node = matchingNode)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
}
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
private val usbDevicesFlow: StateFlow<List<DeviceListEntry.Usb>> =
|
||||
usbRepository.serialDevices
|
||||
.map { usb -> usb.map { (_, d) -> DeviceListEntry.Usb(radioInterfaceService, usbManagerLazy.get(), d) } }
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
val mockDevice = DeviceListEntry.Mock("Demo Mode")
|
||||
discoveredDevicesFlow
|
||||
.map { it?.bleDevices ?: emptyList() }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
/** UI StateFlow for USB devices. */
|
||||
val usbDevicesForUi: StateFlow<List<DeviceListEntry>> =
|
||||
combine(usbDevicesFlow, showMockInterface) { usb, showMock ->
|
||||
val all: List<DeviceListEntry> = usb + if (showMock) listOf(mockDevice) else emptyList()
|
||||
all.map { entry ->
|
||||
val matchingNode =
|
||||
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
|
||||
nodeDb.value.values.find { node ->
|
||||
// Hard to match USB to node without connection, but we can try matching by name if it
|
||||
// follows Meshtastic pattern
|
||||
val suffix = entry.name.split("_").lastOrNull()?.lowercase(Locale.ROOT)
|
||||
suffix != null &&
|
||||
suffix.length >= suffixLength &&
|
||||
node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
entry.copy(node = matchingNode)
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(initialValue = if (showMockInterface.value) listOf(mockDevice) else emptyList())
|
||||
|
||||
// Flow for recent TCP devices, filtered to exclude any currently discovered devices
|
||||
private val filteredRecentTcpDevicesFlow: StateFlow<List<DeviceListEntry.Tcp>> =
|
||||
combine(recentAddressesDataSource.recentAddresses, processedDiscoveredTcpDevicesFlow) {
|
||||
recentList,
|
||||
discoveredDevices,
|
||||
->
|
||||
val discoveredDeviceAddresses = discoveredDevices.map { it.fullAddress }.toSet()
|
||||
recentList
|
||||
.filterNot { recentAddress -> discoveredDeviceAddresses.contains(recentAddress.address) }
|
||||
.map { recentAddress -> DeviceListEntry.Tcp(recentAddress.name, recentAddress.address) }
|
||||
.sortedBy { it.name }
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
private val suffixLength = 4
|
||||
discoveredDevicesFlow.map { it?.usbDevices ?: emptyList() }.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
/** UI StateFlow for discovered TCP devices. */
|
||||
val discoveredTcpDevicesForUi: StateFlow<List<DeviceListEntry>> =
|
||||
combine(processedDiscoveredTcpDevicesFlow, networkRepository.resolvedList, nodeDb) { devices, resolved, db ->
|
||||
devices.map { entry ->
|
||||
val matchingNode =
|
||||
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
|
||||
val resolvedService = resolved.find { "t${it.toAddressString()}" == entry.fullAddress }
|
||||
val deviceId = resolvedService?.attributes?.get("id")?.let { String(it, Charsets.UTF_8) }
|
||||
db.values.find { node ->
|
||||
node.user.id == deviceId || (deviceId != null && node.user.id == "!$deviceId")
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
entry.copy(node = matchingNode)
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(initialValue = listOf())
|
||||
discoveredDevicesFlow
|
||||
.map { it?.discoveredTcpDevices ?: emptyList() }
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
/** UI StateFlow for recently connected TCP devices that are not currently discovered. */
|
||||
val recentTcpDevicesForUi: StateFlow<List<DeviceListEntry>> =
|
||||
combine(filteredRecentTcpDevicesFlow, nodeDb) { devices, db ->
|
||||
devices.map { entry ->
|
||||
val matchingNode =
|
||||
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
|
||||
// For recent TCP, we don't have the TXT records, but maybe the name contains the ID
|
||||
val suffix = entry.name.split("_").lastOrNull()?.lowercase(Locale.ROOT)
|
||||
db.values.find { node ->
|
||||
suffix != null &&
|
||||
suffix.length >= suffixLength &&
|
||||
node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
entry.copy(node = matchingNode)
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(initialValue = listOf())
|
||||
discoveredDevicesFlow
|
||||
.map { it?.recentTcpDevices ?: emptyList() }
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
val selectedAddressFlow: StateFlow<String?> = radioInterfaceService.currentDeviceAddressFlow
|
||||
|
||||
|
||||
@@ -37,9 +37,9 @@ import no.nordicsemi.android.common.scanner.view.ScannerView
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.bluetooth_available_devices
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.bluetooth_available_devices
|
||||
|
||||
/**
|
||||
* Composable that displays a list of Bluetooth Low Energy (BLE) devices and allows scanning. It handles Bluetooth
|
||||
|
||||
+3
-3
@@ -35,9 +35,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.connecting
|
||||
import org.meshtastic.core.strings.disconnect
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.connecting
|
||||
import org.meshtastic.core.resources.disconnect
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
|
||||
+5
-6
@@ -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 com.geeksville.mesh.ui.connections.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -34,10 +33,10 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.geeksville.mesh.ui.connections.DeviceType
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.bluetooth
|
||||
import org.meshtastic.core.strings.network
|
||||
import org.meshtastic.core.strings.serial
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.bluetooth
|
||||
import org.meshtastic.core.resources.network
|
||||
import org.meshtastic.core.resources.serial
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Suppress("LambdaParameterEventTrailing")
|
||||
|
||||
+3
-3
@@ -48,9 +48,9 @@ import no.nordicsemi.kotlin.ble.client.exception.OperationFailedException
|
||||
import no.nordicsemi.kotlin.ble.client.exception.PeripheralNotConnectedException
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.disconnect
|
||||
import org.meshtastic.core.strings.firmware_version
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.disconnect
|
||||
import org.meshtastic.core.resources.firmware_version
|
||||
import org.meshtastic.core.ui.component.MaterialBatteryInfo
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@@ -56,12 +56,12 @@ import com.geeksville.mesh.model.DeviceListEntry
|
||||
import kotlinx.coroutines.delay
|
||||
import no.nordicsemi.android.common.ui.view.RssiIcon
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add
|
||||
import org.meshtastic.core.resources.bluetooth
|
||||
import org.meshtastic.core.resources.network
|
||||
import org.meshtastic.core.resources.serial
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add
|
||||
import org.meshtastic.core.strings.bluetooth
|
||||
import org.meshtastic.core.strings.network
|
||||
import org.meshtastic.core.strings.serial
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
|
||||
private const val RSSI_UPDATE_RATE_MS = 2000L
|
||||
|
||||
@@ -53,17 +53,17 @@ import com.geeksville.mesh.ui.connections.ScannerViewModel
|
||||
import com.geeksville.mesh.ui.connections.isValidAddress
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add_network_device
|
||||
import org.meshtastic.core.resources.address
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.confirm_forget_connection
|
||||
import org.meshtastic.core.resources.discovered_network_devices
|
||||
import org.meshtastic.core.resources.forget_connection
|
||||
import org.meshtastic.core.resources.ip_port
|
||||
import org.meshtastic.core.resources.no_network_devices
|
||||
import org.meshtastic.core.resources.recent_network_devices
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add_network_device
|
||||
import org.meshtastic.core.strings.address
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.confirm_forget_connection
|
||||
import org.meshtastic.core.strings.discovered_network_devices
|
||||
import org.meshtastic.core.strings.forget_connection
|
||||
import org.meshtastic.core.strings.ip_port
|
||||
import org.meshtastic.core.strings.no_network_devices
|
||||
import org.meshtastic.core.strings.recent_network_devices
|
||||
import org.meshtastic.core.ui.component.MeshtasticResourceDialog
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.model.DeviceListEntry
|
||||
import com.geeksville.mesh.ui.connections.ScannerViewModel
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.no_usb_devices
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.no_usb_devices
|
||||
|
||||
@Composable
|
||||
fun UsbDevices(
|
||||
|
||||
@@ -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 com.geeksville.mesh.ui.contact
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
@@ -49,8 +48,8 @@ import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.navigation.ChannelsRoutes
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.conversations
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.conversations
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.icon.Conversations
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
||||
@@ -53,10 +53,10 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.model.Contact
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.sample_message
|
||||
import org.meshtastic.core.strings.some_username
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.sample_message
|
||||
import org.meshtastic.core.resources.some_username
|
||||
import org.meshtastic.core.resources.unknown_username
|
||||
import org.meshtastic.core.ui.component.SecurityIcon
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
|
||||
@@ -70,27 +70,27 @@ import org.meshtastic.core.database.entity.ContactSettings
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.model.util.formatMuteRemainingTime
|
||||
import org.meshtastic.core.model.util.getChannel
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.are_you_sure
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.channel_invalid
|
||||
import org.meshtastic.core.strings.close_selection
|
||||
import org.meshtastic.core.strings.conversations
|
||||
import org.meshtastic.core.strings.currently
|
||||
import org.meshtastic.core.strings.delete
|
||||
import org.meshtastic.core.strings.delete_messages
|
||||
import org.meshtastic.core.strings.delete_selection
|
||||
import org.meshtastic.core.strings.mute_1_week
|
||||
import org.meshtastic.core.strings.mute_8_hours
|
||||
import org.meshtastic.core.strings.mute_always
|
||||
import org.meshtastic.core.strings.mute_notifications
|
||||
import org.meshtastic.core.strings.mute_status_always
|
||||
import org.meshtastic.core.strings.mute_status_muted_for_days
|
||||
import org.meshtastic.core.strings.mute_status_muted_for_hours
|
||||
import org.meshtastic.core.strings.mute_status_unmuted
|
||||
import org.meshtastic.core.strings.okay
|
||||
import org.meshtastic.core.strings.select_all
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.are_you_sure
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.channel_invalid
|
||||
import org.meshtastic.core.resources.close_selection
|
||||
import org.meshtastic.core.resources.conversations
|
||||
import org.meshtastic.core.resources.currently
|
||||
import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.delete_messages
|
||||
import org.meshtastic.core.resources.delete_selection
|
||||
import org.meshtastic.core.resources.mute_1_week
|
||||
import org.meshtastic.core.resources.mute_8_hours
|
||||
import org.meshtastic.core.resources.mute_always
|
||||
import org.meshtastic.core.resources.mute_notifications
|
||||
import org.meshtastic.core.resources.mute_status_always
|
||||
import org.meshtastic.core.resources.mute_status_muted_for_days
|
||||
import org.meshtastic.core.resources.mute_status_muted_for_hours
|
||||
import org.meshtastic.core.resources.mute_status_unmuted
|
||||
import org.meshtastic.core.resources.okay
|
||||
import org.meshtastic.core.resources.select_all
|
||||
import org.meshtastic.core.resources.unmute
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.component.MeshtasticImportFAB
|
||||
|
||||
@@ -39,9 +39,9 @@ import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.getChannel
|
||||
import org.meshtastic.core.model.util.getShortDate
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_name
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.channel_name
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -48,8 +48,8 @@ import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.navigation.ChannelsRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.nodes
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.nodes
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Nodes
|
||||
|
||||
@@ -72,21 +72,21 @@ import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.util.getChannelUrl
|
||||
import org.meshtastic.core.model.util.qrCode
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add
|
||||
import org.meshtastic.core.resources.apply
|
||||
import org.meshtastic.core.resources.are_you_sure_change_default
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.cant_change_no_radio
|
||||
import org.meshtastic.core.resources.edit
|
||||
import org.meshtastic.core.resources.generate_qr_code
|
||||
import org.meshtastic.core.resources.modem_preset
|
||||
import org.meshtastic.core.resources.navigate_into_label
|
||||
import org.meshtastic.core.resources.replace
|
||||
import org.meshtastic.core.resources.reset
|
||||
import org.meshtastic.core.resources.reset_to_defaults
|
||||
import org.meshtastic.core.resources.share_channels_qr
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add
|
||||
import org.meshtastic.core.strings.apply
|
||||
import org.meshtastic.core.strings.are_you_sure_change_default
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.cant_change_no_radio
|
||||
import org.meshtastic.core.strings.edit
|
||||
import org.meshtastic.core.strings.generate_qr_code
|
||||
import org.meshtastic.core.strings.modem_preset
|
||||
import org.meshtastic.core.strings.navigate_into_label
|
||||
import org.meshtastic.core.strings.replace
|
||||
import org.meshtastic.core.strings.reset
|
||||
import org.meshtastic.core.strings.reset_to_defaults
|
||||
import org.meshtastic.core.strings.share_channels_qr
|
||||
import org.meshtastic.core.ui.component.AdaptiveTwoPane
|
||||
import org.meshtastic.core.ui.component.ChannelSelection
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
|
||||
@@ -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 com.geeksville.mesh.ui.sharing
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -43,12 +42,12 @@ import com.geeksville.mesh.model.Contact
|
||||
import com.geeksville.mesh.ui.contact.ContactItem
|
||||
import com.geeksville.mesh.ui.contact.ContactsViewModel
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.sample_message
|
||||
import org.meshtastic.core.strings.share
|
||||
import org.meshtastic.core.strings.share_to
|
||||
import org.meshtastic.core.strings.some_username
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.sample_message
|
||||
import org.meshtastic.core.resources.share
|
||||
import org.meshtastic.core.resources.share_to
|
||||
import org.meshtastic.core.resources.some_username
|
||||
import org.meshtastic.core.resources.unknown_username
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Copyright (c) 2025 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
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Meshtastic</string>
|
||||
</resources>
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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 com.geeksville.mesh.repository.radio
|
||||
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.mock.mock
|
||||
import no.nordicsemi.kotlin.ble.client.mock.ConnectionResult
|
||||
import no.nordicsemi.kotlin.ble.client.mock.PeripheralSpec
|
||||
import no.nordicsemi.kotlin.ble.client.mock.PeripheralSpecEventHandler
|
||||
import no.nordicsemi.kotlin.ble.client.mock.Proximity
|
||||
import no.nordicsemi.kotlin.ble.client.mock.ReadResponse
|
||||
import no.nordicsemi.kotlin.ble.client.mock.WriteResponse
|
||||
import no.nordicsemi.kotlin.ble.client.mock.internal.MockRemoteCharacteristic
|
||||
import no.nordicsemi.kotlin.ble.core.CharacteristicProperty
|
||||
import no.nordicsemi.kotlin.ble.core.LegacyAdvertisingSetParameters
|
||||
import no.nordicsemi.kotlin.ble.core.Permission
|
||||
import no.nordicsemi.kotlin.ble.environment.android.mock.MockAndroidEnvironment
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.FROMNUM_CHARACTERISTIC
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NordicBleInterfaceDrainTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val address = "00:11:22:33:44:55"
|
||||
|
||||
@Test
|
||||
fun `drainPacketQueueAndDispatch reads multiple packets until empty`() = runTest(testDispatcher) {
|
||||
val mockEnvironment = MockAndroidEnvironment.Api31(isBluetoothEnabled = true)
|
||||
val centralManager = CentralManager.mock(mockEnvironment, scope = backgroundScope)
|
||||
val service = mockk<RadioInterfaceService>(relaxed = true)
|
||||
|
||||
var fromRadioHandle: Int = -1
|
||||
var fromNumHandle: Int = -1
|
||||
val packetsToRead = mutableListOf(byteArrayOf(0x01), byteArrayOf(0x02), byteArrayOf(0x03))
|
||||
|
||||
val eventHandler =
|
||||
object : PeripheralSpecEventHandler {
|
||||
override fun onConnectionRequest(preferredPhy: List<no.nordicsemi.kotlin.ble.core.Phy>) =
|
||||
ConnectionResult.Accept
|
||||
|
||||
override fun onReadRequest(characteristic: MockRemoteCharacteristic): ReadResponse {
|
||||
if (characteristic.instanceId == fromRadioHandle) {
|
||||
return if (packetsToRead.isNotEmpty()) {
|
||||
val p = packetsToRead.removeAt(0)
|
||||
println("Mock: Returning packet ${p.contentToString()}")
|
||||
ReadResponse.Success(p)
|
||||
} else {
|
||||
println("Mock: Queue empty, returning empty")
|
||||
ReadResponse.Success(byteArrayOf())
|
||||
}
|
||||
}
|
||||
return ReadResponse.Success(byteArrayOf())
|
||||
}
|
||||
|
||||
override fun onWriteRequest(characteristic: MockRemoteCharacteristic, value: ByteArray) =
|
||||
WriteResponse.Success
|
||||
}
|
||||
|
||||
val peripheralSpec =
|
||||
PeripheralSpec.simulatePeripheral(identifier = address, proximity = Proximity.IMMEDIATE) {
|
||||
advertising(
|
||||
parameters = LegacyAdvertisingSetParameters(connectable = true, interval = 100.milliseconds),
|
||||
) {
|
||||
CompleteLocalName("Meshtastic_Drain")
|
||||
}
|
||||
connectable(
|
||||
name = "Meshtastic_Drain",
|
||||
isBonded = true,
|
||||
eventHandler = eventHandler,
|
||||
cachedServices = {
|
||||
Service(uuid = SERVICE_UUID) {
|
||||
Characteristic(
|
||||
uuid = TORADIO_CHARACTERISTIC,
|
||||
properties =
|
||||
setOf(
|
||||
CharacteristicProperty.WRITE,
|
||||
CharacteristicProperty.WRITE_WITHOUT_RESPONSE,
|
||||
),
|
||||
permission = Permission.WRITE,
|
||||
)
|
||||
fromNumHandle =
|
||||
Characteristic(
|
||||
uuid = FROMNUM_CHARACTERISTIC,
|
||||
properties = setOf(CharacteristicProperty.NOTIFY),
|
||||
permission = Permission.READ,
|
||||
)
|
||||
fromRadioHandle =
|
||||
Characteristic(
|
||||
uuid = FROMRADIO_CHARACTERISTIC,
|
||||
properties = setOf(CharacteristicProperty.READ),
|
||||
permission = Permission.READ,
|
||||
)
|
||||
Characteristic(
|
||||
uuid = LOGRADIO_CHARACTERISTIC,
|
||||
properties = setOf(CharacteristicProperty.NOTIFY),
|
||||
permission = Permission.READ,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
centralManager.simulatePeripherals(listOf(peripheralSpec))
|
||||
|
||||
val nordicInterface =
|
||||
NordicBleInterface(
|
||||
serviceScope = this,
|
||||
centralManager = centralManager,
|
||||
service = service,
|
||||
address = address,
|
||||
)
|
||||
|
||||
// Wait for connection
|
||||
delay(2000.milliseconds)
|
||||
verify(timeout = 5000) { service.onConnect() }
|
||||
clearMocks(service, answers = false, recordedCalls = true)
|
||||
|
||||
// Trigger drain
|
||||
println("Simulating FromNum notification...")
|
||||
peripheralSpec.simulateValueUpdate(fromNumHandle, byteArrayOf(0x01))
|
||||
|
||||
// Wait for all packets to be processed
|
||||
delay(2000.milliseconds)
|
||||
|
||||
// Verify handleFromRadio was called 3 times
|
||||
verify(timeout = 2000) { service.handleFromRadio(p = byteArrayOf(0x01)) }
|
||||
verify(timeout = 2000) { service.handleFromRadio(p = byteArrayOf(0x02)) }
|
||||
verify(timeout = 2000) { service.handleFromRadio(p = byteArrayOf(0x03)) }
|
||||
|
||||
assert(packetsToRead.isEmpty()) { "All packets should have been read" }
|
||||
|
||||
nordicInterface.close()
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ fun Project.configureDokka() {
|
||||
suppress.set(true)
|
||||
}
|
||||
perPackageOption {
|
||||
matchingRegex.set("org.meshtastic.core.strings.*")
|
||||
matchingRegex.set("org.meshtastic.core.resources.*")
|
||||
suppress.set(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ fun Project.configureKover() {
|
||||
|
||||
// Suppress generated code
|
||||
packages("hilt_aggregated_deps")
|
||||
packages("org.meshtastic.core.strings")
|
||||
packages("org.meshtastic.core.resources")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ BarcodeScanner(
|
||||
```mermaid
|
||||
graph TB
|
||||
:core:barcode[barcode]:::android-library
|
||||
:core:barcode -.-> :core:strings
|
||||
:core:barcode -.-> :core:resources
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
|
||||
@@ -29,7 +29,7 @@ configure<LibraryExtension> {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:strings"))
|
||||
implementation(project(":core:resources"))
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
|
||||
+2
-2
@@ -64,8 +64,8 @@ import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.PlanarYUVLuminanceSource
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.close
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.close
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
+2
-2
@@ -64,8 +64,8 @@ import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.close
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.close
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -53,7 +53,8 @@ constructor(
|
||||
hasPermissions = true,
|
||||
),
|
||||
)
|
||||
val state: StateFlow<BluetoothState> = _state.asStateFlow()
|
||||
val state: StateFlow<BluetoothState>
|
||||
get() = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||
|
||||
+5
-3
@@ -67,9 +67,11 @@ constructor(
|
||||
// 1. Emit cached data first, regardless of staleness.
|
||||
// This gives the UI something to show immediately.
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
cachedRelease?.let {
|
||||
Logger.d { "Emitting cached firmware for $releaseType (isStale=${it.isStale()})" }
|
||||
emit(it.asExternalModel())
|
||||
if (cachedRelease != null) {
|
||||
Logger.d { "Emitting cached firmware for $releaseType (isStale=${cachedRelease.isStale()})" }
|
||||
emit(cachedRelease.asExternalModel())
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
|
||||
// 2. If the cache was fresh and we are not forcing a refresh, we're done.
|
||||
|
||||
@@ -30,7 +30,7 @@ graph TB
|
||||
:core:database -.-> :core:di
|
||||
:core:database -.-> :core:model
|
||||
:core:database -.-> :core:proto
|
||||
:core:database -.-> :core:strings
|
||||
:core:database -.-> :core:resources
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
|
||||
@@ -36,7 +36,7 @@ dependencies {
|
||||
implementation(projects.core.di)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.strings)
|
||||
implementation(projects.core.resources)
|
||||
|
||||
implementation(libs.androidx.room.paging)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
@@ -19,33 +19,33 @@ package org.meshtastic.core.database.model
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.database.entity.Reaction
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.delivery_confirmed
|
||||
import org.meshtastic.core.strings.error
|
||||
import org.meshtastic.core.strings.message_delivery_status
|
||||
import org.meshtastic.core.strings.message_status_enroute
|
||||
import org.meshtastic.core.strings.message_status_queued
|
||||
import org.meshtastic.core.strings.message_status_sfpp_confirmed
|
||||
import org.meshtastic.core.strings.message_status_sfpp_routing
|
||||
import org.meshtastic.core.strings.routing_error_admin_bad_session_key
|
||||
import org.meshtastic.core.strings.routing_error_admin_public_key_unauthorized
|
||||
import org.meshtastic.core.strings.routing_error_bad_request
|
||||
import org.meshtastic.core.strings.routing_error_duty_cycle_limit
|
||||
import org.meshtastic.core.strings.routing_error_got_nak
|
||||
import org.meshtastic.core.strings.routing_error_max_retransmit
|
||||
import org.meshtastic.core.strings.routing_error_no_channel
|
||||
import org.meshtastic.core.strings.routing_error_no_interface
|
||||
import org.meshtastic.core.strings.routing_error_no_response
|
||||
import org.meshtastic.core.strings.routing_error_no_route
|
||||
import org.meshtastic.core.strings.routing_error_none
|
||||
import org.meshtastic.core.strings.routing_error_not_authorized
|
||||
import org.meshtastic.core.strings.routing_error_pki_failed
|
||||
import org.meshtastic.core.strings.routing_error_pki_send_fail_public_key
|
||||
import org.meshtastic.core.strings.routing_error_pki_unknown_pubkey
|
||||
import org.meshtastic.core.strings.routing_error_rate_limit_exceeded
|
||||
import org.meshtastic.core.strings.routing_error_timeout
|
||||
import org.meshtastic.core.strings.routing_error_too_large
|
||||
import org.meshtastic.core.strings.unrecognized
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.delivery_confirmed
|
||||
import org.meshtastic.core.resources.error
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.message_status_enroute
|
||||
import org.meshtastic.core.resources.message_status_queued
|
||||
import org.meshtastic.core.resources.message_status_sfpp_confirmed
|
||||
import org.meshtastic.core.resources.message_status_sfpp_routing
|
||||
import org.meshtastic.core.resources.routing_error_admin_bad_session_key
|
||||
import org.meshtastic.core.resources.routing_error_admin_public_key_unauthorized
|
||||
import org.meshtastic.core.resources.routing_error_bad_request
|
||||
import org.meshtastic.core.resources.routing_error_duty_cycle_limit
|
||||
import org.meshtastic.core.resources.routing_error_got_nak
|
||||
import org.meshtastic.core.resources.routing_error_max_retransmit
|
||||
import org.meshtastic.core.resources.routing_error_no_channel
|
||||
import org.meshtastic.core.resources.routing_error_no_interface
|
||||
import org.meshtastic.core.resources.routing_error_no_response
|
||||
import org.meshtastic.core.resources.routing_error_no_route
|
||||
import org.meshtastic.core.resources.routing_error_none
|
||||
import org.meshtastic.core.resources.routing_error_not_authorized
|
||||
import org.meshtastic.core.resources.routing_error_pki_failed
|
||||
import org.meshtastic.core.resources.routing_error_pki_send_fail_public_key
|
||||
import org.meshtastic.core.resources.routing_error_pki_unknown_pubkey
|
||||
import org.meshtastic.core.resources.routing_error_rate_limit_exceeded
|
||||
import org.meshtastic.core.resources.routing_error_timeout
|
||||
import org.meshtastic.core.resources.routing_error_too_large
|
||||
import org.meshtastic.core.resources.unrecognized
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Routing
|
||||
|
||||
|
||||
@@ -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,18 +14,17 @@
|
||||
* 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.database.model
|
||||
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.node_sort_alpha
|
||||
import org.meshtastic.core.strings.node_sort_channel
|
||||
import org.meshtastic.core.strings.node_sort_distance
|
||||
import org.meshtastic.core.strings.node_sort_hops_away
|
||||
import org.meshtastic.core.strings.node_sort_last_heard
|
||||
import org.meshtastic.core.strings.node_sort_via_favorite
|
||||
import org.meshtastic.core.strings.node_sort_via_mqtt
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.node_sort_alpha
|
||||
import org.meshtastic.core.resources.node_sort_channel
|
||||
import org.meshtastic.core.resources.node_sort_distance
|
||||
import org.meshtastic.core.resources.node_sort_hops_away
|
||||
import org.meshtastic.core.resources.node_sort_last_heard
|
||||
import org.meshtastic.core.resources.node_sort_via_favorite
|
||||
import org.meshtastic.core.resources.node_sort_via_mqtt
|
||||
|
||||
enum class NodeSortOption(val sqlValue: String, val stringRes: StringResource) {
|
||||
LAST_HEARD("last_heard", Res.string.node_sort_last_heard),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# `:core:strings`
|
||||
# `:core:resources`
|
||||
|
||||
## Overview
|
||||
The `:core:strings` module is the centralized source for all UI strings and localizable resources. It uses the **Compose Multiplatform Resource** library to provide a type-safe way to access strings.
|
||||
The `:core:resources` module is the centralized source for all UI strings and localizable resources. It uses the **Compose Multiplatform Resource** library to provide a type-safe way to access strings.
|
||||
|
||||
## Key Features
|
||||
|
||||
@@ -13,8 +13,8 @@ The library provides a standard way to access strings in Jetpack Compose.
|
||||
|
||||
```kotlin
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.your_string_key
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.your_string_key
|
||||
|
||||
Text(text = stringResource(Res.string.your_string_key))
|
||||
```
|
||||
@@ -24,7 +24,7 @@ Text(text = stringResource(Res.string.your_string_key))
|
||||
<!--region graph-->
|
||||
```mermaid
|
||||
graph TB
|
||||
:core:strings[strings]:::kmp-library
|
||||
:core:resources[strings]:::kmp-library
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
@@ -29,5 +29,5 @@ kotlin {
|
||||
|
||||
compose.resources {
|
||||
publicResClass = true
|
||||
packageOfResClass = "org.meshtastic.core.strings"
|
||||
packageOfResClass = "org.meshtastic.core.resources"
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* 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.strings
|
||||
package org.meshtastic.core.resources
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 322 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user