Meshtastic-Android/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt

381 lines
15 KiB
Kotlin
Raw Normal View History

2020-04-08 16:49:27 -07:00
package com.geeksville.mesh.ui
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
2020-04-08 16:49:27 -07:00
import android.os.Bundle
import android.text.InputType
import android.view.*
2020-04-08 17:50:23 -07:00
import android.view.inputmethod.EditorInfo
import android.widget.EditText
2020-05-30 14:38:16 -07:00
import android.widget.ImageView
import android.widget.TextView
2022-04-19 15:04:18 -03:00
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
2021-01-25 17:30:21 -08:00
import androidx.cardview.widget.CardView
2021-03-03 08:14:40 +08:00
import androidx.core.content.ContextCompat
2022-04-03 11:25:50 -03:00
import androidx.fragment.app.Fragment
2020-04-08 16:49:27 -07:00
import androidx.fragment.app.activityViewModels
2022-04-03 11:25:50 -03:00
import androidx.fragment.app.setFragmentResultListener
2020-04-08 16:49:27 -07:00
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus
2020-04-08 16:49:27 -07:00
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
import com.geeksville.mesh.databinding.MessagesFragmentBinding
2020-04-08 16:49:27 -07:00
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
2020-05-30 14:38:16 -07:00
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
2020-09-23 23:13:35 -04:00
import java.text.DateFormat
import java.util.*
2020-04-08 16:49:27 -07:00
2020-04-08 17:50:23 -07:00
// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
fun EditText.on(actionId: Int, func: () -> Unit) {
setOnEditorActionListener { _, receivedActionId, _ ->
if (actionId == receivedActionId) {
func()
}
true
}
}
2020-04-08 16:49:27 -07:00
@AndroidEntryPoint
2022-04-03 11:25:50 -03:00
class MessagesFragment : Fragment(), Logging {
2020-04-08 16:49:27 -07:00
2022-04-19 15:04:18 -03:00
private val actionModeCallback: ActionModeCallback = ActionModeCallback()
private var actionMode: ActionMode? = null
private var _binding: MessagesFragmentBinding? = null
2021-01-25 17:30:21 -08:00
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
2022-04-03 11:25:50 -03:00
private var contactId: String = DataPacket.ID_BROADCAST
private var contactName: String = DataPacket.ID_BROADCAST
2020-04-08 16:49:27 -07:00
private val model: UIViewModel by activityViewModels()
2021-11-28 16:14:34 -03:00
// Allows textMultiline with IME_ACTION_SEND
private fun EditText.onActionSend(func: () -> Unit) {
2022-04-07 23:21:12 -03:00
imeOptions = EditorInfo.IME_ACTION_SEND
InputType.TYPE_TEXT_FLAG_MULTI_LINE
setRawInputType(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
2021-11-28 16:14:34 -03:00
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEND) {
func()
}
true
}
}
2020-04-08 16:49:27 -07:00
// Provide a direct reference to each of the views within a data item
// Used to cache the views within the item layout for fast access
2021-01-25 17:30:21 -08:00
class ViewHolder(itemView: AdapterMessageLayoutBinding) :
RecyclerView.ViewHolder(itemView.root) {
2020-05-30 14:38:16 -07:00
val username: Chip = itemView.username
val messageText: TextView = itemView.messageText
val messageTime: TextView = itemView.messageTime
val messageStatusIcon: ImageView = itemView.messageStatusIcon
2021-01-25 17:30:21 -08:00
val card: CardView = itemView.Card
2020-04-08 16:49:27 -07:00
}
private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
2022-04-19 15:04:18 -03:00
private val dateTimeFormat: DateFormat =
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
private val timeFormat: DateFormat =
DateFormat.getTimeInstance(DateFormat.SHORT)
private fun getShortDateTime(time: Date): String {
// return time if within 24 hours, otherwise date/time
val oneDayMsec = 60 * 60 * 24 * 1000L
return if (System.currentTimeMillis() - time.time > oneDayMsec) {
dateTimeFormat.format(time)
} else timeFormat.format(time)
}
2020-04-08 16:49:27 -07:00
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(requireContext())
// Inflate the custom layout
val contactViewBinding = AdapterMessageLayoutBinding.inflate(inflater, parent, false)
2020-04-08 16:49:27 -07:00
// Return a new holder instance
return ViewHolder(contactViewBinding)
2020-04-08 16:49:27 -07:00
}
var messages = arrayOf<DataPacket>()
2022-03-02 11:21:43 -03:00
var selectedList = ArrayList<DataPacket>()
2020-04-08 16:49:27 -07:00
override fun getItemCount(): Int = messages.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
2020-04-08 17:12:39 -07:00
val msg = messages[position]
val nodes = model.nodeDB.nodes.value!!
2022-04-19 15:04:18 -03:00
val node = nodes[msg.from]
2022-04-03 11:25:50 -03:00
// Determine if this is my message (originated on this device)
val isLocal = msg.from == DataPacket.ID_LOCAL
2021-01-25 17:30:21 -08:00
// Set cardview offset and color.
val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams
val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset)
2022-04-03 11:25:50 -03:00
if (isLocal) {
2021-01-25 17:30:21 -08:00
marginParams.leftMargin = messageOffset
marginParams.rightMargin = 0
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_END
2021-03-29 20:33:06 +08:00
context?.let {
holder.card.setCardBackgroundColor(
ContextCompat.getColor(
it,
R.color.colorMyMsg
)
)
}
2021-01-25 17:30:21 -08:00
} else {
marginParams.rightMargin = messageOffset
marginParams.leftMargin = 0
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
2021-03-29 20:33:06 +08:00
context?.let {
holder.card.setCardBackgroundColor(
ContextCompat.getColor(
it,
R.color.colorMsg
)
)
}
2021-01-25 17:30:21 -08:00
}
// Hide the username chip for my messages
2022-04-03 11:25:50 -03:00
if (isLocal) {
2021-01-25 17:30:21 -08:00
holder.username.visibility = View.GONE
} else {
holder.username.visibility = View.VISIBLE
// If we can't find the sender, just use the ID
val user = node?.user
holder.username.text = user?.shortName ?: msg.from
}
2020-04-08 17:12:39 -07:00
if (msg.errorMessage != null) {
context?.let { holder.card.setCardBackgroundColor(Color.RED) }
2020-04-08 17:12:39 -07:00
holder.messageText.text = msg.errorMessage
} else {
holder.messageText.text = msg.text
}
2021-01-25 17:30:21 -08:00
holder.messageTime.text = getShortDateTime(Date(msg.time))
val icon = when (msg.status) {
MessageStatus.QUEUED -> R.drawable.ic_twotone_cloud_upload_24
MessageStatus.DELIVERED -> R.drawable.cloud_on
MessageStatus.ENROUTE -> R.drawable.ic_twotone_cloud_24
MessageStatus.ERROR -> R.drawable.cloud_off
else -> null
}
if (icon != null) {
holder.messageStatusIcon.setImageResource(icon)
holder.messageStatusIcon.visibility = View.VISIBLE
} else
2022-04-19 15:04:18 -03:00
holder.messageStatusIcon.visibility = View.GONE
holder.itemView.setOnLongClickListener {
2022-04-19 15:04:18 -03:00
clickItem(holder)
if (actionMode == null) {
2022-04-19 15:04:18 -03:00
actionMode =
(activity as AppCompatActivity).startSupportActionMode(actionModeCallback)
}
true
}
holder.itemView.setOnClickListener {
if (actionMode != null) clickItem(holder)
}
2022-03-02 11:21:43 -03:00
if (selectedList.contains(msg)) {
holder.itemView.background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = 32f
setColor(Color.rgb(127, 127, 127))
}
} else {
holder.itemView.background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = 32f
setColor(ContextCompat.getColor(holder.itemView.context, R.color.colorAdvancedBackground))
}
}
2020-04-08 16:49:27 -07:00
}
private fun clickItem(holder: ViewHolder) {
val position = holder.bindingAdapterPosition
2022-03-02 11:21:43 -03:00
if (!selectedList.contains(messages[position])) {
selectedList.add(messages[position])
} else {
2022-03-02 11:21:43 -03:00
selectedList.remove(messages[position])
}
2022-03-02 11:21:43 -03:00
if (selectedList.isEmpty()) {
// finish action mode when no items selected
actionMode?.finish()
} else {
// show total items selected on action mode title
2022-04-03 11:25:50 -03:00
actionMode?.title = selectedList.size.toString()
}
notifyItemChanged(position)
}
2020-04-08 16:49:27 -07:00
/// Called when our node DB changes
fun onMessagesChanged(msgIn: Collection<DataPacket>) {
2022-04-19 15:04:18 -03:00
messages = msgIn.filter {
if (contactId == DataPacket.ID_BROADCAST)
it.to == DataPacket.ID_BROADCAST || it.delayed == 1 // MeshPacket.Delayed.DELAYED_BROADCAST_VALUE == 1
else it.from == contactId && it.to != DataPacket.ID_BROADCAST || it.from == DataPacket.ID_LOCAL && it.to == contactId
}.toTypedArray()
2020-04-08 16:49:27 -07:00
notifyDataSetChanged() // FIXME, this is super expensive and redraws all messages
2020-04-08 17:50:23 -07:00
// scroll to the last line
if (itemCount != 0)
binding.messageListView.scrollToPosition(itemCount - 1)
2020-04-08 16:49:27 -07:00
}
}
override fun onPause() {
actionMode?.finish()
super.onPause()
}
2020-04-08 16:49:27 -07:00
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = MessagesFragmentBinding.inflate(inflater, container, false)
return binding.root
2020-04-08 16:49:27 -07:00
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
2022-04-03 11:25:50 -03:00
setFragmentResultListener("requestKey") { _, bundle->
// get the result from bundle
contactId = bundle.getString("contactId").toString()
contactName = bundle.getString("contactName").toString()
binding.messageTitle.text = contactName
}
binding.sendButton.setOnClickListener {
debug("User clicked sendButton")
val str = binding.messageInputText.text.toString().trim()
if (str.isNotEmpty())
2022-04-03 11:25:50 -03:00
model.messagesState.sendMessage(str, contactId)
binding.messageInputText.setText("") // blow away the string the user just entered
// requireActivity().hideKeyboard()
}
2021-11-28 16:14:34 -03:00
binding.messageInputText.onActionSend {
2020-04-08 17:50:23 -07:00
debug("did IME action")
val str = binding.messageInputText.text.toString().trim()
2020-05-15 11:55:32 -07:00
if (str.isNotEmpty())
model.messagesState.sendMessage(str)
binding.messageInputText.setText("") // blow away the string the user just entered
2020-04-09 11:03:17 -07:00
// requireActivity().hideKeyboard()
2020-04-08 17:50:23 -07:00
}
binding.messageListView.adapter = messagesAdapter
2020-04-08 17:50:23 -07:00
val layoutManager = LinearLayoutManager(requireContext())
layoutManager.stackFromEnd = true // We want the last rows to always be shown
binding.messageListView.layoutManager = layoutManager
2020-04-08 16:49:27 -07:00
model.messagesState.messages.observe(viewLifecycleOwner) {
debug("New messages received: ${it.size}")
2020-04-08 16:49:27 -07:00
messagesAdapter.onMessagesChanged(it)
}
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
model.isConnected.observe(viewLifecycleOwner) { connectionState ->
// If we don't know our node ID and we are offline don't let user try to send
val connected = connectionState == MeshService.ConnectionState.CONNECTED
binding.textInputLayout.isEnabled = connected
binding.sendButton.isEnabled = connected
// Just being connected is enough to allow sending texts I think
// && model.nodeDB.myId.value != null && model.radioConfig.value != null
2021-03-02 15:12:57 +08:00
}
2020-04-08 16:49:27 -07:00
}
2022-04-19 15:04:18 -03:00
private inner class ActionModeCallback : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.menu_messages, menu)
mode.title = "1"
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
when (item.itemId) {
R.id.deleteButton -> {
val selectedList = messagesAdapter.selectedList
val deleteMessagesString = resources.getQuantityString(
R.plurals.delete_messages,
selectedList.size,
selectedList.size
)
MaterialAlertDialogBuilder(requireContext())
.setMessage(deleteMessagesString)
.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()
} else {
model.messagesState.deleteMessages(selectedList)
}
mode.finish()
}
.setNeutralButton(R.string.cancel) { _, _ ->
}
.show()
}
R.id.selectAllButton -> {
// if all selected -> unselect all
if (messagesAdapter.selectedList.size == messagesAdapter.messages.size) {
messagesAdapter.selectedList.clear()
mode.finish()
} else {
// else --> select all
messagesAdapter.selectedList.clear()
messagesAdapter.selectedList.addAll(messagesAdapter.messages)
}
actionMode?.title = messagesAdapter.selectedList.size.toString()
messagesAdapter.notifyDataSetChanged()
}
R.id.resendButton -> {
debug("User clicked resendButton")
2022-04-19 16:15:47 -03:00
val selectedList = messagesAdapter.selectedList
2022-04-19 15:04:18 -03:00
var resendText = ""
selectedList.forEach {
resendText = resendText + it.text + System.lineSeparator()
}
if (resendText!="")
resendText = resendText.substring(0, resendText.length - 1)
binding.messageInputText.setText(resendText)
mode.finish()
}
}
return true
}
override fun onDestroyActionMode(mode: ActionMode) {
messagesAdapter.selectedList.clear()
messagesAdapter.notifyDataSetChanged()
actionMode = null
}
}
}