Ring buffer logcat viewer

This commit is contained in:
Matéo Duparc 2024-11-30 13:22:51 +01:00
parent d72cc857e2
commit ae0e23acc9
Signed by: hardcoresushi
GPG Key ID: AFE384344A45E13A
5 changed files with 174 additions and 13 deletions

View File

@ -11,10 +11,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.databinding.ActivityLogcatBinding
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStreamReader
import java.io.InterruptedIOException
import java.io.OutputStreamWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ -40,12 +38,10 @@ class LogcatActivity: BaseActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
BufferedReader(InputStreamReader(Runtime.getRuntime().exec("logcat").also {
BufferedReader(InputStreamReader(ProcessBuilder("logcat", "-T", "500").start().also {
process = it
}.inputStream)).forEachLine {
binding.content.post {
binding.content.append("$it\n")
}
}.inputStream)).lineSequence().forEach {
binding.content.append(it)
}
} catch (_: InterruptedIOException) {}
}
@ -77,11 +73,13 @@ class LogcatActivity: BaseActivity() {
private fun saveTo(uri: Uri) {
lifecycleScope.launch(Dispatchers.IO) {
BufferedWriter(OutputStreamWriter(contentResolver.openOutputStream(uri))).use {
it.write(binding.content.text.toString())
}
launch(Dispatchers.Main) {
Toast.makeText(this@LogcatActivity, R.string.logcat_saved, Toast.LENGTH_SHORT).show()
contentResolver.openOutputStream(uri)?.use { output ->
Runtime.getRuntime().exec("logcat -d").inputStream.use { input ->
input.copyTo(output)
}
launch(Dispatchers.Main) {
Toast.makeText(this@LogcatActivity, R.string.logcat_saved, Toast.LENGTH_SHORT).show()
}
}
}
}

View File

@ -0,0 +1,45 @@
package sushi.hardcore.droidfs.util
/**
* Minimal ring buffer implementation.
*/
class RingBuffer<T>(private val capacity: Int) {
private val buffer = arrayOfNulls<Any?>(capacity)
/**
* Position of the first cell.
*/
private var head = 0
/**
* Position of the next free (or to be overwritten) cell.
*/
private var tail = 0
var size = 0
private set
fun addLast(e: T) {
buffer[tail] = e
tail = (tail + 1) % capacity
if (size < capacity) {
size += 1
} else {
head = (head + 1) % capacity
}
}
@Suppress("UNCHECKED_CAST")
fun popFirst() = buffer[head].also {
head = (head + 1) % capacity
size -= 1
} as T
/**
* Empty the buffer and call [f] for each element.
*/
fun drain(f: (T) -> Unit) {
repeat(size) {
f(popFirst())
}
}
}

View File

@ -0,0 +1,111 @@
package sushi.hardcore.droidfs.widgets
import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.util.RingBuffer
/**
* A [TextView] dropping first lines when appended too fast.
*
* The dropping rate depends on the size of the ring buffer (set by the
* [R.styleable.RingBufferTextView_updateMaxLines] attribute) and the UI update
* time. If the buffer becomes full before the UI finished to update, the first
* (oldest) lines are dropped such as only the latest appended lines in the buffer
* are going to be displayed.
*
* If the ring buffer never fills up completely, the content of the [TextView] can
* grow indefinitely.
*/
class RingBufferTextView: AppCompatTextView {
private var updateMaxLines = -1
private var averageLineLength = -1
/**
* Lines ring buffer of capacity [updateMaxLines].
*
* Must never be used without acquiring the [bufferLock] mutex.
*/
private val buffer by lazy {
RingBuffer<String>(updateMaxLines)
}
private val bufferLock = Mutex()
/**
* Channel used to notify the worker coroutine that a new line has
* been appended to the ring buffer. No data is sent through it.
*
* We use a buffered channel with a capacity of 1 to ensure that the worker
* can be notified that at least one update occurred while allowing the
* sender to never block.
*
* A greater capacity is not desired because the worker empties the buffer each time.
*/
private val channel = Channel<Unit>(1)
private val scope = CoroutineScope(Dispatchers.Default)
constructor(context: Context, attrs: AttributeSet): super(context, attrs) { init(context, attrs) }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int): super(context, attrs, defStyleAttr) { init(context, attrs) }
private fun init(context: Context, attrs: AttributeSet) {
with (context.obtainStyledAttributes(attrs, R.styleable.RingBufferTextView)) {
updateMaxLines = getInt(R.styleable.RingBufferTextView_updateMaxLines, -1)
averageLineLength = getInt(R.styleable.RingBufferTextView_averageLineLength, -1)
recycle()
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
scope.launch {
val text = StringBuilder(updateMaxLines*averageLineLength)
while (isActive) {
channel.receive()
val size: Int
bufferLock.withLock {
size = buffer.size
buffer.drain {
text.appendLine(it)
}
}
withContext(Dispatchers.Main) {
if (size >= updateMaxLines) {
// Buffer full. Lines could have been dropped so we replace the content.
setText(text.toString())
} else {
super.append(text.toString())
}
}
text.clear()
}
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope.cancel()
}
/**
* Append a line to the ring buffer and update the UI.
*
* If the buffer is full (when adding [R.styleable.RingBufferTextView_updateMaxLines]
* lines before the UI had time to update), the oldest line is overwritten.
*/
suspend fun append(line: String) {
bufferLock.withLock {
buffer.addLast(line)
}
channel.trySend(Unit)
}
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -8,10 +9,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
<sushi.hardcore.droidfs.widgets.RingBufferTextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:updateMaxLines="500"
app:averageLineLength="100"
android:textIsSelectable="true"
android:typeface="monospace" />

View File

@ -2,4 +2,8 @@
<resources>
<attr name="buttonBackgroundColor" format="color"/>
<attr name="infoBarBackgroundColor" format="color"/>
<declare-styleable name="RingBufferTextView">
<attr name="updateMaxLines" format="integer"/>
<attr name="averageLineLength" format="integer"/>
</declare-styleable>
</resources>