Ring buffer logcat viewer
This commit is contained in:
parent
d72cc857e2
commit
ae0e23acc9
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
45
app/src/main/java/sushi/hardcore/droidfs/util/RingBuffer.kt
Normal file
45
app/src/main/java/sushi/hardcore/droidfs/util/RingBuffer.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user