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

266 lines
11 KiB
Kotlin
Raw Normal View History

2020-04-08 16:49:27 -07:00
package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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
2021-01-25 17:30:21 -08:00
import androidx.cardview.widget.CardView
2020-04-08 16:49:27 -07:00
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
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
2021-01-25 17:30:21 -08:00
import com.geeksville.mesh.NodeInfo
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
2020-09-23 23:13:35 -04:00
import java.text.DateFormat
2021-01-25 17:30:21 -08:00
import java.text.ParseException
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
class MessagesFragment : ScreenFragment("Messages"), Logging {
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!!
2020-04-08 16:49:27 -07:00
private val model: UIViewModel by activityViewModels()
2021-01-25 17:30:21 -08: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 one_day = 60*60*24*100L
if (System.currentTimeMillis() - time.time > one_day) {
return dateTimeFormat.format(time)
} else return timeFormat.format(time)
}
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>() {
/**
* Called when RecyclerView needs a new [ViewHolder] of the given type to represent
* an item.
*
*
* This new ViewHolder should be constructed with a new View that can represent the items
* of the given type. You can either create a new View manually or inflate it from an XML
* layout file.
*
*
* The new ViewHolder will be used to display items of the adapter using
* [.onBindViewHolder]. Since it will be re-used to display
* different items in the data set, it is a good idea to cache references to sub views of
* the View to avoid unnecessary [View.findViewById] calls.
*
* @param parent The ViewGroup into which the new View will be added after it is bound to
* an adapter position.
* @param viewType The view type of the new View.
*
* @return A new ViewHolder that holds a View of the given view type.
* @see .getItemViewType
* @see .onBindViewHolder
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(requireContext())
// Inflate the custom layout
// 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
}
/**
* Returns the total number of items in the data set held by the adapter.
*
* @return The total number of items in this adapter.
*/
override fun getItemCount(): Int = messages.size
/**
* Called by RecyclerView to display the data at the specified position. This method should
* update the contents of the [ViewHolder.itemView] to reflect the item at the given
* position.
*
*
* Note that unlike [android.widget.ListView], RecyclerView will not call this method
* again if the position of the item changes in the data set unless the item itself is
* invalidated or the new position cannot be determined. For this reason, you should only
* use the `position` parameter while acquiring the related data item inside
* this method and should not keep a copy of it. If you need the position of an item later
* on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will
* have the updated adapter position.
*
* Override [.onBindViewHolder] instead if Adapter can
* handle efficient partial bind.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
*/
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!!
val node = nodes.get(msg.from)
2021-01-25 17:30:21 -08:00
// Determine if this is my message (originated on this device).
val isMe = model.myNodeInfo.value?.myNodeNum == node?.num
// Set cardview offset and color.
val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams
val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset)
if (isMe) {
marginParams.leftMargin = messageOffset
marginParams.rightMargin = 0
holder.card.setCardBackgroundColor(resources.getColor(R.color.colorMyMsg))
} else {
marginParams.rightMargin = messageOffset
marginParams.leftMargin = 0
holder.card.setCardBackgroundColor(resources.getColor(R.color.colorMsg))
}
// Hide the username chip for my messages
if (isMe) {
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) {
// FIXME, set the style to show a red error message
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
2021-01-25 17:30:21 -08:00
} else
holder.messageStatusIcon.visibility = View.INVISIBLE
2020-04-08 16:49:27 -07:00
}
private var messages = arrayOf<DataPacket>()
2020-04-08 16:49:27 -07:00
/// Called when our node DB changes
fun onMessagesChanged(msgIn: Collection<DataPacket>) {
messages = msgIn.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 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)
binding.messageInputText.on(EditorInfo.IME_ACTION_DONE) {
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
2021-01-25 17:30:21 -08:00
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
model.messagesState.messages.observe(viewLifecycleOwner, Observer {
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, Observer { connected ->
// If we don't know our node ID and we are offline don't let user try to send
binding.textInputLayout.isEnabled =
connected != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null
})
model.nodeDB.myId.observe(viewLifecycleOwner, Observer { myId ->
// If we don't know our node ID and we are offline don't let user try to send
binding.textInputLayout.isEnabled =
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && myId != null
})
2021-01-25 17:30:21 -08:00
model.myNodeInfo.observe(viewLifecycleOwner, Observer { myNodeInfo ->
// If our Id changed, the UI may need to be updated
messagesAdapter.notifyDataSetChanged()
debug("New id received ${myNodeInfo?.myNodeNum}")
})
2020-04-08 16:49:27 -07:00
}
}
2020-04-08 16:49:27 -07:00