diff --git a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt index 85cfabf5..9c166c02 100644 --- a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt +++ b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt @@ -53,7 +53,7 @@ interface PacketDao { @Transaction fun getDataPacketById(requestId: Int): DataPacket? { - return getDataPackets().firstOrNull { it.id == requestId } + return getDataPackets().lastOrNull { it.id == requestId } } @Transaction diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 611c7b63..ab19a0dd 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -161,17 +161,15 @@ class UIViewModel @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) val contacts: LiveData> = _packets.mapLatest { list -> - list.associateBy { packet -> packet.contact_key } - .filter { it.value.port_num == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE } + list.filter { it.port_num == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE } + .associateBy { packet -> packet.contact_key } }.asLiveData() @OptIn(ExperimentalCoroutinesApi::class) - val waypoints: LiveData> = _packets.mapLatest { list -> - list.associateBy { packet -> packet.data.waypoint?.id } - .filterValues { - val expired = (it.data.waypoint?.expire ?: 0) < System.currentTimeMillis() / 1000 - it.port_num == Portnums.PortNum.WAYPOINT_APP_VALUE && !expired - } + val waypoints: LiveData> = _packets.mapLatest { list -> + list.filter { it.port_num == Portnums.PortNum.WAYPOINT_APP_VALUE } + .associateBy { packet -> packet.data.waypoint!!.id } + .filterValues { it.data.waypoint!!.expire > System.currentTimeMillis() / 1000 } }.asLiveData() fun generatePacketId(): Int? { @@ -198,7 +196,7 @@ class UIViewModel @Inject constructor( 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)) + if (wpt.id != 0) sendDataPacket(p) } private fun sendDataPacket(p: DataPacket) { @@ -294,6 +292,7 @@ class UIViewModel @Inject constructor( /// hardware info about our local device (can be null) private val _myNodeInfo = MutableLiveData() val myNodeInfo: LiveData get() = _myNodeInfo + val myNodeNum get() = _myNodeInfo.value?.myNodeNum fun setMyNodeInfo(info: MyNodeInfo?) { _myNodeInfo.value = info @@ -526,7 +525,7 @@ class UIViewModel @Inject constructor( viewModelScope.launch(Dispatchers.Main) { // Extract distances to this device from position messages and put (node,SNR,distance) in // the file_uri - val myNodeNum = myNodeInfo.value?.myNodeNum ?: return@launch + val myNodeNum = myNodeNum ?: return@launch // Capture the current node value while we're still on main thread val nodes = nodeDB.nodes.value ?: emptyMap() diff --git a/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt index 0716dbe9..5cefe6be 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt @@ -20,9 +20,11 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import com.geeksville.mesh.BuildConfig import com.geeksville.mesh.DataPacket +import com.geeksville.mesh.MeshProtos.Waypoint import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.R import com.geeksville.mesh.android.Logging +import com.geeksville.mesh.copy import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.databinding.MapViewBinding import com.geeksville.mesh.model.UIViewModel @@ -57,6 +59,7 @@ 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 java.text.DateFormat import kotlin.math.log2 @@ -76,7 +79,8 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging { private val prefsName = "org.geeksville.osm.prefs" private val mapStyleId = "map_style_id" private var nodePositions = listOf() - private var wayPoints = listOf() + private var waypoints = mapOf() + private var waypointMarkers = listOf() private val nodeLayer = 1 // Distance of bottom corner to top corner of bounding box @@ -133,6 +137,7 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging { } model.waypoints.observe(viewLifecycleOwner) { debug("New waypoints received: ${it.size}") + waypoints = it.mapValues { p -> p.value.data.waypoint } onWaypointChanged(it.values) drawOverlays() } @@ -244,21 +249,88 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging { }.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") + private data class DialogBuilder( + val builder: MaterialAlertDialogBuilder, + val nameInput: EditText, + val descInput: EditText, + val lockedSwitch: SwitchMaterial, + ) { + val name get() = nameInput.text.toString().trim() + val description get() = descInput.text.toString().trim() + } + + private fun createEditDialog(context: Context, title: String): DialogBuilder { + val builder = MaterialAlertDialogBuilder(context) + val layout = LayoutInflater.from(context).inflate(R.layout.dialog_add_waypoint, null) + + val nameInput: EditText = layout.findViewById(R.id.waypointName) + val descInput: EditText= layout.findViewById(R.id.waypointDescription) + val lockedSwitch: SwitchMaterial = layout.findViewById(R.id.waypointLocked) + + builder.setTitle(title) + builder.setView(layout) + + return DialogBuilder(builder, nameInput, descInput, lockedSwitch) + } + + private fun showDeleteMarkerDialog(id: Int) { + val waypoint = waypoints[id] + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setTitle(R.string.waypoint_delete) + builder.setNeutralButton(R.string.cancel) { _, _ -> + debug("User canceled marker delete dialog") + } + builder.setNegativeButton(R.string.delete_for_me) { _, _ -> + debug("User deleted waypoint $id for me") + model.deleteWaypoint(id) + } + if (waypoint != null && waypoint.lockedTo in setOf(0, model.myNodeNum ?: 0)) + builder.setPositiveButton(R.string.delete_for_everyone) { _, _ -> + debug("User deleted waypoint $id for everyone") + model.sendWaypoint(waypoint.copy { expire = 1 }) model.deleteWaypoint(id) } - .show() + val dialog = builder.show() + for (button in setOf( + AlertDialog.BUTTON_NEUTRAL, + AlertDialog.BUTTON_NEGATIVE, + AlertDialog.BUTTON_POSITIVE + )) with(dialog.getButton(button)) { textSize = 12F; isAllCaps = false } + } + + private fun showEditMarkerDialog(waypoint: Waypoint) { + val dialog = createEditDialog(requireContext(), getString(R.string.waypoint_edit)) + dialog.nameInput.setText(waypoint.name) + dialog.descInput.setText(waypoint.description) + dialog.lockedSwitch.isEnabled = false + dialog.lockedSwitch.isChecked = waypoint.lockedTo != 0 + dialog.builder.setNeutralButton(R.string.cancel) { _, _ -> + debug("User canceled marker edit dialog") + } + dialog.builder.setNegativeButton(R.string.delete) { _, _ -> + debug("User clicked delete waypoint ${waypoint.id}") + showDeleteMarkerDialog(waypoint.id) + } + dialog.builder.setPositiveButton(getString(R.string.send)) { _, _ -> + debug("User edited waypoint ${waypoint.id}") + model.sendWaypoint(waypoint.copy { + name = dialog.name.ifEmpty { return@setPositiveButton } + description = dialog.description + expire = Int.MAX_VALUE // TODO add expire picker + icon = 0 // TODO add emoji picker + }) + } + dialog.builder.show() + } + + fun showMarkerLongPressDialog(id: Int) { + debug("marker long pressed id=${id}") + val waypoint = waypoints[id] + // edit only when unlocked or lockedTo myNodeNum + if (waypoint != null && waypoint.lockedTo in setOf(0, model.myNodeNum ?: 0)) + showEditMarkerDialog(waypoint) + else + showDeleteMarkerDialog(id) } private fun downloadJobAlert() { @@ -444,12 +516,14 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging { lateinit var marker: MarkerWithLabel pt.data.waypoint?.let { val lock = if (it.lockedTo != 0) "\uD83D\uDD12" else "" + val time = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + .format(pt.received_time) 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.snippet = "[$time] " + it.description marker.position = GeoPoint(it.latitudeI * 1e-7, it.longitudeI * 1e-7) marker.setVisible(false) } @@ -457,7 +531,7 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging { } return wayPoint } - wayPoints = getCurrentWayPoints() + waypointMarkers = getCurrentWayPoints() } private fun onNodesChanged(nodes: Collection) { @@ -525,7 +599,7 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging { addCopyright() // Copyright is required for certain map sources createLatLongGrid(false) map.overlayManager.addAll(nodeLayer, nodePositions) - map.overlayManager.addAll(nodeLayer, wayPoints) + map.overlayManager.addAll(nodeLayer, waypointMarkers) map.overlayManager.add(nodeLayer, MapEventsOverlay(object : MapEventsReceiver { override fun singleTapConfirmedHelper(p: GeoPoint): Boolean { InfoWindow.closeAllInfoWindowsOn(map) @@ -536,33 +610,24 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging { performHapticFeedback() if (!model.isConnected()) return true - val layout = LayoutInflater.from(context) - .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.send)) { _, _ -> - 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() + val dialog = createEditDialog(requireContext(), getString(R.string.waypoint_new)) + dialog.builder.setNeutralButton(R.string.cancel) { _, _ -> + debug("User canceled marker create dialog") + } + dialog.builder.setPositiveButton(getString(R.string.send)) { _, _ -> + debug("User created waypoint") + model.sendWaypoint(waypoint { + name = dialog.name.ifEmpty { return@setPositiveButton } + description = dialog.description + 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 (!dialog.lockedSwitch.isChecked) 0 else model.myNodeNum ?: 0 + }) + } + dialog.builder.show() return true } })) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 282dddbb..c89bd623 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,6 +111,8 @@ Delete %s messages? Delete + Delete for everyone + Delete for me Select all Long Range / Slow Style Selection @@ -169,4 +171,7 @@ Download complete with %s errors %s tiles bearing: %1$s° distance: %2$s + Edit waypoint + Delete waypoint? + New waypoint