diff --git a/app/src/main/java/com/geeksville/mesh/DataPacket.kt b/app/src/main/java/com/geeksville/mesh/DataPacket.kt index c67f42b7e..55b996ea5 100644 --- a/app/src/main/java/com/geeksville/mesh/DataPacket.kt +++ b/app/src/main/java/com/geeksville/mesh/DataPacket.kt @@ -41,9 +41,11 @@ data class DataPacket( /** * Syntactic sugar to make it easy to create text messages */ - constructor(to: String? = ID_BROADCAST, text: String) : this( - to, text.toByteArray(utf8), - Portnums.PortNum.TEXT_MESSAGE_APP_VALUE + constructor(to: String?, channel: Int, text: String) : this( + to = to, + bytes = text.toByteArray(utf8), + dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE, + channel = channel ) /** diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 0e1fe1fc2..819f37b3b 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -403,7 +403,6 @@ class MainActivity : BaseActivity(), Logging { val filter = IntentFilter() filter.addAction(MeshService.ACTION_MESH_CONNECTED) filter.addAction(MeshService.ACTION_NODE_CHANGE) - filter.addAction(MeshService.actionReceived(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)) filter.addAction((MeshService.ACTION_MESSAGE_STATUS)) registerReceiver(meshServiceReceiver, filter) receiverRegistered = true @@ -588,22 +587,6 @@ class MainActivity : BaseActivity(), Logging { } } - MeshService.actionReceived(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) -> { - debug("received new message from service") - val payload = - intent.getParcelableExtra(EXTRA_PAYLOAD)!! - - model.messagesState.addMessage(payload) - } - - MeshService.ACTION_MESSAGE_STATUS -> { - debug("received message status from service") - val id = intent.getIntExtra(EXTRA_PACKET_ID, 0) - val status = intent.getParcelableExtra(EXTRA_STATUS)!! - - model.messagesState.updateStatus(id, status) - } - MeshService.ACTION_MESH_CONNECTED -> { val extra = intent.getStringExtra(EXTRA_CONNECTED) if (extra != null) { @@ -672,15 +655,8 @@ class MainActivity : BaseActivity(), Logging { // We don't start listening for packets until after we are connected to the service registerMeshReceiver() - // Init our messages table with the service's record of past text messages (ignore all other message types) - val allMsgs = service.oldMessages - val msgs = - allMsgs.filter { p -> p.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE } - model.setMyNodeInfo(service.myNodeInfo) // Note: this could be NULL! - debug("Service provided ${msgs.size} messages and myNodeNum ${model.myNodeInfo.value?.myNodeNum}") - model.messagesState.setMessages(msgs) val connectionState = MeshService.ConnectionState.valueOf(service.connectionState()) @@ -841,7 +817,7 @@ class MainActivity : BaseActivity(), Logging { debug("Sending ping") val str = "Ping " + DateFormat.getTimeInstance(DateFormat.MEDIUM) .format(Date(System.currentTimeMillis())) - model.messagesState.sendMessage(str) + model.sendMessage(str) handler.postDelayed({ postPing() }, 30000) } item.isChecked = !item.isChecked // toggle ping test diff --git a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt index af56e3879..56eea3ee8 100644 --- a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt @@ -1,5 +1,7 @@ package com.geeksville.mesh.database +import com.geeksville.mesh.DataPacket +import com.geeksville.mesh.MessageStatus import com.geeksville.mesh.database.dao.PacketDao import com.geeksville.mesh.database.entity.Packet import kotlinx.coroutines.Dispatchers @@ -12,7 +14,7 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz packetDaoLazy.get() } - suspend fun getAll(): Flow> = withContext(Dispatchers.IO) { + suspend fun getAllPackets(): Flow> = withContext(Dispatchers.IO) { packetDao.getAllPackets() } @@ -20,10 +22,21 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz packetDao.insert(packet) } - suspend fun deleteAll() = withContext(Dispatchers.IO) { - packetDao.deleteAll() + suspend fun getMessagesFrom(contact: String) = withContext(Dispatchers.IO) { + packetDao.getMessagesFrom(contact) } + suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) = withContext(Dispatchers.IO) { + packetDao.updateMessageStatus(d, m) + } + + suspend fun deleteAllMessages() = withContext(Dispatchers.IO) { + packetDao.deleteAllMessages() + } + + suspend fun deleteMessages(uuidList: List) = withContext(Dispatchers.IO) { + packetDao.deleteMessages(uuidList) + } suspend fun delete(packet: Packet) = withContext(Dispatchers.IO) { packetDao.delete(packet) } 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 ebb81c4f7..a4bb580ca 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 @@ -5,6 +5,8 @@ import androidx.room.Insert import androidx.room.Update import androidx.room.Query import androidx.room.Transaction +import com.geeksville.mesh.DataPacket +import com.geeksville.mesh.MessageStatus import com.geeksville.mesh.database.entity.Packet import kotlinx.coroutines.flow.Flow @@ -17,8 +19,17 @@ interface PacketDao { @Insert fun insert(packet: Packet) - @Query("Delete from packet") - fun deleteAll() + @Query("Select * from packet where port_num = 1 and contact_key = :contact order by received_time asc") + fun getMessagesFrom(contact: String): Flow> + + @Query("Select * from packet where data = :data") + fun findDataPacket(data: DataPacket): Packet + + @Query("Delete from packet where port_num = 1") + fun deleteAllMessages() + + @Query("Delete from packet where uuid in (:uuidList)") + fun deleteMessages(uuidList: List) @Query("Delete from packet where uuid=:uuid") fun _delete(uuid: Long) @@ -31,4 +42,9 @@ interface PacketDao { @Update fun update(packet: Packet) + @Transaction + fun updateMessageStatus(data: DataPacket, m: MessageStatus) { + val new = data.copy(status = m) + update(findDataPacket(data).copy(data = new)) + } } diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt index b9d0ed8e4..c9d1b995a 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt @@ -4,16 +4,13 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import com.geeksville.mesh.DataPacket -import com.geeksville.mesh.MessageStatus @Entity(tableName = "packet") data class Packet( @PrimaryKey(autoGenerate = true) val uuid: Long, @ColumnInfo(name = "port_num") val port_num: Int, - @ColumnInfo(name = "contact_id") val contact_id: String?, - @ColumnInfo(name = "channel") val channel: Int, - @ColumnInfo(name = "status") val status: MessageStatus = MessageStatus.UNKNOWN, + @ColumnInfo(name = "contact_key") val contact_key: String, @ColumnInfo(name = "received_time") val received_time: Long, - @ColumnInfo(name = "packet") val packet: DataPacket + @ColumnInfo(name = "data") val data: DataPacket ) { } diff --git a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt deleted file mode 100644 index 4343addee..000000000 --- a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.geeksville.mesh.model - -import android.os.RemoteException -import androidx.lifecycle.MutableLiveData -import com.geeksville.mesh.android.Logging -import com.geeksville.mesh.DataPacket -import com.geeksville.mesh.MessageStatus - - -class MessagesState(private val ui: UIViewModel) : Logging { - /* We now provide fake messages a via MockInterface - private val testTexts = listOf( - DataPacket( - "+16508765310", - "I found the cache" - ), - DataPacket( - "+16508765311", - "Help! I've fallen and I can't get up." - ) - ) */ - - /// This is the inner storage for messages - private val messagesList = emptyList().toMutableList() - - // If the following (unused otherwise) line is commented out, the IDE preview window works. - // if left in the preview always renders as empty. - val messages = - object : MutableLiveData>(messagesList) { - - } - - private var contactsList = emptyMap().toMutableMap() - val contacts = object : MutableLiveData>() { - - } - - private fun emptyDataPacket(to: String? = DataPacket.ID_BROADCAST): DataPacket { - return DataPacket(to, null, 1, DataPacket.ID_LOCAL, 0L) - } - - // Map each contactId to last DataPacket message sent or received - // Broadcast: it.to == DataPacket.ID_BROADCAST; Direct Messages: it.to != DataPacket.ID_BROADCAST - private fun buildContacts() { - contactsList = messagesList.associateBy { - if (it.from == DataPacket.ID_LOCAL || it.to == DataPacket.ID_BROADCAST) - it.to else it.from - }.toMutableMap() - - val all = DataPacket.ID_BROADCAST // always show contacts, even when empty - if (contactsList[all] == null) - contactsList[all] = emptyDataPacket() - - contacts.value = contactsList - } - - fun setMessages(m: List) { - messagesList.clear() - messagesList.addAll(m) - messages.value = messagesList - buildContacts() - } - - /// add a message our GUI list of past msgs - fun addMessage(m: DataPacket) { - debug("Adding message to view id=${m.id}") - // FIXME - don't just slam in a new list each time, it probably causes extra drawing. - - messagesList.add(m) - - messages.value = messagesList - buildContacts() - } - - private fun removeMessages(deleteList: List) { - debug("Removing ${deleteList.size} messages from view") - - messagesList.removeAll(deleteList) - messages.value = messagesList - buildContacts() - } - - private fun removeAllMessages() { - debug("Removing all messages") - - messagesList.clear() - messages.value = messagesList - buildContacts() - } - - fun updateStatus(id: Int, status: MessageStatus) { - // Super inefficent but this is rare - debug("Handling message status change $id: $status") - - messagesList.find { it.id == id }?.let { p -> - // Note: it seems that the service is keeping only a reference to our original packet (so it has already updated p.status) - // This seems to be an AIDL optimization when both the service and the client are in the same process. But we still want to trigger - // a GUI update - // if (p.status != status) { - p.status = status - // Trigger an expensive complete redraw FIXME - messages.value = messagesList - // } - } - } - - /// Send a message and added it to our GUI log - fun sendMessage(str: String, dest: String = DataPacket.ID_BROADCAST) { - - val service = ui.meshService - val p = DataPacket(dest, str) - - if (service != null) - try { - service.send(p) - } catch (ex: RemoteException) { - p.errorMessage = "Error: ${ex.message}" - } - else - p.errorMessage = "Error: No Mesh service" - - // FIXME - why is the first time we are called p is already in the list at this point? - addMessage(p) - } - - fun deleteMessages(deleteList: List) { - val service = ui.meshService - - if (service != null) { - try { - service.deleteMessages(deleteList) - } catch (ex: RemoteException) { - debug("Error: ${ex.message}") - } - } else { - debug("Error: No Mesh service") - } - removeMessages(deleteList) - } - - fun deleteAllMessages() { - val service = ui.meshService - if (service != null) { - try { - service.deleteAllMessages() - } catch (ex: RemoteException) { - errormsg("Error: ${ex.message}") - } - removeAllMessages() - } - } -} 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 2d6b279c5..be1950870 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -10,6 +10,7 @@ import androidx.core.content.edit import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.geeksville.mesh.android.Logging import com.geeksville.mesh.* @@ -28,9 +29,12 @@ import com.geeksville.mesh.util.GPSFormat import com.geeksville.mesh.util.positionToMeter import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.BufferedWriter @@ -98,8 +102,8 @@ class UIViewModel @Inject constructor( } } viewModelScope.launch { - packetRepository.getAll().collect { meshPackets -> - _packets.value = meshPackets + packetRepository.getAllPackets().collect { packets -> + _packets.value = packets } } viewModelScope.launch { @@ -120,10 +124,53 @@ class UIViewModel @Inject constructor( debug("ViewModel created") } + private val contactKey: MutableStateFlow = MutableStateFlow(DataPacket.ID_BROADCAST) + fun setContactKey(contact: String) { + contactKey.value = contact + } + + @OptIn(ExperimentalCoroutinesApi::class) + val messages: LiveData> = contactKey.flatMapLatest { contactKey -> + packetRepository.getMessagesFrom(contactKey) + }.asLiveData() + + @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 } + }.asLiveData() + + @OptIn(ExperimentalCoroutinesApi::class) + val waypoints: LiveData> = _packets.mapLatest { list -> + list.associateBy { packet -> packet.data.waypoint?.id } + .filter { it.value.port_num == Portnums.PortNum.WAYPOINT_APP_VALUE } + }.asLiveData() + + fun sendMessage(str: String, channel: Int = 0, dest: String = DataPacket.ID_BROADCAST) { + val p = DataPacket(dest, channel, str) + sendDataPacket(p) + } + + fun sendDataPacket(p: DataPacket) { + try { + meshService?.send(p) + } catch (ex: RemoteException) { + errormsg("Send DataPacket error: ${ex.message}") + } + } + fun deleteAllLogs() = viewModelScope.launch(Dispatchers.IO) { meshLogRepository.deleteAll() } + fun deleteAllMessages() = viewModelScope.launch(Dispatchers.IO) { + packetRepository.deleteAllMessages() + } + + fun deleteMessages(uuidList: List) = viewModelScope.launch(Dispatchers.IO) { + packetRepository.deleteMessages(uuidList) + } + companion object { fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE) @@ -134,7 +181,6 @@ class UIViewModel @Inject constructor( var meshService: IMeshService? = null val nodeDB = NodeDB(this) - val messagesState = MessagesState(this) /// Connection state to our radio device private val _connectionState = MutableLiveData(MeshService.ConnectionState.DISCONNECTED) 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 bc5c27f14..78b470770 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -525,12 +525,14 @@ class MeshService : Service(), Logging { wantAck: Boolean = false, id: Int = generatePacketId(), // always assign a packet ID if we didn't already have one hopLimit: Int = 0, + channel: Int = 0, priority: MeshPacket.Priority = MeshPacket.Priority.UNSET, initFn: MeshProtos.Data.Builder.() -> Unit ): MeshPacket { this.wantAck = wantAck this.id = id this.hopLimit = hopLimit + this.channel = channel this.priority = priority decoded = MeshProtos.Data.newBuilder().also { initFn(it) @@ -609,7 +611,8 @@ class MeshService : Service(), Logging { return newMeshPacketTo(p.to!!).buildMeshPacket( id = p.id, wantAck = true, - hopLimit = p.hopLimit + hopLimit = p.hopLimit, + channel = p.channel, ) { portnumValue = p.dataType payload = ByteString.copyFrom(p.bytes) @@ -622,12 +625,17 @@ class MeshService : Service(), Logging { if (dataPacket.dataType == Portnums.PortNum.WAYPOINT_APP_VALUE || dataPacket.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE ) { + val fromLocal = dataPacket.from == DataPacket.ID_LOCAL + val toBroadcast = dataPacket.to == DataPacket.ID_BROADCAST + val contactId = if (fromLocal || toBroadcast) dataPacket.to else dataPacket.from + + // contactKey: unique contact key filter (channel)+(nodeId) + val contactKey = "${dataPacket.channel}$contactId" + val packetToSave = Packet( 0L, // autoGenerated dataPacket.dataType, - if (dataPacket.from == DataPacket.ID_LOCAL || dataPacket.to == DataPacket.ID_BROADCAST) dataPacket.to else dataPacket.from, - dataPacket.channel, - MessageStatus.RECEIVED, + contactKey, System.currentTimeMillis(), dataPacket ) @@ -884,7 +892,7 @@ class MeshService : Service(), Logging { offlineSentPackets.forEach { p -> // encapsulate our payload in the proper protobufs and fire it off sendNow(p) - serviceBroadcasts.broadcastMessageStatus(p) + changeStatus(p, MessageStatus.ENROUTE) } offlineSentPackets.clear() } @@ -893,8 +901,9 @@ class MeshService : Service(), Logging { * Change the status on a data packet and update watchers */ private fun changeStatus(p: DataPacket, m: MessageStatus) { - p.status = m - serviceBroadcasts.broadcastMessageStatus(p) + serviceScope.handledLaunch { + packetRepository.get().updateMessageStatus(p, m) + } } /** diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt index 05ed747be..4cbc58f8f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt @@ -2,7 +2,6 @@ package com.geeksville.mesh.service import android.content.Context import android.content.Intent -import android.os.Parcelable import com.geeksville.mesh.DataPacket import com.geeksville.mesh.NodeInfo @@ -38,20 +37,6 @@ class MeshServiceBroadcasts( explicitBroadcast(intent) } - fun broadcastMessageStatus(p: DataPacket) { - if (p.id == 0) { - MeshService.debug("Ignoring anonymous packet status") - } else { - // Do not log, contains PII possibly - // MeshService.debug("Broadcasting message status $p") - val intent = Intent(MeshService.ACTION_MESSAGE_STATUS).apply { - putExtra(EXTRA_PACKET_ID, p.id) - putExtra(EXTRA_STATUS, p.status as Parcelable) - } - explicitBroadcast(intent) - } - } - /** * Broadcast our current connection status */ diff --git a/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt index 6883358f5..894ef00c2 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt @@ -16,8 +16,10 @@ import androidx.recyclerview.widget.RecyclerView import com.geeksville.mesh.android.Logging import com.geeksville.mesh.DataPacket import com.geeksville.mesh.R +import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.databinding.AdapterContactLayoutBinding import com.geeksville.mesh.databinding.FragmentContactsBinding +import com.geeksville.mesh.model.Channel import com.geeksville.mesh.model.UIViewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @@ -71,36 +73,36 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { return ViewHolder(contactsView) } - var contacts = arrayOf() + var contacts = arrayOf() var selectedList = ArrayList() override fun getItemCount(): Int = contacts.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val contact = contacts[position] + val packet = contacts[position] + val contact = packet.data // Determine if this is my message (originated on this device) - val isLocal = contact.from == DataPacket.ID_LOCAL - val isBroadcast = contact.to == DataPacket.ID_BROADCAST - val contactId = if (isLocal || isBroadcast) contact.to else contact.from + val fromLocal = contact.from == DataPacket.ID_LOCAL + val toBroadcast = contact.to == DataPacket.ID_BROADCAST // grab usernames from NodeInfo val nodes = model.nodeDB.nodes.value!! - val node = nodes[if (isLocal) contact.to else contact.from] + val node = nodes[if (fromLocal) contact.to else contact.from] //grab channel names from DeviceConfig - val channels = model.channels.value - val primaryChannel = channels.primaryChannel + val channels = model.channels.value.protobuf + val channelName = if (channels.settingsCount > contact.channel) + Channel(channels.settingsList[contact.channel], channels.loraConfig).name else null val shortName = node?.user?.shortName ?: "???" - val longName = - if (isBroadcast) primaryChannel?.name ?: getString(R.string.channel_name) - else node?.user?.longName ?: getString(R.string.unknown_username) + val longName = if (toBroadcast) channelName ?: getString(R.string.channel_name) + else node?.user?.longName ?: getString(R.string.unknown_username) - holder.shortName.text = if (isBroadcast) "All" else shortName + holder.shortName.text = if (toBroadcast) "${contact.channel}" else shortName holder.longName.text = longName - val text = if (isLocal) contact.text else "$shortName: ${contact.text}" + val text = if (fromLocal) contact.text else "$shortName: ${contact.text}" holder.lastMessageText.text = text if (contact.time != 0L) { @@ -109,7 +111,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { } else holder.lastMessageTime.visibility = View.INVISIBLE holder.itemView.setOnLongClickListener { - clickItem(holder, contactId) + clickItem(holder, packet.contact_key) if (actionMode == null) { actionMode = (activity as AppCompatActivity).startSupportActionMode(actionModeCallback) @@ -117,12 +119,12 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { true } holder.itemView.setOnClickListener { - if (actionMode != null) clickItem(holder, contactId) + if (actionMode != null) clickItem(holder, packet.contact_key) else { - debug("calling MessagesFragment filter:$contactId") + debug("calling MessagesFragment filter:${packet.contact_key}") setFragmentResult( "requestKey", - bundleOf("contactId" to contactId, "contactName" to longName) + bundleOf("contactKey" to packet.contact_key, "contactName" to longName) ) parentFragmentManager.beginTransaction() .replace(R.id.mainActivityLayout, MessagesFragment()) @@ -131,7 +133,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { } } - if (selectedList.contains(contactId)) { + if (selectedList.contains(packet.contact_key)) { holder.itemView.background = GradientDrawable().apply { shape = GradientDrawable.RECTANGLE cornerRadius = 32f @@ -151,12 +153,12 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { } } - private fun clickItem(holder: ViewHolder, contactId: String? = DataPacket.ID_BROADCAST) { + private fun clickItem(holder: ViewHolder, contactKey: String) { val position = holder.bindingAdapterPosition - if (contactId != null && !selectedList.contains(contactId)) { - selectedList.add(contactId) + if (!selectedList.contains(contactKey)) { + selectedList.add(contactKey) } else { - selectedList.remove(contactId) + selectedList.remove(contactKey) } if (selectedList.isEmpty()) { // finish action mode when no items selected @@ -169,13 +171,13 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { } /// Called when our contacts DB changes - fun onContactsChanged(contactsIn: Collection) { - contacts = contactsIn.sortedByDescending { it.time }.toTypedArray() + fun onContactsChanged(contacts: Collection) { + this.contacts = contacts.sortedByDescending { it.received_time }.toTypedArray() notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes } fun onChannelsChanged() { - val oldBroadcast = contacts.find { it.to == DataPacket.ID_BROADCAST } + val oldBroadcast = contacts.find { it.contact_key == DataPacket.ID_BROADCAST } if (oldBroadcast != null) { notifyItemChanged(contacts.indexOf(oldBroadcast)) } @@ -209,9 +211,23 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { contactsAdapter.notifyDataSetChanged() } - model.messagesState.contacts.observe(viewLifecycleOwner) { + model.contacts.observe(viewLifecycleOwner) { debug("New contacts received: ${it.size}") - contactsAdapter.onContactsChanged(it.values) + fun emptyDataPacket(channel: Int = 0): DataPacket { + return DataPacket(bytes = null, dataType = 1, time = 0L, channel = channel) + } + + fun emptyPacket(contactKey: String, channel: Int = 0): Packet { + return Packet(0L, 1, contactKey, 0L, emptyDataPacket(channel)) + } + + // Add empty channel placeholders + val mutableContacts = it.toMutableMap() + val all = DataPacket.ID_BROADCAST // always show Broadcast contacts, even when empty + for (ch in 0 until model.channels.value.protobuf.settingsCount) + if (it["$ch$all"] == null) mutableContacts["$ch$all"] = emptyPacket("$ch$all", ch) + + contactsAdapter.onContactsChanged(mutableContacts.values) } } @@ -230,15 +246,12 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { when (item.itemId) { R.id.deleteButton -> { - val messagesTotal = model.messagesState.messages.value!! + val messagesTotal = model.packets.value.filter { it.port_num == 1 } val selectedList = contactsAdapter.selectedList - val deleteList = ArrayList() + val deleteList = ArrayList() // find messages for each contactId - selectedList.forEach { contactId -> - deleteList += messagesTotal.filter { - if (contactId == DataPacket.ID_BROADCAST) it.to == DataPacket.ID_BROADCAST - else it.from == contactId && it.to != DataPacket.ID_BROADCAST || it.from == DataPacket.ID_LOCAL && it.to == contactId - } + selectedList.forEach { contact -> + deleteList += messagesTotal.filter { it.contact_key == contact } } val deleteMessagesString = resources.getQuantityString( R.plurals.delete_messages, @@ -251,9 +264,9 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { debug("User clicked deleteButton") // all items selected --> deleteAllMessages() if (deleteList.size == messagesTotal.size) { - model.messagesState.deleteAllMessages() + model.deleteAllMessages() } else { - model.messagesState.deleteMessages(deleteList) + model.deleteMessages(deleteList.map { it.uuid }) } mode.finish() } @@ -270,9 +283,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { // else --> select all contactsAdapter.selectedList.clear() contactsAdapter.contacts.forEach { - if (it.from == DataPacket.ID_LOCAL || it.to == DataPacket.ID_BROADCAST) - contactsAdapter.selectedList.add(it.to!!) - else contactsAdapter.selectedList.add(it.from!!) + contactsAdapter.selectedList.add(it.contact_key) } } actionMode?.title = contactsAdapter.selectedList.size.toString() diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt index 1ce870a7d..34f19452b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt @@ -21,6 +21,7 @@ import com.geeksville.mesh.android.Logging import com.geeksville.mesh.DataPacket import com.geeksville.mesh.MessageStatus import com.geeksville.mesh.R +import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.QuickChatAction import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding import com.geeksville.mesh.databinding.MessagesFragmentBinding @@ -52,7 +53,7 @@ class MessagesFragment : Fragment(), Logging { // This property is only valid between onCreateView and onDestroyView. private val binding get() = _binding!! - private var contactId: String = DataPacket.ID_BROADCAST + private var contactKey: String = DataPacket.ID_BROADCAST private var contactName: String = DataPacket.ID_BROADCAST private val model: UIViewModel by activityViewModels() @@ -106,13 +107,14 @@ class MessagesFragment : Fragment(), Logging { return ViewHolder(contactViewBinding) } - var messages = arrayOf() - var selectedList = ArrayList() + var messages = listOf() + var selectedList = ArrayList() override fun getItemCount(): Int = messages.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val msg = messages[position] + val packet = messages[position] + val msg = packet.data val nodes = model.nodeDB.nodes.value!! val node = nodes[msg.from] // Determine if this is my message (originated on this device) @@ -190,7 +192,7 @@ class MessagesFragment : Fragment(), Logging { if (actionMode != null) clickItem(holder) } - if (selectedList.contains(msg)) { + if (selectedList.contains(packet)) { holder.itemView.background = GradientDrawable().apply { shape = GradientDrawable.RECTANGLE cornerRadius = 32f @@ -223,11 +225,8 @@ class MessagesFragment : Fragment(), Logging { } /// Called when our node DB changes - fun onMessagesChanged(msgIn: Collection) { - messages = msgIn.filter { - if (contactId == DataPacket.ID_BROADCAST) it.to == DataPacket.ID_BROADCAST - else it.from == contactId && it.to != DataPacket.ID_BROADCAST || it.from == DataPacket.ID_LOCAL && it.to == contactId - }.toTypedArray() + fun onMessagesChanged(messages: List) { + this.messages = messages notifyDataSetChanged() // FIXME, this is super expensive and redraws all messages // scroll to the last line @@ -254,17 +253,27 @@ class MessagesFragment : Fragment(), Logging { setFragmentResultListener("requestKey") { _, bundle-> // get the result from bundle - contactId = bundle.getString("contactId").toString() + contactKey = bundle.getString("contactKey").toString() contactName = bundle.getString("contactName").toString() + model.setContactKey(contactKey) binding.messageTitle.text = contactName } + // contactKey: unique contact key filter (channel)+(nodeId) + fun sendMessage(str: String, contactKey: String) { + model.sendMessage( + str, + contactKey[0].digitToInt(), // Channel + contactKey.substring(1) // NodeID + ) + } + binding.sendButton.setOnClickListener { debug("User clicked sendButton") val str = binding.messageInputText.text.toString().trim() if (str.isNotEmpty()) - model.messagesState.sendMessage(str, contactId) + sendMessage(str, contactKey) binding.messageInputText.setText("") // blow away the string the user just entered // requireActivity().hideKeyboard() @@ -274,8 +283,7 @@ class MessagesFragment : Fragment(), Logging { debug("did IME action") val str = binding.messageInputText.text.toString().trim() - if (str.isNotEmpty()) - model.messagesState.sendMessage(str) + if (str.isNotEmpty()) sendMessage(str, contactKey) binding.messageInputText.setText("") // blow away the string the user just entered // requireActivity().hideKeyboard() @@ -286,7 +294,7 @@ class MessagesFragment : Fragment(), Logging { layoutManager.stackFromEnd = true // We want the last rows to always be shown binding.messageListView.layoutManager = layoutManager - model.messagesState.messages.observe(viewLifecycleOwner) { + model.messages.observe(viewLifecycleOwner) { debug("New messages received: ${it.size}") messagesAdapter.onMessagesChanged(it) } @@ -310,7 +318,7 @@ class MessagesFragment : Fragment(), Logging { binding.quickChatLayout.removeAllViews() for (action in actions) { val button = Button(context) - button.setText(action.name) + button.text = action.name button.isEnabled = isConnected if (action.mode == QuickChatAction.Mode.Instant) { button.backgroundTintList = ContextCompat.getColorStateList(requireActivity(), R.color.colorMyMsg) @@ -327,7 +335,7 @@ class MessagesFragment : Fragment(), Logging { binding.messageInputText.setText(newText) binding.messageInputText.setSelection(newText.length) } else { - model.messagesState.sendMessage(action.message, contactId) + sendMessage(action.message, contactKey) } } binding.quickChatLayout.addView(button) @@ -361,11 +369,11 @@ class MessagesFragment : Fragment(), Logging { .setPositiveButton(getString(R.string.delete)) { _, _ -> debug("User clicked deleteButton") // all items selected --> deleteAllMessages() - val messagesTotal = model.messagesState.messages.value - if (messagesTotal != null && selectedList.size == messagesTotal.size) { - model.messagesState.deleteAllMessages() + val messagesTotal = model.packets.value.filter { it.port_num == 1 } + if (selectedList.size == messagesTotal.size) { + model.deleteAllMessages() } else { - model.messagesState.deleteMessages(selectedList) + model.deleteMessages(selectedList.map { it.uuid }) } mode.finish() } @@ -391,7 +399,7 @@ class MessagesFragment : Fragment(), Logging { val selectedList = messagesAdapter.selectedList var resendText = "" selectedList.forEach { - resendText = resendText + it.text + System.lineSeparator() + resendText = resendText + it.data.text + System.lineSeparator() } if (resendText!="") resendText = resendText.substring(0, resendText.length - 1) diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt index 40840eb22..2328e7d26 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -166,11 +166,12 @@ class UsersFragment : ScreenFragment("Users"), Logging { } } holder.itemView.setOnLongClickListener { - if (position > 0) { - debug("calling MessagesFragment filter:${n.user?.id}") + val node = n.user + if (position > 0 && node != null) { + debug("calling MessagesFragment filter:${node.id}") setFragmentResult( "requestKey", - bundleOf("contactId" to n.user?.id, "contactName" to name) + bundleOf("contactKey" to "0${node.id}", "contactName" to name) ) parentFragmentManager.beginTransaction() .replace(R.id.mainActivityLayout, MessagesFragment())