diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 3a36977bd..9898c03c4 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -331,7 +331,6 @@
UnusedParameter:ChannelSettingsItemList.kt$onBack: () -> Unit
UnusedParameter:ChannelSettingsItemList.kt$title: String
UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule
- ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet)
ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)
ViewModelInjection:DebugSearch.kt$viewModel
Wrapping:Message.kt${ event -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -> navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }
diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
index abb6ca82c..a78638a43 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
+++ b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
@@ -27,12 +27,12 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.MetricsViewModel
-import com.geeksville.mesh.ui.map.NodeMapViewModel
-import com.geeksville.mesh.ui.map.rememberMapViewWithLifecycle
import org.meshtastic.feature.map.addCopyright
import org.meshtastic.feature.map.addPolyline
import org.meshtastic.feature.map.addPositionMarkers
import org.meshtastic.feature.map.addScaleBarOverlay
+import org.meshtastic.feature.map.node.NodeMapViewModel
+import org.meshtastic.feature.map.rememberMapViewWithLifecycle
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
diff --git a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
index 52a0c7447..b420d276e 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
@@ -27,11 +27,9 @@ import androidx.compose.ui.Modifier
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.MetricsViewModel
-import com.geeksville.mesh.ui.map.MapView
-import com.geeksville.mesh.ui.map.NodeMapViewModel
import org.meshtastic.core.ui.component.MainAppBar
-
-const val DEG_D = 1e-7
+import org.meshtastic.feature.map.MapView
+import org.meshtastic.feature.map.node.NodeMapViewModel
@Composable
fun NodeMapScreen(
diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextExtensions.kt b/app/src/main/java/com/geeksville/mesh/android/ContextExtensions.kt
deleted file mode 100644
index b2de39a7e..000000000
--- a/app/src/main/java/com/geeksville/mesh/android/ContextExtensions.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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 .
- */
-
-package com.geeksville.mesh.android
-
-import android.app.Activity
-import android.content.Context
-import android.view.inputmethod.InputMethodManager
-import android.widget.Toast
-
-// / show a toast
-fun Context.toast(message: CharSequence) = Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
-
-// / Utility function to hide the soft keyboard per stack overflow
-fun Activity.hideKeyboard() {
- // Check if no view has focus:
- currentFocus?.let { v ->
- val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
- imm?.hideSoftInputFromWindow(v.windowToken, 0)
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt
index 774350619..5de1c6933 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt
@@ -21,10 +21,10 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
-import com.geeksville.mesh.ui.map.MapScreen
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.MapRoutes
import org.meshtastic.core.navigation.NodesRoutes
+import org.meshtastic.feature.map.MapScreen
fun NavGraphBuilder.mapGraph(navController: NavHostController) {
composable(deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/map"))) {
diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt
index fa8272547..58756fa38 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt
@@ -28,7 +28,6 @@ import androidx.annotation.RequiresPermission
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.CoroutineDispatchers
-import com.geeksville.mesh.android.hasBluetoothPermission
import com.geeksville.mesh.util.registerReceiverCompat
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
+import org.meshtastic.core.common.hasBluetoothPermission
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index 8555d1c30..bdf31fbfa 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -49,7 +49,6 @@ import com.geeksville.mesh.StoreAndForwardProtos
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.TelemetryProtos.LocalStats
import com.geeksville.mesh.XmodemProtos
-import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.copy
import com.geeksville.mesh.fromRadio
@@ -78,6 +77,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.analytics.DataPair
+import org.meshtastic.core.common.hasLocationPermission
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt
index ca08831c5..10a4bf3d2 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt
@@ -67,11 +67,11 @@ import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.model.MetricsViewModel
import org.meshtastic.core.model.util.metersIn
import org.meshtastic.core.model.util.toString
+import org.meshtastic.core.proto.formatPositionTime
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.theme.AppTheme
import java.text.DateFormat
-import kotlin.time.Duration.Companion.days
@Composable
private fun RowScope.PositionText(text: String, weight: Float) {
@@ -106,7 +106,6 @@ private fun HeaderItem(compactWidth: Boolean) {
const val DEG_D = 1e-7
const val HEADING_DEG = 1e-5
-private const val SECONDS_TO_MILLIS = 1000L
@Composable
fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateFormat: DateFormat, system: DisplayUnits) {
@@ -122,24 +121,10 @@ fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateForma
PositionText("${position.groundSpeed} Km/h", WEIGHT_15)
PositionText("%.0f°".format(position.groundTrack * HEADING_DEG), WEIGHT_15)
}
- PositionText(formatPositionTime(position, dateFormat), WEIGHT_40)
+ PositionText(position.formatPositionTime(dateFormat), WEIGHT_40)
}
}
-@Composable
-fun formatPositionTime(position: MeshProtos.Position, dateFormat: DateFormat): String {
- val currentTime = System.currentTimeMillis()
- val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds
- val isOlderThanSixMonths = position.time * SECONDS_TO_MILLIS < sixMonthsAgo
- val timeText =
- if (isOlderThanSixMonths) {
- stringResource(id = R.string.unknown_age)
- } else {
- dateFormat.format(position.time * SECONDS_TO_MILLIS)
- }
- return timeText
-}
-
@Composable
private fun ActionButtons(
clearButtonEnabled: Boolean,
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
index 17c9c489b..3950ecb74 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
@@ -60,7 +60,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
-import com.geeksville.mesh.android.gpsDisabled
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.settings.radio.RadioConfigItemList
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@@ -71,6 +70,7 @@ import com.geeksville.mesh.util.LanguageUtils.getLanguageMap
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.coroutines.delay
+import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
index 43363fe44..73a41424b 100644
--- a/core/common/build.gradle.kts
+++ b/core/common/build.gradle.kts
@@ -22,4 +22,4 @@ plugins {
android { namespace = "org.meshtastic.core.common" }
-dependencies {}
+dependencies { implementation(libs.core.ktx) }
diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt b/core/common/src/main/kotlin/org/meshtastic/core/common/ContextServices.kt
similarity index 98%
rename from app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
rename to core/common/src/main/kotlin/org/meshtastic/core/common/ContextServices.kt
index 818c23a8e..51ed91048 100644
--- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
+++ b/core/common/src/main/kotlin/org/meshtastic/core/common/ContextServices.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.android
+package org.meshtastic.core.common
import android.Manifest
import android.content.Context
diff --git a/core/proto/build.gradle.kts b/core/proto/build.gradle.kts
index e914679f3..10bc12232 100644
--- a/core/proto/build.gradle.kts
+++ b/core/proto/build.gradle.kts
@@ -34,6 +34,7 @@
plugins {
alias(libs.plugins.meshtastic.android.library)
+ alias(libs.plugins.meshtastic.android.library.compose)
alias(libs.plugins.protobuf)
}
@@ -53,6 +54,8 @@ protobuf {
}
dependencies {
+ implementation(projects.core.strings)
+
// This needs to be API for consuming modules
api(libs.protobuf.kotlin)
}
diff --git a/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt b/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt
new file mode 100644
index 000000000..5b6d9bab6
--- /dev/null
+++ b/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 .
+ */
+
+package org.meshtastic.core.proto
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.geeksville.mesh.MeshProtos
+import java.text.DateFormat
+import kotlin.time.Duration.Companion.days
+
+private const val SECONDS_TO_MILLIS = 1000L
+
+@Composable
+fun MeshProtos.Position.formatPositionTime(dateFormat: DateFormat): String {
+ val currentTime = System.currentTimeMillis()
+ val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds
+ val isOlderThanSixMonths = time * SECONDS_TO_MILLIS < sixMonthsAgo
+ val timeText =
+ if (isOlderThanSixMonths) {
+ stringResource(id = org.meshtastic.core.strings.R.string.unknown_age)
+ } else {
+ dateFormat.format(time * SECONDS_TO_MILLIS)
+ }
+ return timeText
+}
diff --git a/app/src/main/res/drawable/ic_baseline_location_on_24.xml b/core/ui/src/main/res/drawable/ic_baseline_location_on_24.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_baseline_location_on_24.xml
rename to core/ui/src/main/res/drawable/ic_baseline_location_on_24.xml
diff --git a/app/src/main/res/drawable/ic_map_location_dot_24.xml b/core/ui/src/main/res/drawable/ic_map_location_dot_24.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_map_location_dot_24.xml
rename to core/ui/src/main/res/drawable/ic_map_location_dot_24.xml
diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts
index b0c04419d..cba6a7b29 100644
--- a/feature/map/build.gradle.kts
+++ b/feature/map/build.gradle.kts
@@ -41,6 +41,7 @@ dependencies {
implementation(libs.bundles.osm)
googleImplementation(libs.bundles.maps.compose)
+ implementation(libs.accompanist.permissions)
implementation(libs.annotation)
implementation(libs.timber)
}
diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt
similarity index 97%
rename from app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt
rename to feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt
index 27d0c79d6..2b3874feb 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt
+++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.map
+package org.meshtastic.feature.map
import android.Manifest // Added for Accompanist
import android.content.Context
@@ -63,31 +63,25 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos.Waypoint
-import com.geeksville.mesh.android.gpsDisabled
-import com.geeksville.mesh.android.hasGps
import com.geeksville.mesh.copy
-import com.geeksville.mesh.ui.map.components.EditWaypointDialog
-import com.geeksville.mesh.util.SqlTileWriterExt
import com.geeksville.mesh.waypoint
import com.google.accompanist.permissions.ExperimentalPermissionsApi // Added for Accompanist
import com.google.accompanist.permissions.rememberMultiplePermissionsState // Added for Accompanist
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.meshtastic.core.common.gpsDisabled
+import org.meshtastic.core.common.hasGps
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.strings.R
-import org.meshtastic.feature.map.MapViewModel
-import org.meshtastic.feature.map.addCopyright
-import org.meshtastic.feature.map.addScaleBarOverlay
import org.meshtastic.feature.map.cluster.RadiusMarkerClusterer
import org.meshtastic.feature.map.component.CacheLayout
import org.meshtastic.feature.map.component.DownloadButton
+import org.meshtastic.feature.map.component.EditWaypointDialog
import org.meshtastic.feature.map.component.MapButton
-import org.meshtastic.feature.map.createLatLongGrid
import org.meshtastic.feature.map.model.CustomTileSource
import org.meshtastic.feature.map.model.MarkerWithLabel
-import org.meshtastic.feature.map.zoomIn
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
import org.osmdroid.config.Configuration
import org.osmdroid.events.MapEventsReceiver
@@ -277,10 +271,11 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
MyLocationNewOverlay(this).apply {
enableMyLocation()
enableFollowLocation()
- getBitmapFromVectorDrawable(context, com.geeksville.mesh.R.drawable.ic_map_location_dot_24)?.let {
- setPersonIcon(it)
- setPersonAnchor(0.5f, 0.5f)
- }
+ getBitmapFromVectorDrawable(context, org.meshtastic.core.ui.R.drawable.ic_map_location_dot_24)
+ ?.let {
+ setPersonIcon(it)
+ setPersonAnchor(0.5f, 0.5f)
+ }
getBitmapFromVectorDrawable(context, org.meshtastic.core.ui.R.drawable.ic_map_navigation_24)?.let {
setDirectionIcon(it)
setDirectionAnchor(0.5f, 0.5f)
@@ -309,7 +304,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap())
val markerIcon = remember {
- AppCompatResources.getDrawable(context, com.geeksville.mesh.R.drawable.ic_baseline_location_on_24)
+ AppCompatResources.getDrawable(context, org.meshtastic.core.ui.R.drawable.ic_baseline_location_on_24)
}
fun MapView.onNodesChanged(nodes: Collection): List {
diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewWithLifecycle.kt
similarity index 97%
rename from app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt
rename to feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewWithLifecycle.kt
index bdfe8c019..090ae1b98 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt
+++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewWithLifecycle.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.map
+package org.meshtastic.feature.map
import android.annotation.SuppressLint
import android.content.Context
@@ -33,7 +33,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
-import org.meshtastic.feature.map.requiredZoomLevel
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.ITileSource
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
@@ -72,7 +71,7 @@ private const val DEFAULT_ZOOM_LEVEL = 15.0
@Suppress("MagicNumber")
@Composable
-internal fun rememberMapViewWithLifecycle(
+fun rememberMapViewWithLifecycle(
applicationId: String,
box: BoundingBox,
tileSource: ITileSource = TileSourceFactory.DEFAULT_TILE_SOURCE,
diff --git a/app/src/fdroid/java/com/geeksville/mesh/util/SqlTileWriterExt.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/SqlTileWriterExt.kt
similarity index 99%
rename from app/src/fdroid/java/com/geeksville/mesh/util/SqlTileWriterExt.kt
rename to feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/SqlTileWriterExt.kt
index 896359d61..e5999e2e1 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/util/SqlTileWriterExt.kt
+++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/SqlTileWriterExt.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.util
+package org.meshtastic.feature.map
import android.database.Cursor
import org.osmdroid.tileprovider.modules.DatabaseFileArchive
diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
similarity index 99%
rename from app/src/fdroid/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt
rename to feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
index 227be8a9c..7ce69740b 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt
+++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
@@ -15,9 +15,11 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.map.components
+package org.meshtastic.feature.map.component
import android.app.DatePickerDialog
+import android.app.TimePickerDialog
+import android.text.format.DateFormat
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.compose.foundation.Image
@@ -76,7 +78,7 @@ import java.util.Locale
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@Composable
-internal fun EditWaypointDialog(
+fun EditWaypointDialog(
waypoint: Waypoint,
onSendClicked: (Waypoint) -> Unit,
onDeleteClicked: (Waypoint) -> Unit,
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt
similarity index 94%
rename from app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt
index d8ad936e8..246360fe8 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
+++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt
@@ -17,7 +17,7 @@
@file:Suppress("MagicNumber")
-package com.geeksville.mesh.ui.map
+package org.meshtastic.feature.map
import android.app.Activity
import android.content.Intent
@@ -67,13 +67,6 @@ import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.MeshProtos.Position
import com.geeksville.mesh.MeshProtos.Waypoint
import com.geeksville.mesh.copy
-import com.geeksville.mesh.ui.map.components.ClusterItemsListDialog
-import com.geeksville.mesh.ui.map.components.EditWaypointDialog
-import com.geeksville.mesh.ui.map.components.NodeClusterMarkers
-import com.geeksville.mesh.ui.map.components.WaypointMarkers
-import com.geeksville.mesh.ui.metrics.HEADING_DEG
-import com.geeksville.mesh.ui.metrics.formatPositionTime
-import com.geeksville.mesh.ui.node.DEG_D
import com.geeksville.mesh.waypoint
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
@@ -87,7 +80,6 @@ import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.JointType
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
-import com.google.maps.android.clustering.ClusterItem
import com.google.maps.android.compose.ComposeMapColorScheme
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MapEffect
@@ -110,19 +102,23 @@ import org.meshtastic.core.model.util.metersIn
import org.meshtastic.core.model.util.mpsToKmph
import org.meshtastic.core.model.util.mpsToMph
import org.meshtastic.core.model.util.toString
+import org.meshtastic.core.proto.formatPositionTime
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.NodeChip
-import org.meshtastic.feature.map.LastHeardFilter
-import org.meshtastic.feature.map.LayerType
-import org.meshtastic.feature.map.LocationPermissionsHandler
-import org.meshtastic.feature.map.MapViewModel
+import org.meshtastic.feature.map.component.ClusterItemsListDialog
import org.meshtastic.feature.map.component.CustomMapLayersSheet
import org.meshtastic.feature.map.component.CustomTileProviderManagerSheet
+import org.meshtastic.feature.map.component.EditWaypointDialog
import org.meshtastic.feature.map.component.MapControlsOverlay
+import org.meshtastic.feature.map.component.NodeClusterMarkers
+import org.meshtastic.feature.map.component.WaypointMarkers
+import org.meshtastic.feature.map.model.NodeClusterItem
import timber.log.Timber
import java.text.DateFormat
private const val MIN_TRACK_POINT_DISTANCE_METERS = 20f
+private const val DEG_D = 1e-7
+private const val HEADING_DEG = 1e-5
@Suppress("CyclomaticComplexMethod", "LongMethod")
@OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -651,34 +647,6 @@ fun Uri.getFileName(context: android.content.Context): String {
return name
}
-data class NodeClusterItem(val node: Node, val nodePosition: LatLng, val nodeTitle: String, val nodeSnippet: String) :
- ClusterItem {
- override fun getPosition(): LatLng = nodePosition
-
- override fun getTitle(): String = nodeTitle
-
- override fun getSnippet(): String = nodeSnippet
-
- override fun getZIndex(): Float? = null
-
- fun getPrecisionMeters(): Double? {
- val precisionMap =
- mapOf(
- 10 to 23345.484932,
- 11 to 11672.7369,
- 12 to 5836.36288,
- 13 to 2918.175876,
- 14 to 1459.0823719999053,
- 15 to 729.53562,
- 16 to 364.7622,
- 17 to 182.375556,
- 18 to 91.182212,
- 19 to 45.58554,
- )
- return precisionMap[this.node.position.precisionBits]
- }
-}
-
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Suppress("LongMethod")
@@ -698,15 +666,9 @@ private fun PositionInfoWindowContent(
Card {
Column(modifier = Modifier.padding(8.dp)) {
- PositionRow(
- label = stringResource(R.string.latitude),
- value = "%.5f".format(position.latitudeI * com.geeksville.mesh.ui.metrics.DEG_D),
- )
+ PositionRow(label = stringResource(R.string.latitude), value = "%.5f".format(position.latitudeI * DEG_D))
- PositionRow(
- label = stringResource(R.string.longitude),
- value = "%.5f".format(position.longitudeI * com.geeksville.mesh.ui.metrics.DEG_D),
- )
+ PositionRow(label = stringResource(R.string.longitude), value = "%.5f".format(position.longitudeI * DEG_D))
PositionRow(label = stringResource(R.string.sats), value = position.satsInView.toString())
@@ -722,7 +684,7 @@ private fun PositionInfoWindowContent(
value = "%.0f°".format(position.groundTrack * HEADING_DEG),
)
- PositionRow(label = stringResource(R.string.timestamp), value = formatPositionTime(position, dateFormat))
+ PositionRow(label = stringResource(R.string.timestamp), value = position.formatPositionTime(dateFormat))
}
}
}
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/ClusterItemsListDialog.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/ClusterItemsListDialog.kt
similarity index 96%
rename from app/src/google/java/com/geeksville/mesh/ui/map/components/ClusterItemsListDialog.kt
rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/component/ClusterItemsListDialog.kt
index 6b87b1ed2..576e01594 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/components/ClusterItemsListDialog.kt
+++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/ClusterItemsListDialog.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.map.components
+package org.meshtastic.feature.map.component
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
@@ -31,9 +31,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import com.geeksville.mesh.ui.map.NodeClusterItem
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.NodeChip
+import org.meshtastic.feature.map.model.NodeClusterItem
@Composable
fun ClusterItemsListDialog(
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
similarity index 99%
rename from app/src/google/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt
rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
index a05d34e4c..74aa4612f 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt
+++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.map.components
+package org.meshtastic.feature.map.component
import android.app.DatePickerDialog
import android.app.TimePickerDialog
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/NodeClusterMarkers.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt
similarity index 96%
rename from app/src/google/java/com/geeksville/mesh/ui/map/components/NodeClusterMarkers.kt
rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt
index f9eb7136f..2640a204e 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/components/NodeClusterMarkers.kt
+++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt
@@ -15,12 +15,11 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.map.components
+package org.meshtastic.feature.map.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.graphics.Color
-import com.geeksville.mesh.ui.map.NodeClusterItem
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.view.DefaultClusterRenderer
import com.google.maps.android.compose.Circle
@@ -28,6 +27,7 @@ import com.google.maps.android.compose.MapsComposeExperimentalApi
import com.google.maps.android.compose.clustering.Clustering
import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.feature.map.BaseMapViewModel
+import org.meshtastic.feature.map.model.NodeClusterItem
@OptIn(MapsComposeExperimentalApi::class)
@Suppress("NestedBlockDepth")
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/WaypointMarkers.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt
similarity index 96%
rename from app/src/google/java/com/geeksville/mesh/ui/map/components/WaypointMarkers.kt
rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt
index 24126075c..669efa9ac 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/components/WaypointMarkers.kt
+++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt
@@ -15,13 +15,12 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.map.components
+package org.meshtastic.feature.map.component
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.ui.node.DEG_D
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.Marker
@@ -29,6 +28,8 @@ import com.google.maps.android.compose.rememberUpdatedMarkerState
import org.meshtastic.core.strings.R
import org.meshtastic.feature.map.BaseMapViewModel
+private const val DEG_D = 1e-7
+
@Composable
fun WaypointMarkers(
displayableWaypoints: List,
diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt
new file mode 100644
index 000000000..c1f171ad6
--- /dev/null
+++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt
@@ -0,0 +1,50 @@
+/*
+ * 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 .
+ */
+
+package org.meshtastic.feature.map.model
+
+import com.google.android.gms.maps.model.LatLng
+import com.google.maps.android.clustering.ClusterItem
+import org.meshtastic.core.database.model.Node
+
+data class NodeClusterItem(val node: Node, val nodePosition: LatLng, val nodeTitle: String, val nodeSnippet: String) :
+ ClusterItem {
+ override fun getPosition(): LatLng = nodePosition
+
+ override fun getTitle(): String = nodeTitle
+
+ override fun getSnippet(): String = nodeSnippet
+
+ override fun getZIndex(): Float? = null
+
+ fun getPrecisionMeters(): Double? {
+ val precisionMap =
+ mapOf(
+ 10 to 23345.484932,
+ 11 to 11672.7369,
+ 12 to 5836.36288,
+ 13 to 2918.175876,
+ 14 to 1459.0823719999053,
+ 15 to 729.53562,
+ 16 to 364.7622,
+ 17 to 182.375556,
+ 18 to 91.182212,
+ 19 to 45.58554,
+ )
+ return precisionMap[this.node.position.precisionBits]
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/MapScreen.kt
similarity index 96%
rename from app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt
rename to feature/map/src/main/kotlin/org/meshtastic/feature/map/MapScreen.kt
index 3706f010a..fa95ccc7d 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt
+++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/MapScreen.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.map
+package org.meshtastic.feature.map
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
@@ -28,7 +28,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
-import org.meshtastic.feature.map.MapViewModel
@Composable
fun MapScreen(
diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/NodeMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt
similarity index 96%
rename from app/src/main/java/com/geeksville/mesh/ui/map/NodeMapViewModel.kt
rename to feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt
index 0491f938d..3b29d6257 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/map/NodeMapViewModel.kt
+++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.map
+package org.meshtastic.feature.map.node
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel