mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-03 21:53:55 -04:00
feat: waypoints
This commit is contained in:
@@ -55,6 +55,13 @@ data class DataPacket(
|
||||
else
|
||||
null
|
||||
|
||||
constructor(to: String?, channel: Int, waypoint: MeshProtos.Waypoint) : this(
|
||||
to = to,
|
||||
bytes = waypoint.toByteArray(),
|
||||
dataType = Portnums.PortNum.WAYPOINT_APP_VALUE,
|
||||
channel = channel
|
||||
)
|
||||
|
||||
val waypoint: MeshProtos.Waypoint?
|
||||
get() = if (dataType == Portnums.PortNum.WAYPOINT_APP_VALUE)
|
||||
MeshProtos.Waypoint.parseFrom(bytes)
|
||||
@@ -149,6 +156,7 @@ data class DataPacket(
|
||||
const val NODENUM_BROADCAST = (0xffffffff).toInt()
|
||||
|
||||
fun nodeNumToDefaultId(n: Int): String = "!%08x".format(n)
|
||||
fun idToDefaultNodeNum(id: String?): Int? = id?.toLong(16)?.toInt()
|
||||
|
||||
override fun createFromParcel(parcel: Parcel): DataPacket {
|
||||
return DataPacket(parcel)
|
||||
|
||||
@@ -45,6 +45,11 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
|
||||
suspend fun deleteMessages(uuidList: List<Long>) = withContext(Dispatchers.IO) {
|
||||
packetDao.deleteMessages(uuidList)
|
||||
}
|
||||
|
||||
suspend fun deleteWaypoint(id: Int) = withContext(Dispatchers.IO) {
|
||||
packetDao.deleteWaypoint(id)
|
||||
}
|
||||
|
||||
suspend fun delete(packet: Packet) = withContext(Dispatchers.IO) {
|
||||
packetDao.delete(packet)
|
||||
}
|
||||
|
||||
@@ -59,4 +59,13 @@ interface PacketDao {
|
||||
@Transaction
|
||||
fun getQueuedPackets(): List<DataPacket>? =
|
||||
getDataPackets().filter { it.status in setOf(MessageStatus.ENROUTE, MessageStatus.QUEUED) }
|
||||
|
||||
@Query("Select * from packet where port_num = 8 order by received_time asc")
|
||||
fun getAllWaypoints(): List<Packet>
|
||||
|
||||
@Transaction
|
||||
fun deleteWaypoint(id: Int) {
|
||||
val uuidList = getAllWaypoints().filter { it.data.waypoint?.id == id }.map { it.uuid }
|
||||
deleteMessages(uuidList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,12 +162,34 @@ class UIViewModel @Inject constructor(
|
||||
}
|
||||
}.asLiveData()
|
||||
|
||||
fun generatePacketId(): Int? {
|
||||
return try {
|
||||
meshService?.packetId
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("RemoteException: ${ex.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
|
||||
// contactKey: unique contact key filter (channel)+(nodeId)
|
||||
val channel = contactKey[0].digitToIntOrNull()
|
||||
val dest = if (channel != null) contactKey.substring(1) else contactKey
|
||||
|
||||
val p = DataPacket(dest, channel ?: 0, str)
|
||||
sendDataPacket(p)
|
||||
}
|
||||
|
||||
fun sendWaypoint(wpt: MeshProtos.Waypoint, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
|
||||
// contactKey: unique contact key filter (channel)+(nodeId)
|
||||
val channel = contactKey[0].digitToIntOrNull()
|
||||
val dest = if (channel != null) contactKey.substring(1) else contactKey
|
||||
|
||||
val p = DataPacket(dest, channel ?: 0, wpt)
|
||||
if (wpt.id != 0) sendDataPacket(p.copy(id = wpt.id))
|
||||
}
|
||||
|
||||
private fun sendDataPacket(p: DataPacket) {
|
||||
try {
|
||||
meshService?.send(p)
|
||||
} catch (ex: RemoteException) {
|
||||
@@ -195,6 +217,10 @@ class UIViewModel @Inject constructor(
|
||||
packetRepository.deleteMessages(uuidList)
|
||||
}
|
||||
|
||||
fun deleteWaypoint(id: Int) = viewModelScope.launch(Dispatchers.IO) {
|
||||
packetRepository.deleteWaypoint(id)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getPreferences(context: Context): SharedPreferences =
|
||||
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
|
||||
@@ -210,7 +236,7 @@ class UIViewModel @Inject constructor(
|
||||
private val _connectionState = MutableLiveData(MeshService.ConnectionState.DISCONNECTED)
|
||||
val connectionState: LiveData<MeshService.ConnectionState> get() = _connectionState
|
||||
|
||||
fun isConnected() = _connectionState.value == MeshService.ConnectionState.CONNECTED
|
||||
fun isConnected() = _connectionState.value != MeshService.ConnectionState.DISCONNECTED
|
||||
|
||||
fun setConnectionState(connectionState: MeshService.ConnectionState) {
|
||||
_connectionState.value = connectionState
|
||||
|
||||
@@ -1627,6 +1627,8 @@ class MeshService : Service(), Logging {
|
||||
|
||||
override fun getMyId() = toRemoteExceptions { myNodeID }
|
||||
|
||||
override fun getPacketId() = toRemoteExceptions { generatePacketId() }
|
||||
|
||||
override fun setOwner(myId: String?, longName: String, shortName: String, isLicensed: Boolean) =
|
||||
toRemoteExceptions {
|
||||
this@MeshService.setOwner(myId, longName, shortName, isLicensed)
|
||||
|
||||
@@ -8,11 +8,19 @@ import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.Logging
|
||||
@@ -23,10 +31,13 @@ import com.geeksville.mesh.model.map.CustomOverlayManager
|
||||
import com.geeksville.mesh.model.map.CustomTileSource
|
||||
import com.geeksville.mesh.util.SqlTileWriterExt
|
||||
import com.geeksville.mesh.util.formatAgo
|
||||
import com.geeksville.mesh.waypoint
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.osmdroid.api.IMapController
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.osmdroid.events.MapEventsReceiver
|
||||
import org.osmdroid.events.MapListener
|
||||
import org.osmdroid.events.ScrollEvent
|
||||
import org.osmdroid.events.ZoomEvent
|
||||
@@ -40,8 +51,12 @@ import org.osmdroid.util.BoundingBox
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.CustomZoomButtonsController
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.*
|
||||
import org.osmdroid.views.overlay.CopyrightOverlay
|
||||
import org.osmdroid.views.overlay.MapEventsOverlay
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.osmdroid.views.overlay.Polygon
|
||||
import org.osmdroid.views.overlay.gridlines.LatLonGridlineOverlay2
|
||||
import org.osmdroid.views.overlay.infowindow.InfoWindow
|
||||
import java.io.File
|
||||
import kotlin.math.log2
|
||||
|
||||
@@ -141,6 +156,10 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
|
||||
}
|
||||
}
|
||||
|
||||
private fun performHapticFeedback() = requireView().performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
|
||||
)
|
||||
|
||||
private fun showCacheManagerDialog() {
|
||||
val alertDialogBuilder = AlertDialog.Builder(
|
||||
@@ -249,6 +268,22 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun showMarkerLongPressDialog(id: Int) {
|
||||
debug("marker long pressed id=${id}")
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("${getString(R.string.delete)}?")
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
debug("User canceled marker edit dialog")
|
||||
}
|
||||
// .setNegativeButton(R.string.edit) { _, _ ->
|
||||
// debug("Negative button pressed") // TODO add Edit option
|
||||
// }
|
||||
.setPositiveButton(getString(R.string.delete)) { _, _ ->
|
||||
debug("User deleted local waypoint $id")
|
||||
model.deleteWaypoint(id)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun downloadJobAlert() {
|
||||
//prompt for input params .
|
||||
@@ -423,6 +458,9 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUsername(id: String?) = if (id == DataPacket.ID_LOCAL) getString(R.string.you)
|
||||
else model.nodeDB.nodes.value?.get(id)?.user?.longName ?: getString(R.string.unknown_username)
|
||||
|
||||
private fun onWaypointChanged(wayPt: Collection<Packet>) {
|
||||
|
||||
/**
|
||||
@@ -430,17 +468,19 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
|
||||
*/
|
||||
// Find all waypoints
|
||||
fun getCurrentWayPoints(): List<MarkerWithLabel> {
|
||||
debug("Showing on map: ${wayPt.size} waypoints")
|
||||
val wayPoint = wayPt.map { pt ->
|
||||
debug("Showing on map: $pt")
|
||||
lateinit var marker: MarkerWithLabel
|
||||
pt.data.waypoint?.let {
|
||||
val label = it.name + " " + formatAgo(it.expire)
|
||||
marker = MarkerWithLabel(map, label, String(Character.toChars(it.icon)))
|
||||
marker.title = it.name
|
||||
val lock = if (it.lockedTo != 0) "\uD83D\uDD12" else ""
|
||||
val label = it.name + " " + formatAgo((pt.received_time / 1000).toInt())
|
||||
val emoji = String(Character.toChars(if (it.icon == 0) 128205 else it.icon))
|
||||
marker = MarkerWithLabel(map, label, emoji)
|
||||
marker.id = "${it.id}"
|
||||
marker.title = "${it.name} (${getUsername(pt.data.from)}$lock)"
|
||||
marker.snippet = it.description
|
||||
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
marker.position = GeoPoint(it.latitudeI.toDouble(), it.longitudeI.toDouble())
|
||||
marker.icon = android.graphics.drawable.ColorDrawable(Color.TRANSPARENT)
|
||||
marker.position = GeoPoint(it.latitudeI * 1e-7, it.longitudeI * 1e-7)
|
||||
marker.setVisible(false)
|
||||
}
|
||||
marker
|
||||
}
|
||||
@@ -516,6 +556,46 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
|
||||
createLatLongGrid(false)
|
||||
map.overlayManager.addAll(nodeLayer, nodePositions)
|
||||
map.overlayManager.addAll(nodeLayer, wayPoints)
|
||||
map.overlayManager.add(nodeLayer, MapEventsOverlay(object : MapEventsReceiver {
|
||||
override fun singleTapConfirmedHelper(p: GeoPoint): Boolean {
|
||||
InfoWindow.closeAllInfoWindowsOn(map)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun longPressHelper(p: GeoPoint): Boolean {
|
||||
performHapticFeedback()
|
||||
if (!model.isConnected()) return true
|
||||
|
||||
val layout = LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.dialog_add_waypoint, null)
|
||||
|
||||
val nameInput: EditText = layout.findViewById(R.id.waypointName)
|
||||
val descriptionInput: EditText= layout.findViewById(R.id.waypointDescription)
|
||||
val lockedInput: SwitchMaterial = layout.findViewById(R.id.waypointLocked)
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(layout)
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
debug("User canceled marker create dialog")
|
||||
}
|
||||
.setPositiveButton(getString(R.string.save)) { _, _ ->
|
||||
debug("User created waypoint")
|
||||
model.sendWaypoint(waypoint {
|
||||
name = nameInput.text.toString().ifEmpty { return@setPositiveButton }
|
||||
description = descriptionInput.text.toString()
|
||||
id = model.generatePacketId() ?: return@setPositiveButton
|
||||
latitudeI = (p.latitude * 1e7).toInt()
|
||||
longitudeI = (p.longitude * 1e7).toInt()
|
||||
expire = Int.MAX_VALUE // TODO add expire picker
|
||||
icon = 0 // TODO add emoji picker
|
||||
lockedTo = if (!lockedInput.isChecked) 0
|
||||
else model.myNodeInfo.value?.myNodeNum ?: 0
|
||||
})
|
||||
}
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
}))
|
||||
map.invalidate()
|
||||
}
|
||||
|
||||
@@ -671,6 +751,15 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLongPress(event: MotionEvent?, mapView: MapView?): Boolean {
|
||||
val touched = hitTest(event, mapView)
|
||||
if (touched && this.id != null) {
|
||||
performHapticFeedback()
|
||||
this.id.toIntOrNull()?.run(::showMarkerLongPressDialog)
|
||||
}
|
||||
return super.onLongPress(event, mapView)
|
||||
}
|
||||
|
||||
override fun draw(c: Canvas, osmv: MapView?, shadow: Boolean) {
|
||||
super.draw(c, osmv, false)
|
||||
val p = mPositionPixels
|
||||
|
||||
@@ -26,7 +26,6 @@ import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
||||
import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -298,9 +297,9 @@ class MessagesFragment : Fragment(), Logging {
|
||||
}
|
||||
|
||||
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
|
||||
model.connectionState.observe(viewLifecycleOwner) { connectionState ->
|
||||
model.connectionState.observe(viewLifecycleOwner) {
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
isConnected = connectionState != MeshService.ConnectionState.DISCONNECTED
|
||||
isConnected = model.isConnected()
|
||||
binding.textInputLayout.isEnabled = isConnected
|
||||
binding.sendButton.isEnabled = isConnected
|
||||
for (subView: View in binding.quickChatLayout.allViews) {
|
||||
|
||||
Reference in New Issue
Block a user