Added a debug panel

final
This commit is contained in:
Ludovic Goix 2020-09-23 22:47:45 -04:00
parent 2dab8ccf19
commit 6ec16073c1
15 changed files with 484 additions and 68 deletions

View file

@ -12,6 +12,8 @@ apply plugin: 'com.google.firebase.crashlytics'
// protobuf
apply plugin: 'com.google.protobuf'
apply plugin: 'kotlin-kapt'
android {
/*
signingConfigs {
@ -105,6 +107,9 @@ protobuf {
}
dependencies {
def room_version = "2.2.5"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
@ -117,7 +122,15 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

View file

@ -30,6 +30,8 @@ import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.geeksville.android.GeeksvilleApplication
@ -917,6 +919,15 @@ class MainActivity : AppCompatActivity(), Logging,
Toast.makeText(applicationContext, item.title, Toast.LENGTH_SHORT).show()
return true
}
R.id.debug -> {
val fragmentManager: FragmentManager = supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
val nameFragment = DebugFragment()
fragmentTransaction.add(R.id.mainActivityLayout, nameFragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
return true
}
else -> super.onOptionsItemSelected(item)
}
}

View file

@ -0,0 +1,36 @@
package com.geeksville.mesh.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.Packet
@Database(entities = [Packet::class], version = 1, exportSchema = false)
abstract class MeshtasticDatabase : RoomDatabase() {
abstract fun packetDao(): PacketDao
companion object {
@Volatile
private var INSTANCE: MeshtasticDatabase? = null
fun getDatabase(
context: Context
): MeshtasticDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
MeshtasticDatabase::class.java,
"meshtastic_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}

View file

@ -0,0 +1,17 @@
package com.geeksville.mesh.database
import androidx.lifecycle.LiveData
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.Packet
class PacketRepository(private val packetDao : PacketDao) {
val allPackets : LiveData<List<Packet>> = packetDao.getAllPacket(500)
suspend fun insert(packet: Packet) {
packetDao.insert(packet)
}
suspend fun deleteAll() {
packetDao.deleteAll()
}
}

View file

@ -0,0 +1,21 @@
package com.geeksville.mesh.database.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.geeksville.mesh.database.entity.Packet
@Dao
interface PacketDao {
@Query("Select * from packet order by rowid desc limit 0,:maxItem")
fun getAllPacket(maxItem: Int): LiveData<List<Packet>>
@Insert
fun insert(packet: Packet)
@Query("DELETE from packet")
fun deleteAll()
}

View file

@ -0,0 +1,17 @@
package com.geeksville.mesh.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "packet")
data class Packet(@PrimaryKey val uuid: String,
@ColumnInfo(name = "type") val message_type: String,
@ColumnInfo(name = "received_date") val received_date: Long,
@ColumnInfo(name = "message") val raw_message: String
) {
}

View file

@ -8,13 +8,20 @@ import android.os.RemoteException
import android.view.Menu
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.geeksville.android.BuildUtils.isEmulator
import com.geeksville.android.Logging
import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.service.MeshService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/// Given a human name, strip out the first letter of the first three words and return that as the initials for
/// that user. If the original name is only one word, strip vowels from the original name and if the result is
@ -37,10 +44,27 @@ fun getInitials(nameIn: String): String {
}
class UIViewModel(app: Application) : AndroidViewModel(app), Logging {
private val repository: PacketRepository
val allPackets: LiveData<List<Packet>>
init {
val packetsDao = MeshtasticDatabase.getDatabase(app).packetDao()
repository = PacketRepository(packetsDao)
allPackets = repository.allPackets
debug("ViewModel created")
}
fun insertPacket(packet: Packet) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(packet)
}
fun deleteAllPacket() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
}
companion object {
/**
* Return the current channel info

View file

@ -28,6 +28,9 @@ import com.geeksville.mesh.*
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
import com.geeksville.mesh.R
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.util.*
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.common.api.ResolvableApiException
@ -163,6 +166,8 @@ class MeshService : Service(), Logging {
/// The current state of our connection
private var connectionState = ConnectionState.DISCONNECTED
private var packetRepo: PacketRepository? = null
/*
see com.geeksville.mesh broadcast intents
// RECEIVED_OPAQUE for data received from other nodes
@ -481,6 +486,9 @@ class MeshService : Service(), Logging {
info("Creating mesh service")
val packetsDao = MeshtasticDatabase.getDatabase(applicationContext).packetDao()
packetRepo = PacketRepository(packetsDao)
// Switch to the IO thread
serviceScope.handledLaunch {
loadSettings() // Load our last known node DB
@ -964,6 +972,8 @@ class MeshService : Service(), Logging {
// debug("Recieved: $packet")
val p = packet.decoded
val packetToSave = Packet(UUID.randomUUID().toString(), "packet", System.currentTimeMillis(), packet.toString())
insertPacket(packetToSave)
// If the rxTime was not set by the device (because device software was old), guess at a time
val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond()
@ -995,6 +1005,13 @@ class MeshService : Service(), Logging {
handleAckNak(false, p.failId)
}
private fun insertPacket(packetToSave: Packet) {
serviceScope.handledLaunch {
info("insert: ${packetToSave.message_type} = ${packetToSave.raw_message.toOneLineString()}")
packetRepo!!.insert(packetToSave)
}
}
private fun currentSecond() = (System.currentTimeMillis() / 1000).toInt()
@ -1229,6 +1246,8 @@ class MeshService : Service(), Logging {
private fun handleRadioConfig(radio: MeshProtos.RadioConfig) {
val packetToSave = Packet(UUID.randomUUID().toString(), "RadioConfig", System.currentTimeMillis(), radio.toString())
insertPacket(packetToSave)
radioConfig = radio
}
@ -1257,6 +1276,9 @@ class MeshService : Service(), Logging {
private fun handleNodeInfo(info: MeshProtos.NodeInfo) {
debug("Received nodeinfo num=${info.num}, hasUser=${info.hasUser()}, hasPosition=${info.hasPosition()}")
val packetToSave = Packet(UUID.randomUUID().toString(), "NodeInfo", System.currentTimeMillis(), info.toString())
insertPacket(packetToSave)
logAssert(newNodes.size <= 256) // Sanity check to make sure a device bug can't fill this list forever
newNodes.add(info)
}
@ -1266,6 +1288,9 @@ class MeshService : Service(), Logging {
* Update the nodeinfo (called from either new API version or the old one)
*/
private fun handleMyInfo(myInfo: MeshProtos.MyNodeInfo) {
val packetToSave = Packet(UUID.randomUUID().toString(), "MyNodeInfo", System.currentTimeMillis(), myInfo.toString())
insertPacket(packetToSave)
setFirmwareUpdateFilename(myInfo)
val mi = with(myInfo) {
@ -1312,6 +1337,10 @@ class MeshService : Service(), Logging {
private fun handleConfigComplete(configCompleteId: Int) {
if (configCompleteId == configNonce) {
val packetToSave = Packet(UUID.randomUUID().toString(), "ConfigComplete", System.currentTimeMillis(), configCompleteId.toString())
insertPacket(packetToSave)
// This was our config request
if (newMyNodeInfo == null || newNodes.isEmpty())
errormsg("Did not receive a valid config")

View file

@ -0,0 +1,50 @@
package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import kotlinx.android.synthetic.main.debug_fragment.*
class DebugFragment : Fragment() {
val model: UIViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.debug_fragment, container, false)
}
//Button to clear All log
//List all log
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = view.findViewById<RecyclerView>(R.id.packets_recyclerview)
val adapter = PacketListAdapter(requireContext())
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
clearButton.setOnClickListener {
model.deleteAllPacket()
}
closeButton.setOnClickListener{
parentFragmentManager.popBackStack();
}
model.allPackets.observe(viewLifecycleOwner, Observer {
packets -> packets?.let { adapter.setPackets(it) }
})
}
}

View file

@ -0,0 +1,49 @@
package com.geeksville.mesh.ui
import android.content.Context
import java.text.DateFormat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.Packet
import java.util.*
class PacketListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<PacketListAdapter.PacketViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var packets = emptyList<Packet>()
private val timeFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
inner class PacketViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val packetTypeView: TextView = itemView.findViewById(R.id.type)
val packetDateReceivedView: TextView = itemView.findViewById(R.id.dateReceived)
val packetRawMessage : TextView = itemView.findViewById(R.id.rawMessage)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PacketViewHolder {
val itemView = inflater.inflate(R.layout.adapter_packet_layout, parent, false)
return PacketViewHolder(itemView)
}
override fun onBindViewHolder(holder: PacketViewHolder, position: Int) {
val current = packets[position]
holder.packetTypeView.text = current.message_type
holder.packetRawMessage.text = current.raw_message
val date = Date(current.received_date)
holder.packetDateReceivedView.text = timeFormat.format(date)
}
internal fun setPackets(packets: List<Packet>) {
this.packets = packets
notifyDataSetChanged()
}
override fun getItemCount() = packets.size
}

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M8,13H10.55V10H13.45V13H16L12,17L8,13M19.35,10.04C21.95,10.22 24,12.36 24,15A5,5 0 0,1 19,20H6A6,6 0 0,1 0,14C0,10.91 2.34,8.36 5.35,8.04C6.6,5.64 9.11,4 12,4C15.64,4 18.67,6.59 19.35,10.04M19,18A3,3 0 0,0 22,15C22,13.45 20.78,12.14 19.22,12.04L17.69,11.93L17.39,10.43C16.88,7.86 14.62,6 12,6C9.94,6 8.08,7.14 7.13,8.97L6.63,9.92L5.56,10.03C3.53,10.24 2,11.95 2,14A4,4 0 0,0 6,18H19Z" />
</vector>

View file

@ -1,90 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
tools:context=".MainActivity"
android:id="@+id/mainActivityLayout">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView5"
android:layout_width="wrap_content"
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:contentDescription="@string/application_icon"
android:scaleType="center"
android:scaleX="1.5"
android:scaleY="1.5"
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24" />
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
<ImageView
android:id="@+id/imageView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:contentDescription="@string/application_icon"
android:scaleType="center"
android:scaleX="1.5"
android:scaleY="1.5"
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center"
android:minHeight="?actionBarSize"
android:padding="16dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" />
</androidx.appcompat.widget.Toolbar>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Screen.messages -> MessagesContent()
Screen.settings -> SettingsContent()
Screen.users -> UsersContent()
Screen.channel -> ChannelContent(UIState.getChannel())
Screen.map -> MapContent()
<com.google.android.material.tabs.TabItem
android:icon="@drawable/ic_twotone_message_24"
android:text="Messages"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center"
android:minHeight="?actionBarSize"
android:padding="16dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" />
android:layout_width="wrap_content" />
</androidx.appcompat.widget.Toolbar>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabItem
android:icon="@drawable/ic_twotone_settings_applications_24"
android:text="Settings"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
-->
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
<!-- Screen.messages -> MessagesContent()
Screen.settings -> SettingsContent()
Screen.users -> UsersContent()
Screen.channel -> ChannelContent(UIState.getChannel())
Screen.map -> MapContent()
<com.google.android.material.tabs.TabItem
android:icon="@drawable/ic_twotone_message_24"
android:text="Messages"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
<com.google.android.material.tabs.TabItem
android:icon="@drawable/ic_twotone_settings_applications_24"
android:text="Settings"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
-->
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</FrameLayout>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false">
<com.google.android.material.card.MaterialCardView
style="@style/Widget.App.CardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="node_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/dateReceived"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="09-17 21:00:58.641"
app:layout_constraintBottom_toBottomOf="@+id/cloudDownloadIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/cloudDownloadIcon" />
<TextView
android:id="@+id/rawMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:fontFamily="monospace"
android:text="TextView"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textIsSelectable="true"
android:textSize="8sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/type" />
<ImageView
android:id="@+id/cloudDownloadIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:alpha="0.4"
app:layout_constraintEnd_toStartOf="@+id/dateReceived"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/cloud_download_outline_24" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/packets_recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/clearButton" />
<Button
android:id="@+id/clearButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="@string/clear_last_messages"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/closeButton"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:backgroundTint="#D1D1D1"
app:icon="@android:drawable/ic_menu_close_clear_cancel"
app:iconPadding="0dp"
app:iconSize="24dp"
app:iconTint="#000000"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/Meshtastic.Button.Rounded" />
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/debug_last_messages"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/packets_recyclerview"
app:layout_constraintEnd_toStartOf="@+id/clearButton"
app:layout_constraintStart_toEndOf="@+id/closeButton"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -7,6 +7,10 @@
android:tint="#FFFFFF"
android:title="@string/disconnected"
app:showAsAction="ifRoom" />
<item
android:id="@+id/debug"
android:title="@string/debug_panel"
app:showAsAction="withText" />
<item
android:id="@+id/about"
android:title="@string/about"