kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
commit
80a2f2601c
|
@ -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
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<DataPacket>(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<MessageStatus>(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
|
||||
|
|
|
@ -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<List<Packet>> = withContext(Dispatchers.IO) {
|
||||
suspend fun getAllPackets(): Flow<List<Packet>> = 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<Long>) = withContext(Dispatchers.IO) {
|
||||
packetDao.deleteMessages(uuidList)
|
||||
}
|
||||
suspend fun delete(packet: Packet) = withContext(Dispatchers.IO) {
|
||||
packetDao.delete(packet)
|
||||
}
|
||||
|
|
|
@ -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<List<Packet>>
|
||||
|
||||
@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<Long>)
|
||||
|
||||
@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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
}
|
||||
|
|
|
@ -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<DataPacket>().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<List<DataPacket>>(messagesList) {
|
||||
|
||||
}
|
||||
|
||||
private var contactsList = emptyMap<String?, DataPacket>().toMutableMap()
|
||||
val contacts = object : MutableLiveData<MutableMap<String?, DataPacket>>() {
|
||||
|
||||
}
|
||||
|
||||
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<DataPacket>) {
|
||||
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<DataPacket>) {
|
||||
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<DataPacket>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> = MutableStateFlow(DataPacket.ID_BROADCAST)
|
||||
fun setContactKey(contact: String) {
|
||||
contactKey.value = contact
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val messages: LiveData<List<Packet>> = contactKey.flatMapLatest { contactKey ->
|
||||
packetRepository.getMessagesFrom(contactKey)
|
||||
}.asLiveData()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val contacts: LiveData<Map<String, Packet>> = _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<Map<Int?, Packet>> = _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<Long>) = 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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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<DataPacket>()
|
||||
var contacts = arrayOf<Packet>()
|
||||
var selectedList = ArrayList<String>()
|
||||
|
||||
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<DataPacket>) {
|
||||
contacts = contactsIn.sortedByDescending { it.time }.toTypedArray()
|
||||
fun onContactsChanged(contacts: Collection<Packet>) {
|
||||
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<DataPacket>()
|
||||
val deleteList = ArrayList<Packet>()
|
||||
// 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()
|
||||
|
|
|
@ -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<DataPacket>()
|
||||
var selectedList = ArrayList<DataPacket>()
|
||||
var messages = listOf<Packet>()
|
||||
var selectedList = ArrayList<Packet>()
|
||||
|
||||
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<DataPacket>) {
|
||||
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<Packet>) {
|
||||
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)
|
||||
|
|
|
@ -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())
|
||||
|
|
Ładowanie…
Reference in New Issue