From ba2a6e05739570c82dfe4a5faea05b71e4cbf7a5 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sun, 3 Apr 2022 11:25:50 -0300 Subject: [PATCH 1/5] add contacts --- .../java/com/geeksville/mesh/DataPacket.kt | 15 +- .../java/com/geeksville/mesh/MainActivity.kt | 5 +- .../geeksville/mesh/model/MessagesState.kt | 33 ++ .../geeksville/mesh/service/MeshService.kt | 6 +- .../geeksville/mesh/ui/ContactsFragment.kt | 354 ++++++++++++++++++ .../geeksville/mesh/ui/MessagesFragment.kt | 55 ++- app/src/main/res/layout/activity_main.xml | 13 +- .../res/layout/adapter_contact_layout.xml | 62 +++ .../res/layout/adapter_message_layout.xml | 2 +- app/src/main/res/layout/fragment_contacts.xml | 19 + app/src/main/res/layout/messages_fragment.xml | 36 +- 11 files changed, 565 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt create mode 100644 app/src/main/res/layout/adapter_contact_layout.xml create mode 100644 app/src/main/res/layout/fragment_contacts.xml diff --git a/app/src/main/java/com/geeksville/mesh/DataPacket.kt b/app/src/main/java/com/geeksville/mesh/DataPacket.kt index 7f5f20b76..cd7ae69ce 100644 --- a/app/src/main/java/com/geeksville/mesh/DataPacket.kt +++ b/app/src/main/java/com/geeksville/mesh/DataPacket.kt @@ -29,7 +29,9 @@ data class DataPacket( var time: Long = System.currentTimeMillis(), // msecs since 1970 var id: Int = 0, // 0 means unassigned var status: MessageStatus? = MessageStatus.UNKNOWN, - var hopLimit: Int = 0 + var hopLimit: Int = 0, + var channel: Int = 0, // channel index + var delayed: Int = 0 // S&F MeshProtos.MeshPacket.Delayed.(...)_VALUE ) : Parcelable { /** @@ -64,6 +66,8 @@ data class DataPacket( parcel.readLong(), parcel.readInt(), parcel.readParcelable(MessageStatus::class.java.classLoader), + parcel.readInt(), + parcel.readInt(), parcel.readInt() ) @@ -75,6 +79,7 @@ data class DataPacket( if (from != other.from) return false if (to != other.to) return false + if (channel != other.channel) return false if (time != other.time) return false if (id != other.id) return false if (dataType != other.dataType) return false @@ -94,6 +99,8 @@ data class DataPacket( result = 31 * result + bytes!!.contentHashCode() result = 31 * result + status.hashCode() result = 31 * result + hopLimit + result = 31 * result + channel + result = 31 * result + delayed return result } @@ -106,6 +113,8 @@ data class DataPacket( parcel.writeInt(id) parcel.writeParcelable(status, flags) parcel.writeInt(hopLimit) + parcel.writeInt(channel) + parcel.writeInt(delayed) } override fun describeContents(): Int { @@ -122,6 +131,8 @@ data class DataPacket( id = parcel.readInt() status = parcel.readParcelable(MessageStatus::class.java.classLoader) hopLimit = parcel.readInt() + channel = parcel.readInt() + delayed = parcel.readInt() } companion object CREATOR : Parcelable.Creator { @@ -145,7 +156,7 @@ data class DataPacket( override fun newArray(size: Int): Array { return arrayOfNulls(size) } - val utf8 = Charset.forName("UTF-8") + val utf8: Charset = Charset.forName("UTF-8") } diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index d7bf8c0dd..045d1b4b7 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -13,6 +13,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler +import android.os.Looper import android.os.RemoteException import android.text.method.LinkMovementMethod import android.view.Menu @@ -144,7 +145,7 @@ class MainActivity : AppCompatActivity(), Logging, TabInfo( "Messages", R.drawable.ic_twotone_message_24, - MessagesFragment() + ContactsFragment() ), TabInfo( "Users", @@ -1010,7 +1011,7 @@ class MainActivity : AppCompatActivity(), Logging, } val handler: Handler by lazy { - Handler(mainLooper) + Handler(Looper.getMainLooper()) } override fun onPrepareOptionsMenu(menu: Menu): Boolean { diff --git a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt index a678034fc..470ce8a5e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt @@ -30,10 +30,40 @@ class MessagesState(private val ui: UIViewModel) : Logging { } + 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() + + val nodes = ui.nodeDB.nodes.value!! + nodes.keys.forEachIndexed { index, node -> + if (index != 0 && contactsList[node] == null) + contactsList[node] = emptyDataPacket(node) + } + 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 @@ -44,6 +74,7 @@ class MessagesState(private val ui: UIViewModel) : Logging { messagesList.add(m) messages.value = messagesList + buildContacts() } fun removeMessage(m: DataPacket) { @@ -51,6 +82,7 @@ class MessagesState(private val ui: UIViewModel) : Logging { messagesList.remove(m) messages.value = messagesList + buildContacts() } private fun removeAllMessages() { @@ -58,6 +90,7 @@ class MessagesState(private val ui: UIViewModel) : Logging { messagesList.clear() messages.value = messagesList + buildContacts() } fun updateStatus(id: Int, status: MessageStatus) { 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 e0a10c812..b6b70d4eb 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -696,7 +696,9 @@ class MeshService : Service(), Logging { id = packet.id, dataType = data.portnumValue, bytes = bytes, - hopLimit = hopLimit + hopLimit = hopLimit, + channel = packet.channel, + delayed = packet.delayedValue ) } } @@ -719,7 +721,7 @@ class MeshService : Service(), Logging { // we only care about old text messages, we just store those... if (dataPacket.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) { // discard old messages if needed then add the new one - while (recentDataPackets.size > 50) + while (recentDataPackets.size > 100) recentDataPackets.removeAt(0) // FIXME - possible kotlin bug in 1.3.72 - it seems that if we start with the (globally shared) emptyList, diff --git a/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt new file mode 100644 index 000000000..5a33898eb --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt @@ -0,0 +1,354 @@ +package com.geeksville.mesh.ui + +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.os.Bundle +import android.view.* +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.setFragmentResult +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.geeksville.android.Logging +import com.geeksville.mesh.DataPacket +import com.geeksville.mesh.MainActivity +import com.geeksville.mesh.R +import com.geeksville.mesh.databinding.AdapterContactLayoutBinding +import com.geeksville.mesh.databinding.FragmentContactsBinding +import com.geeksville.mesh.model.UIViewModel +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import java.text.DateFormat +import java.util.* + +@AndroidEntryPoint +class ContactsFragment : ScreenFragment("Messages"), Logging { + + private var actionMode: ActionMode? = null + private var _binding: FragmentContactsBinding? = null + + // This property is only valid between onCreateView and onDestroyView. + private val binding get() = _binding!! + + private val model: UIViewModel by activityViewModels() + + 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) + } + + // 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 + class ViewHolder(itemView: AdapterContactLayoutBinding) : + RecyclerView.ViewHolder(itemView.root) { + val shortName = itemView.shortName + val longName = itemView.longName + val lastMessageTime = itemView.lastMessageTime + val lastMessageText = itemView.lastMessageText + } + + private val contactsAdapter = object : RecyclerView.Adapter() { + + /** + * 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 + val contactsView = AdapterContactLayoutBinding.inflate(inflater, parent, false) + + // Return a new holder instance + return ViewHolder(contactsView) + } + + private var messages = arrayOf() + private var contacts = arrayOf() + private var selectedList = ArrayList() + + /** + * 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 = contacts.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) { + val contact = contacts[position] + + // 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 + + // grab usernames from NodeInfo + val nodes = model.nodeDB.nodes.value!! + val node = nodes[if (isLocal) contact.to else contact.from] + + //grab channel names from RadioConfig + val channels = model.channels.value + val primaryChannel = channels?.primaryChannel + + 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) + + holder.shortName.text = if (isBroadcast) "All" else shortName + holder.longName.text = longName + + val text = if (isLocal) contact.text else "$shortName: ${contact.text}" + holder.lastMessageText.text = text + + if (contact.time != 0L) { + holder.lastMessageTime.visibility = View.VISIBLE + holder.lastMessageTime.text = getShortDateTime(Date(contact.time)) + } else holder.lastMessageTime.visibility = View.INVISIBLE + + holder.itemView.setOnLongClickListener { + if (actionMode == null) { + actionMode = + (activity as MainActivity).startActionMode(object : 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 { + clickItem(holder, contactId) + return true + } + + override fun onActionItemClicked( + mode: ActionMode, + item: MenuItem + ): Boolean { + when (item.itemId) { + R.id.deleteButton -> { + val messagesByContactId = ArrayList() + selectedList.forEach { contactId -> + messagesByContactId += messages.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 + } + } + val deleteMessagesString = resources.getQuantityString( + R.plurals.delete_messages, + messagesByContactId.size, + messagesByContactId.size + ) + MaterialAlertDialogBuilder(requireContext()) + .setMessage(deleteMessagesString) + .setPositiveButton(getString(R.string.delete)) { _, _ -> + debug("User clicked deleteButton") + // all items selected --> deleteAllMessages() + if (messagesByContactId.size == messages.size) { + model.messagesState.deleteAllMessages() + } else { + messagesByContactId.forEach { + model.messagesState.deleteMessage(it) + } + } + mode.finish() + } + .setNeutralButton(R.string.cancel) { _, _ -> + } + .show() + } + R.id.selectAllButton -> { + // if all selected -> unselect all + if (selectedList.size == contacts.size) { + selectedList.clear() + mode.finish() + } else { + // else --> select all + selectedList.clear() + + contacts.forEach { + if (it.from == DataPacket.ID_LOCAL || it.to == DataPacket.ID_BROADCAST) + selectedList.add(it.to!!) else selectedList.add(it.from!!) + } + } + actionMode?.title = selectedList.size.toString() + notifyDataSetChanged() + } + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + selectedList.clear() + notifyDataSetChanged() + actionMode = null + } + }) + } else { + // when action mode is enabled + clickItem(holder, contactId) + } + true + } + holder.itemView.setOnClickListener { + if (actionMode != null) clickItem(holder, contactId) + else { + debug("calling MessagesFragment filter:$contactId") + setFragmentResult( + "requestKey", + bundleOf("contactId" to contactId, "contactName" to longName) + ) + parentFragmentManager.beginTransaction() + .replace(R.id.mainActivityLayout, MessagesFragment()) + .addToBackStack(null) + .commit() + } + } + + if (selectedList.contains(contactId)) { + 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 + ) + ) + } + } + } + + private fun clickItem( + holder: ViewHolder, + contactId: String? = DataPacket.ID_BROADCAST + ) { + val position = holder.bindingAdapterPosition + if (contactId != null && !selectedList.contains(contactId)) { + selectedList.add(contactId) + } else { + selectedList.remove(contactId) + } + if (selectedList.isEmpty()) { + // finish action mode when no items selected + actionMode?.finish() + } else { + // show total items selected on action mode title + actionMode?.title = selectedList.size.toString() + } + notifyItemChanged(position) + } + + /// Called when our contacts DB changes + fun onContactsChanged(contactsIn: Collection) { + contacts = contactsIn.sortedByDescending { it.time }.toTypedArray() + notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes + } + + /// Called when our message DB changes + fun onMessagesChanged(msgIn: Collection) { + messages = msgIn.toTypedArray() + } + + fun onChannelsChanged() { + val oldBroadcast = contacts.find { it.to == DataPacket.ID_BROADCAST } + if (oldBroadcast != null) { + notifyItemChanged(contacts.indexOf(oldBroadcast)) + } + } + } + + override fun onPause() { + actionMode?.finish() + super.onPause() + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentContactsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.contactsView.adapter = contactsAdapter + binding.contactsView.layoutManager = LinearLayoutManager(requireContext()) + + model.channels.observe(viewLifecycleOwner) { + contactsAdapter.onChannelsChanged() + } + + model.messagesState.contacts.observe(viewLifecycleOwner) { + debug("New contacts received: ${it.size}") + contactsAdapter.onContactsChanged(it.values) + } + + model.messagesState.messages.observe(viewLifecycleOwner) { + contactsAdapter.onMessagesChanged(it) + } + } +} 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 5a4bf550b..b4415a128 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt @@ -11,7 +11,9 @@ import android.widget.ImageView import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.setFragmentResultListener import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.geeksville.android.Logging @@ -41,13 +43,15 @@ fun EditText.on(actionId: Int, func: () -> Unit) { } @AndroidEntryPoint -class MessagesFragment : ScreenFragment("Messages"), Logging { +class MessagesFragment : Fragment(), Logging { private var actionMode: ActionMode? = null private var _binding: MessagesFragmentBinding? = null // This property is only valid between onCreateView and onDestroyView. private val binding get() = _binding!! + private var contactId: String = DataPacket.ID_BROADCAST + private var contactName: String = DataPacket.ID_BROADCAST private val model: UIViewModel by activityViewModels() @@ -158,14 +162,27 @@ class MessagesFragment : ScreenFragment("Messages"), Logging { val msg = messages[position] val nodes = model.nodeDB.nodes.value!! val node = nodes.get(msg.from) - // Determine if this is my message (originated on this device). - // val isMe = model.myNodeInfo.value?.myNodeNum == node?.num - val isMe = msg.from == "^local" + // Determine if this is my message (originated on this device) + val isLocal = msg.from == DataPacket.ID_LOCAL + val isBroadcast = (msg.to == DataPacket.ID_BROADCAST + || msg.delayed == 1) // MeshProtos.MeshPacket.Delayed.DELAYED_BROADCAST_VALUE == 1 + + // Filter messages by contactId + if (contactId == DataPacket.ID_BROADCAST) { + if (isBroadcast) { + holder.card.visibility = View.VISIBLE + } else holder.card.visibility = View.GONE + } else { + if (msg.from == contactId && msg.to != DataPacket.ID_BROADCAST + || msg.from == DataPacket.ID_LOCAL && msg.to == contactId) { + holder.card.visibility = View.VISIBLE + } else holder.card.visibility = View.GONE + } // Set cardview offset and color. val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset) - if (isMe) { + if (isLocal) { marginParams.leftMargin = messageOffset marginParams.rightMargin = 0 holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_END @@ -191,7 +208,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging { } } // Hide the username chip for my messages - if (isMe) { + if (isLocal) { holder.username.visibility = View.GONE } else { holder.username.visibility = View.VISIBLE @@ -259,22 +276,30 @@ class MessagesFragment : ScreenFragment("Messages"), Logging { selectedList.forEach { model.messagesState.deleteMessage(it) } - mode.finish() } + mode.finish() } .setNeutralButton(R.string.cancel) { _, _ -> } .show() } R.id.selectAllButton -> { + // filter messages by ContactId + val messagesByContactId = messages.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 + } // if all selected -> unselect all - if (selectedList.size == messages.size) { + if (selectedList.size == messagesByContactId.size) { selectedList.clear() mode.finish() } else { // else --> select all selectedList.clear() - selectedList.addAll(messages) + selectedList.addAll(messagesByContactId) } actionMode?.title = selectedList.size.toString() notifyDataSetChanged() @@ -326,7 +351,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging { actionMode?.finish() } else { // show total items selected on action mode title - actionMode?.title = "${selectedList.size}" + actionMode?.title = selectedList.size.toString() } notifyItemChanged(position) } @@ -357,12 +382,20 @@ class MessagesFragment : ScreenFragment("Messages"), Logging { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + 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()) - model.messagesState.sendMessage(str) + model.messagesState.sendMessage(str, contactId) binding.messageInputText.setText("") // blow away the string the user just entered // requireActivity().hideKeyboard() diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1ce253cc3..5a9e3b695 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -62,11 +62,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:tabIconTint="@color/tab_color_selector" - app:tabIndicatorColor="@color/selectedColor" - > - - - + app:tabIndicatorColor="@color/selectedColor" /> - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_contact_layout.xml b/app/src/main/res/layout/adapter_contact_layout.xml new file mode 100644 index 000000000..b1aa9e7ce --- /dev/null +++ b/app/src/main/res/layout/adapter_contact_layout.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_message_layout.xml b/app/src/main/res/layout/adapter_message_layout.xml index 25f104d45..3c9b0e059 100644 --- a/app/src/main/res/layout/adapter_message_layout.xml +++ b/app/src/main/res/layout/adapter_message_layout.xml @@ -61,9 +61,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" - android:layout_marginBottom="8dp" android:contentDescription="@string/message_reception_time" android:text="3 minutes ago" + android:textSize="12sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/messageStatusIcon" app:layout_constraintTop_toBottomOf="@id/messageText" /> diff --git a/app/src/main/res/layout/fragment_contacts.xml b/app/src/main/res/layout/fragment_contacts.xml new file mode 100644 index 000000000..a96d827ca --- /dev/null +++ b/app/src/main/res/layout/fragment_contacts.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/messages_fragment.xml b/app/src/main/res/layout/messages_fragment.xml index 411a6d8e0..851934b08 100644 --- a/app/src/main/res/layout/messages_fragment.xml +++ b/app/src/main/res/layout/messages_fragment.xml @@ -2,21 +2,45 @@ + android:layout_height="match_parent" + android:background="@color/colorAdvancedBackground"> + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/toolbar" /> Date: Sun, 3 Apr 2022 12:11:24 -0300 Subject: [PATCH 2/5] updating proto submodule to latest --- app/src/main/proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/proto b/app/src/main/proto index 99ce57802..c7649e961 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit 99ce57802da9e8a3106d29b94e14e4a090cde3b3 +Subproject commit c7649e9611087c9ae975d3723efd33167f7fd21f From b1f2a83c23a745f4f8b774fa86e5637baf5929f0 Mon Sep 17 00:00:00 2001 From: Andre Kirchhoff Date: Mon, 4 Apr 2022 00:10:50 -0300 Subject: [PATCH 3/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95ee92947..b4ba424af 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ However, if you must use 'raw' APKs you can get them from our [github releases]( If you would like to develop this application we'd love your help! These build instructions are brief and should be improved, please send a PR if you can. -- Use Android Studio 4.1.2 to build/debug (other versions might work but no promises) +- Use Android Studio to build/debug - Use "git submodule update --init --recursive" to pull in the various submodules we depend on - There are a few config files which you'll need to copy from templates included in the project. Run the following commands to do so: From 78f7477c8561d7c91947c833ca805faf699ba7db Mon Sep 17 00:00:00 2001 From: andrekir Date: Mon, 4 Apr 2022 19:10:15 -0300 Subject: [PATCH 4/5] promote device metrics --- .../main/java/com/geeksville/mesh/NodeInfo.kt | 20 +++++++++---------- .../geeksville/mesh/service/MeshService.kt | 15 +++++++------- .../com/geeksville/mesh/ui/UsersFragment.kt | 6 +++--- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index fb52b6f21..e6945a735 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -81,7 +81,7 @@ data class Position( @Serializable @Parcelize -data class Telemetry( +data class DeviceMetrics( val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!) val batteryLevel: Int = 0, val voltage: Float, @@ -94,16 +94,16 @@ data class Telemetry( /** Create our model object from a protobuf. */ - constructor(p: TelemetryProtos.Telemetry, defaultTime: Int = currentTime()) : this( - if (p.time != 0) p.time else defaultTime, - p.deviceMetrics.batteryLevel, - p.deviceMetrics.voltage, - p.deviceMetrics.channelUtilization, - p.deviceMetrics.airUtilTx + constructor(p: TelemetryProtos.DeviceMetrics, telemetryTime: Int = currentTime()) : this( + telemetryTime, + p.batteryLevel, + p.voltage, + p.channelUtilization, + p.airUtilTx ) override fun toString(): String { - return "Telemetry(time=${time}, batteryLevel=${batteryLevel}, voltage=${voltage}, channelUtilization=${channelUtilization}, airUtilTx=${airUtilTx})" + return "DeviceMetrics(time=${time}, batteryLevel=${batteryLevel}, voltage=${voltage}, channelUtilization=${channelUtilization}, airUtilTx=${airUtilTx})" } } @@ -117,10 +117,10 @@ data class NodeInfo( var snr: Float = Float.MAX_VALUE, var rssi: Int = Int.MAX_VALUE, var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970 - var telemetry: Telemetry? = null + var deviceMetrics: DeviceMetrics? = null ) : Parcelable { - val batteryPctLevel get() = telemetry?.batteryLevel + val batteryPctLevel get() = deviceMetrics?.batteryLevel /** * true if the device was heard from recently 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 b6b70d4eb..e04cc934a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -902,14 +902,17 @@ class MeshService : Service(), Logging { } } - /// Update our DB of users based on someone sending out a User subpacket + /// Update our DB of users based on someone sending out a Telemetry subpacket private fun handleReceivedTelemetry( fromNum: Int, p: TelemetryProtos.Telemetry, defaultTime: Long = System.currentTimeMillis() ) { updateNodeInfo(fromNum) { - it.telemetry = Telemetry(p, (defaultTime / 1000L).toInt()) + it.deviceMetrics = DeviceMetrics( + p.deviceMetrics, + if (p.time != 0) p.time else (defaultTime / 1000L).toInt() + ) } } @@ -1300,10 +1303,8 @@ class MeshService : Service(), Logging { it.position = Position(info.position) } - if (info.hasTelemetry()) { - // For the local node, it might not be able to update its times because it doesn't have a valid GPS reading yet - // so if the info is for _our_ node we always assume time is current - it.telemetry = Telemetry(info.telemetry) + if (info.hasDeviceMetrics()) { + it.deviceMetrics = DeviceMetrics(info.deviceMetrics) } it.lastHeard = info.lastHeard @@ -1311,7 +1312,7 @@ class MeshService : Service(), Logging { } private fun handleNodeInfo(info: MeshProtos.NodeInfo) { - debug("Received nodeinfo num=${info.num}, hasUser=${info.hasUser()}, hasPosition=${info.hasPosition()}, hasTelemetry=${info.hasTelemetry()}") + debug("Received nodeinfo num=${info.num}, hasUser=${info.hasUser()}, hasPosition=${info.hasPosition()}, hasDeviceMetrics=${info.hasDeviceMetrics()}") val packetToSave = Packet( UUID.randomUUID().toString(), 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 2a182643e..815054268 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -136,7 +136,7 @@ class UsersFragment : ScreenFragment("Users"), Logging { } else { holder.distanceView.visibility = View.INVISIBLE } - renderBattery(n.batteryPctLevel, n.telemetry?.voltage, holder) + renderBattery(n.batteryPctLevel, n.deviceMetrics?.voltage, holder) holder.lastTime.text = formatAgo(n.lastHeard) @@ -146,8 +146,8 @@ class UsersFragment : ScreenFragment("Users"), Logging { val text = String.format( "ChUtil %.1f%% AirUtilTX %.1f%%", - n.telemetry?.channelUtilization ?: info.channelUtilization, - n.telemetry?.airUtilTx ?: info.airUtilTx + n.deviceMetrics?.channelUtilization ?: info.channelUtilization, + n.deviceMetrics?.airUtilTx ?: info.airUtilTx ) holder.signalView.text = text holder.signalView.visibility = View.VISIBLE From 57c05864439e825ab53b07a8471c89746ae85ad5 Mon Sep 17 00:00:00 2001 From: Jm Casler Date: Tue, 5 Apr 2022 22:23:19 -0700 Subject: [PATCH 5/5] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b4ba424af..1541dcaae 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Open in Visual Studio Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/meshtastic/Meshtastic-Android) [![Android CI](https://github.com/meshtastic/Meshtastic-Android/actions/workflows/android.yml/badge.svg)](https://github.com/meshtastic/Meshtastic-Android/actions/workflows/android.yml) ![GitHub all releases](https://img.shields.io/github/downloads/meshtastic/meshtastic-android/total) +[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/Meshtastic-Android)](https://cla-assistant.io/meshtastic/Meshtastic-Android) This is a tool for using Android with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the the device side code, see [here](https://github.com/meshtastic/Meshtastic-esp32).