Skip to content

Commit

Permalink
autoscroll
Browse files Browse the repository at this point in the history
  • Loading branch information
maskedeken authored and SaeedDev94 committed Sep 13, 2024
1 parent 98b4e66 commit 9cfc309
Showing 1 changed file with 66 additions and 47 deletions.
113 changes: 66 additions & 47 deletions app/src/main/java/io/github/saeeddev94/xray/activity/LogsActivity.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
package io.github.saeeddev94.xray.activity

import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.ScrollView
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import io.github.saeeddev94.xray.BuildConfig
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.databinding.ActivityLogsBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets

class LogsActivity : AppCompatActivity() {

private lateinit var binding: ActivityLogsBinding
private var loggingThread: Thread? = null
private var loggingProcess: Process? = null

companion object {
private const val MAX_BUFFERED_LINES = (1 shl 14) - 1
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -28,16 +38,8 @@ class LogsActivity : AppCompatActivity() {
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}

override fun onStart() {
super.onStart()
startLoop()
}

override fun onStop() {
super.onStop()
stopLoop()
lifecycleScope.launch(Dispatchers.IO) { streamingLog() }
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
Expand All @@ -48,54 +50,27 @@ class LogsActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.deleteLogs -> {
stopLoop()
flush()
startLoop()
}
R.id.copyLogs -> copyToClipboard(binding.logsTextView.text.toString())
else -> finish()
}
return true
}

private fun startLoop() {
loggingThread = Thread {
try {
loggingProcess = ProcessBuilder("logcat", "-v", "raw", "-s", "GoLog,${BuildConfig.APPLICATION_ID}").start()
val reader = BufferedReader(InputStreamReader(loggingProcess!!.inputStream))
val logs = StringBuilder()
while (!Thread.currentThread().isInterrupted && reader.readLine().also { logs.append("$it\n") } != null) {
runOnUiThread {
binding.logsTextView.text = logs.toString()
binding.logsScrollView.post {
binding.logsScrollView.fullScroll(ScrollView.FOCUS_DOWN)
}
}
}
} catch (error: Exception) {
error.printStackTrace()
} finally {
loggingProcess?.destroy()
private fun flush() {
lifecycleScope.launch(Dispatchers.IO) {
val command = listOf("logcat", "-c")
val process = ProcessBuilder(command).start()
process.waitFor()
withContext(Dispatchers.Main) {
binding.logsTextView.text = ""
}
}
loggingThread?.start()
}

private fun stopLoop() {
loggingThread?.interrupt()
loggingProcess?.destroy()
loggingThread = null
loggingProcess = null
}

private fun flush() {
val command = listOf("logcat", "-c")
val process = ProcessBuilder(command).start()
process.waitFor()
binding.logsTextView.text = ""
}

private fun copyToClipboard(text: String) {
if (text.isBlank()) return
try {
val clipData = ClipData.newPlainText(null, text)
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
Expand All @@ -106,4 +81,48 @@ class LogsActivity : AppCompatActivity() {
}
}

@SuppressLint("SetTextI18n")
private suspend fun streamingLog() = withContext(Dispatchers.IO) {
val builder = ProcessBuilder("logcat", "-v", "time", "-s", "GoLog,${BuildConfig.APPLICATION_ID}")
builder.environment()["LC_ALL"] = "C"
var process: Process? = null
try {
process = try {
builder.start()
} catch (e: IOException) {
Log.e(packageName, Log.getStackTraceString(e))
return@withContext
}
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))

var timeLastNotify = System.nanoTime()
val bufferedLogLines = arrayListOf<String>()
var timeout = 1000000000L / 2 // The timeout is initially small so that the view gets populated immediately.

while (true) {
val line = stdout.readLine() ?: break
bufferedLogLines.add(line)
val timeNow = System.nanoTime()
if (bufferedLogLines.size < MAX_BUFFERED_LINES && (timeNow - timeLastNotify) < timeout && stdout.ready())
continue
timeout = 1000000000L * 5 / 2 // Increase the timeout after the initial view has something in it.
timeLastNotify = timeNow

withContext(Dispatchers.Main) {
val contentHeight = binding.logsTextView.height
val scrollViewHeight = binding.logsScrollView.height
val isScrolledToBottomAlready = (binding.logsScrollView.scrollY + scrollViewHeight) >= contentHeight * 0.95
binding.logsTextView.text = binding.logsTextView.text.toString() + bufferedLogLines.joinToString(separator = "\n", postfix = "\n")
bufferedLogLines.clear()
if (isScrolledToBottomAlready) {
binding.logsScrollView.post {
binding.logsScrollView.fullScroll(View.FOCUS_DOWN)
}
}
}
}
} finally {
process?.destroy()
}
}
}

0 comments on commit 9cfc309

Please sign in to comment.