sforkowany z mirror/meshtastic-android
feat: waypoints
rodzic
a9784f3747
commit
62420132f1
|
@ -61,6 +61,9 @@ interface IMeshService {
|
|||
/// Return my unique user ID string
|
||||
String getMyId();
|
||||
|
||||
/// Return a unique packet ID
|
||||
int getPacketId();
|
||||
|
||||
/*
|
||||
Send a packet to a specified node name
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="16dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/waypointName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:hint="@string/name"
|
||||
android:inputType="textShortMessage"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/waypointDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:hint="@string/description"
|
||||
android:inputType="textMultiLine"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageLock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/sl_lock_24dp" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/waypointLocked"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/locked"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageLock"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -18,8 +18,10 @@
|
|||
<string name="connection_status">Connection status</string>
|
||||
<string name="application_icon">application icon</string>
|
||||
<string name="unknown_username">Unknown Username</string>
|
||||
<string name="send">Send</string>
|
||||
<string name="send_text">Send Text</string>
|
||||
<string name="warning_not_paired">You haven\'t yet paired a Meshtastic compatible radio with this phone. Please pair a device and set your username.\n\nThis open-source application is in development, if you find problems please post on our forum: meshtastic.discourse.group.\n\nFor more information see our web page - www.meshtastic.org.</string>
|
||||
<string name="you">You</string>
|
||||
<string name="your_name">Your Name</string>
|
||||
<string name="analytics_okay">Anonymous usage statistics and crash reports.</string>
|
||||
<string name="looking_for_meshtastic_devices">Looking for Meshtastic devices…</string>
|
||||
|
@ -114,6 +116,8 @@
|
|||
<string name="map_style_selection">Style Selection</string>
|
||||
<string name="map_download_region">Download Region</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="description">Description</string>
|
||||
<string name="locked">Locked</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="preferences_language">Language</string>
|
||||
<string name="preferences_system_default">System default</string>
|
||||
|
|
Ładowanie…
Reference in New Issue