Skip to content

Commit 2676a51

Browse files
authored
refactor(ui): compose resources, domain layer (#4628)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
1 parent 96adc70 commit 2676a51

322 files changed

Lines changed: 3026 additions & 2785 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ This file serves as a comprehensive guide for AI agents and developers working o
3232
- **Material 3:** The app uses Material 3. Look for ways to use **Material 3 Expressive** components where appropriate.
3333
- **Strings:**
3434
- Do **not** use `app/src/main/res/values/strings.xml` for UI strings.
35-
- Use the **Compose Multiplatform Resource** library in `core/strings`.
36-
- **Definition:** Add strings to `core/strings/src/commonMain/composeResources/values/strings.xml`.
35+
- Use the **Compose Multiplatform Resource** library in `core:resources`.
36+
- **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`.
3737
- **Usage:**
3838
```kotlin
3939
import org.jetbrains.compose.resources.stringResource
40-
import org.meshtastic.core.strings.Res
41-
import org.meshtastic.core.strings.your_string_key
40+
import org.meshtastic.core.resources.Res
41+
import org.meshtastic.core.resources.your_string_key
4242

4343
Text(text = stringResource(Res.string.your_string_key))
4444
```
@@ -102,7 +102,7 @@ This file serves as a comprehensive guide for AI agents and developers working o
102102
1. **Explore First:** Before making changes, read `gradle/libs.versions.toml` and the relevant `build.gradle.kts` to understand the environment.
103103
2. **Plan:** Identify which modules (`core` or `feature`) need modification.
104104
3. **Implement:**
105-
- If adding a string, modify `core/strings`.
105+
- If adding a string, modify `core:resources`.
106106
- If adding a dependency, modify `libs.versions.toml` first.
107107
4. **Verify:**
108108
- Run `./gradlew spotlessApply` (Essential!).
@@ -118,8 +118,14 @@ This file serves as a comprehensive guide for AI agents and developers working o
118118
119119
## 7. Troubleshooting
120120
121-
- **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.
121+
- **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.
122122
- **Build Errors:** Check `gradle/libs.versions.toml` for version conflicts. Use `build-logic` conventions to ensure plugins are applied correctly.
123123
124124
---
125125
*Refer to `CONTRIBUTING.md` for human-centric processes like Code of Conduct and Pull Request etiquette.*
126+
127+
### E. Resources and Assets
128+
- **Centralization:** All global app resources (Strings, Drawables, Fonts, raw files) should be placed in `:core:resources`.
129+
- **Module Path:** `core/resources/src/commonMain/composeResources/`
130+
- **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.
131+
- **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`).

CONTRIBUTING.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ Thank you for your interest in contributing to Meshtastic-Android! We welcome co
1919
- Write clear, descriptive variable and function names.
2020
- Add comments where necessary, especially for complex logic.
2121
- Keep methods and classes focused and concise.
22-
- **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:strings`.
22+
- **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:resources`.
2323
- Do **not** use the legacy `app/src/main/res/values/strings.xml`.
24-
- **Definition:** Add strings to `core/strings/src/commonMain/composeResources/values/strings.xml`.
24+
- **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`.
2525
- **Usage:**
2626
```kotlin
2727
import org.jetbrains.compose.resources.stringResource
28-
import org.meshtastic.core.strings.Res
29-
import org.meshtastic.core.strings.your_string_key
28+
import org.meshtastic.core.resources.Res
29+
import org.meshtastic.core.resources.your_string_key
3030

3131
Text(text = stringResource(Res.string.your_string_key))
3232
```

app/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ graph TB
3939
:app -.-> :core:prefs
4040
:app -.-> :core:proto
4141
:app -.-> :core:service
42-
:app -.-> :core:strings
42+
:app -.-> :core:resources
4343
:app -.-> :core:ui
4444
:app -.-> :core:barcode
4545
:app -.-> :feature:intro

app/build.gradle.kts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,9 @@ secrets {
188188

189189
androidComponents {
190190
onVariants(selector().withBuildType("debug")) { variant ->
191-
variant.flavorName?.let { flavor ->
192-
variant.applicationId = "com.geeksville.mesh.$flavor.debug"
193-
}
191+
variant.flavorName?.let { flavor -> variant.applicationId = "com.geeksville.mesh.$flavor.debug" }
194192
}
195-
193+
196194
onVariants(selector().withBuildType("release")) { variant ->
197195
if (variant.flavorName == "google") {
198196
val variantNameCapped = variant.name.replaceFirstChar { it.uppercase() }
@@ -226,7 +224,7 @@ dependencies {
226224
implementation(projects.core.prefs)
227225
implementation(projects.core.proto)
228226
implementation(projects.core.service)
229-
implementation(projects.core.strings)
227+
implementation(projects.core.resources)
230228
implementation(projects.core.ui)
231229
implementation(projects.core.barcode)
232230
implementation(projects.feature.intro)

app/src/main/java/com/geeksville/mesh/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
5050
import no.nordicsemi.kotlin.ble.environment.android.compose.LocalEnvironmentOwner
5151
import org.meshtastic.core.model.util.dispatchMeshtasticUri
5252
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
53-
import org.meshtastic.core.strings.Res
54-
import org.meshtastic.core.strings.channel_invalid
53+
import org.meshtastic.core.resources.Res
54+
import org.meshtastic.core.resources.channel_invalid
5555
import org.meshtastic.core.ui.theme.AppTheme
5656
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
5757
import org.meshtastic.core.ui.util.showToast
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Copyright (c) 2025-2026 Meshtastic LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package com.geeksville.mesh.domain.usecase
18+
19+
import android.hardware.usb.UsbManager
20+
import android.net.nsd.NsdServiceInfo
21+
import com.geeksville.mesh.model.DeviceListEntry
22+
import com.geeksville.mesh.model.getMeshtasticShortName
23+
import com.geeksville.mesh.repository.network.NetworkRepository
24+
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
25+
import com.geeksville.mesh.repository.radio.RadioInterfaceService
26+
import com.geeksville.mesh.repository.usb.UsbRepository
27+
import kotlinx.coroutines.flow.Flow
28+
import kotlinx.coroutines.flow.combine
29+
import kotlinx.coroutines.flow.map
30+
import org.jetbrains.compose.resources.getString
31+
import org.meshtastic.core.ble.BluetoothRepository
32+
import org.meshtastic.core.data.repository.NodeRepository
33+
import org.meshtastic.core.database.DatabaseManager
34+
import org.meshtastic.core.database.model.Node
35+
import org.meshtastic.core.datastore.RecentAddressesDataSource
36+
import org.meshtastic.core.datastore.model.RecentAddress
37+
import org.meshtastic.core.resources.Res
38+
import org.meshtastic.core.resources.meshtastic
39+
import java.util.Locale
40+
import javax.inject.Inject
41+
42+
data class DiscoveredDevices(
43+
val bleDevices: List<DeviceListEntry>,
44+
val usbDevices: List<DeviceListEntry>,
45+
val discoveredTcpDevices: List<DeviceListEntry>,
46+
val recentTcpDevices: List<DeviceListEntry>,
47+
)
48+
49+
@Suppress("LongParameterList")
50+
class GetDiscoveredDevicesUseCase
51+
@Inject
52+
constructor(
53+
private val bluetoothRepository: BluetoothRepository,
54+
private val networkRepository: NetworkRepository,
55+
private val recentAddressesDataSource: RecentAddressesDataSource,
56+
private val nodeRepository: NodeRepository,
57+
private val databaseManager: DatabaseManager,
58+
private val usbRepository: UsbRepository,
59+
private val radioInterfaceService: RadioInterfaceService,
60+
private val usbManagerLazy: dagger.Lazy<UsbManager>,
61+
) {
62+
private val suffixLength = 4
63+
64+
@Suppress("LongMethod", "CyclomaticComplexMethod")
65+
fun invoke(showMock: Boolean): Flow<DiscoveredDevices> {
66+
val nodeDb = nodeRepository.nodeDBbyNum
67+
68+
val bondedBleFlow = bluetoothRepository.state.map { ble -> ble.bondedDevices.map { DeviceListEntry.Ble(it) } }
69+
70+
val processedTcpFlow =
71+
combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) {
72+
tcpServices,
73+
recentList,
74+
->
75+
val recentMap = recentList.associateBy({ it.address }) { it.name }
76+
tcpServices
77+
.map { service ->
78+
val address = "t${service.toAddressString()}"
79+
val txtRecords = service.attributes
80+
val shortNameBytes = txtRecords["shortname"]
81+
val idBytes = txtRecords["id"]
82+
83+
val shortName =
84+
shortNameBytes?.let { String(it, Charsets.UTF_8) } ?: getString(Res.string.meshtastic)
85+
val deviceId = idBytes?.let { String(it, Charsets.UTF_8) }?.replace("!", "")
86+
var displayName = recentMap[address] ?: shortName
87+
if (deviceId != null && (displayName.split("_").none { it == deviceId })) {
88+
displayName += "_$deviceId"
89+
}
90+
DeviceListEntry.Tcp(displayName, address)
91+
}
92+
.sortedBy { it.name }
93+
}
94+
95+
val usbDevicesFlow =
96+
usbRepository.serialDevices.map { usb ->
97+
usb.map { (_, d) -> DeviceListEntry.Usb(radioInterfaceService, usbManagerLazy.get(), d) }
98+
}
99+
100+
return combine(
101+
nodeDb,
102+
bondedBleFlow,
103+
processedTcpFlow,
104+
usbDevicesFlow,
105+
networkRepository.resolvedList,
106+
recentAddressesDataSource.recentAddresses,
107+
) { args: Array<Any> ->
108+
@Suppress("UNCHECKED_CAST", "MagicNumber")
109+
val db = args[0] as Map<Int, Node>
110+
111+
@Suppress("UNCHECKED_CAST", "MagicNumber")
112+
val bondedBle = args[1] as List<DeviceListEntry.Ble>
113+
114+
@Suppress("UNCHECKED_CAST", "MagicNumber")
115+
val processedTcp = args[2] as List<DeviceListEntry.Tcp>
116+
117+
@Suppress("UNCHECKED_CAST", "MagicNumber")
118+
val usbDevices = args[3] as List<DeviceListEntry.Usb>
119+
120+
@Suppress("UNCHECKED_CAST", "MagicNumber")
121+
val resolved = args[4] as List<NsdServiceInfo>
122+
123+
@Suppress("UNCHECKED_CAST", "MagicNumber")
124+
val recentList = args[5] as List<RecentAddress>
125+
126+
val bleForUi =
127+
bondedBle
128+
.map { entry ->
129+
val matchingNode =
130+
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
131+
db.values.find { node ->
132+
val suffix = entry.peripheral.getMeshtasticShortName()?.lowercase(Locale.ROOT)
133+
suffix != null && node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
134+
}
135+
} else {
136+
null
137+
}
138+
entry.copy(node = matchingNode)
139+
}
140+
.sortedBy { it.name }
141+
142+
val usbForUi =
143+
(usbDevices + if (showMock) listOf(DeviceListEntry.Mock("Demo Mode")) else emptyList()).map { entry ->
144+
val matchingNode =
145+
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
146+
db.values.find { node ->
147+
val suffix = entry.name.split("_").lastOrNull()?.lowercase(Locale.ROOT)
148+
suffix != null &&
149+
suffix.length >= suffixLength &&
150+
node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
151+
}
152+
} else {
153+
null
154+
}
155+
entry.copy(node = matchingNode)
156+
}
157+
158+
val discoveredTcpForUi =
159+
processedTcp.map { entry ->
160+
val matchingNode =
161+
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
162+
val resolvedService = resolved.find { "t${it.toAddressString()}" == entry.fullAddress }
163+
val deviceId = resolvedService?.attributes?.get("id")?.let { String(it, Charsets.UTF_8) }
164+
db.values.find { node ->
165+
node.user.id == deviceId || (deviceId != null && node.user.id == "!$deviceId")
166+
}
167+
} else {
168+
null
169+
}
170+
entry.copy(node = matchingNode)
171+
}
172+
173+
val discoveredTcpAddresses = processedTcp.map { it.fullAddress }.toSet()
174+
val recentTcpForUi =
175+
recentList
176+
.filterNot { discoveredTcpAddresses.contains(it.address) }
177+
.map { DeviceListEntry.Tcp(it.name, it.address) }
178+
.map { entry ->
179+
val matchingNode =
180+
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
181+
val suffix = entry.name.split("_").lastOrNull()?.lowercase(Locale.ROOT)
182+
db.values.find { node ->
183+
suffix != null &&
184+
suffix.length >= suffixLength &&
185+
node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
186+
}
187+
} else {
188+
null
189+
}
190+
entry.copy(node = matchingNode)
191+
}
192+
.sortedBy { it.name }
193+
194+
DiscoveredDevices(
195+
bleDevices = bleForUi,
196+
usbDevices = usbForUi,
197+
discoveredTcpDevices = discoveredTcpForUi,
198+
recentTcpDevices = recentTcpForUi,
199+
)
200+
}
201+
}
202+
}

app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource
5353
import org.meshtastic.core.model.TracerouteMapAvailability
5454
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
5555
import org.meshtastic.core.model.util.dispatchMeshtasticUri
56+
import org.meshtastic.core.resources.Res
57+
import org.meshtastic.core.resources.client_notification
58+
import org.meshtastic.core.resources.compromised_keys
5659
import org.meshtastic.core.service.IMeshService
5760
import org.meshtastic.core.service.MeshServiceNotifications
5861
import org.meshtastic.core.service.ServiceRepository
5962
import org.meshtastic.core.service.TracerouteResponse
60-
import org.meshtastic.core.strings.Res
61-
import org.meshtastic.core.strings.client_notification
62-
import org.meshtastic.core.strings.compromised_keys
6363
import org.meshtastic.core.ui.component.ScrollToTopEvent
6464
import org.meshtastic.core.ui.util.AlertManager
6565
import org.meshtastic.core.ui.util.ComposableContent

app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
4646
import org.meshtastic.core.navigation.NodeDetailRoutes
4747
import org.meshtastic.core.navigation.NodesRoutes
4848
import org.meshtastic.core.navigation.Route
49-
import org.meshtastic.core.strings.Res
50-
import org.meshtastic.core.strings.device
51-
import org.meshtastic.core.strings.environment
52-
import org.meshtastic.core.strings.host
53-
import org.meshtastic.core.strings.neighbor_info
54-
import org.meshtastic.core.strings.pax
55-
import org.meshtastic.core.strings.position_log
56-
import org.meshtastic.core.strings.power
57-
import org.meshtastic.core.strings.signal
58-
import org.meshtastic.core.strings.traceroute
49+
import org.meshtastic.core.resources.Res
50+
import org.meshtastic.core.resources.device
51+
import org.meshtastic.core.resources.environment
52+
import org.meshtastic.core.resources.host
53+
import org.meshtastic.core.resources.neighbor_info
54+
import org.meshtastic.core.resources.pax
55+
import org.meshtastic.core.resources.position_log
56+
import org.meshtastic.core.resources.power
57+
import org.meshtastic.core.resources.signal
58+
import org.meshtastic.core.resources.traceroute
5959
import org.meshtastic.core.ui.component.ScrollToTopEvent
6060
import org.meshtastic.feature.map.node.NodeMapScreen
6161
import org.meshtastic.feature.map.node.NodeMapViewModel

app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ import org.meshtastic.core.common.util.nowSeconds
3636
import org.meshtastic.core.data.repository.NodeRepository
3737
import org.meshtastic.core.data.repository.RadioConfigRepository
3838
import org.meshtastic.core.prefs.ui.UiPrefs
39+
import org.meshtastic.core.resources.Res
40+
import org.meshtastic.core.resources.connected_count
41+
import org.meshtastic.core.resources.connecting
42+
import org.meshtastic.core.resources.device_sleeping
43+
import org.meshtastic.core.resources.disconnected
44+
import org.meshtastic.core.resources.getString
3945
import org.meshtastic.core.service.ConnectionState
4046
import org.meshtastic.core.service.MeshServiceNotifications
41-
import org.meshtastic.core.strings.Res
42-
import org.meshtastic.core.strings.connected_count
43-
import org.meshtastic.core.strings.connecting
44-
import org.meshtastic.core.strings.device_sleeping
45-
import org.meshtastic.core.strings.disconnected
46-
import org.meshtastic.core.strings.getString
4747
import org.meshtastic.proto.AdminMessage
4848
import org.meshtastic.proto.Config
4949
import org.meshtastic.proto.Telemetry

app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ import org.meshtastic.core.model.util.SfppHasher
4343
import org.meshtastic.core.model.util.decodeOrNull
4444
import org.meshtastic.core.model.util.toOneLiner
4545
import org.meshtastic.core.prefs.mesh.MeshPrefs
46+
import org.meshtastic.core.resources.Res
47+
import org.meshtastic.core.resources.critical_alert
48+
import org.meshtastic.core.resources.error_duty_cycle
49+
import org.meshtastic.core.resources.getString
50+
import org.meshtastic.core.resources.unknown_username
51+
import org.meshtastic.core.resources.waypoint_received
4652
import org.meshtastic.core.service.MeshServiceNotifications
4753
import org.meshtastic.core.service.ServiceRepository
4854
import org.meshtastic.core.service.filter.MessageFilterService
49-
import org.meshtastic.core.strings.Res
50-
import org.meshtastic.core.strings.critical_alert
51-
import org.meshtastic.core.strings.error_duty_cycle
52-
import org.meshtastic.core.strings.getString
53-
import org.meshtastic.core.strings.unknown_username
54-
import org.meshtastic.core.strings.waypoint_received
5555
import org.meshtastic.proto.AdminMessage
5656
import org.meshtastic.proto.MeshPacket
5757
import org.meshtastic.proto.Paxcount

0 commit comments

Comments
 (0)