LogcatActivity.kt 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package com.github.kr328.clash
  2. import android.content.ComponentName
  3. import android.content.Context
  4. import android.content.ServiceConnection
  5. import android.net.Uri
  6. import android.os.IBinder
  7. import android.widget.Toast
  8. import androidx.activity.result.contract.ActivityResultContracts
  9. import com.github.kr328.clash.common.compat.startForegroundServiceCompat
  10. import com.github.kr328.clash.common.log.Log
  11. import com.github.kr328.clash.common.util.fileName
  12. import com.github.kr328.clash.common.util.intent
  13. import com.github.kr328.clash.common.util.ticker
  14. import com.github.kr328.clash.core.model.LogMessage
  15. import com.github.kr328.clash.design.LogcatDesign
  16. import com.github.kr328.clash.design.dialog.withModelProgressBar
  17. import com.github.kr328.clash.design.model.LogFile
  18. import com.github.kr328.clash.design.ui.ToastDuration
  19. import com.github.kr328.clash.design.util.showExceptionToast
  20. import com.github.kr328.clash.log.LogcatFilter
  21. import com.github.kr328.clash.log.LogcatReader
  22. import com.github.kr328.clash.util.logsDir
  23. import kotlinx.coroutines.Dispatchers
  24. import kotlinx.coroutines.isActive
  25. import kotlinx.coroutines.selects.select
  26. import kotlinx.coroutines.withContext
  27. import java.io.OutputStreamWriter
  28. import kotlin.coroutines.resume
  29. import kotlin.coroutines.suspendCoroutine
  30. class LogcatActivity : BaseActivity<LogcatDesign>() {
  31. private var conn: ServiceConnection? = null
  32. override suspend fun main() {
  33. val fileName = intent?.fileName
  34. if (fileName != null) {
  35. val file = LogFile.parseFromFileName(fileName) ?: return showInvalid()
  36. return mainLocalFile(file)
  37. }
  38. return mainStreaming()
  39. }
  40. private suspend fun mainLocalFile(file: LogFile) {
  41. val messages = try {
  42. LogcatReader(this, file).readAll()
  43. } catch (e: Exception) {
  44. Log.e("Fail to read log file ${file.fileName}: ${e.message}")
  45. return showInvalid()
  46. }
  47. val design = LogcatDesign(this, false)
  48. setContentDesign(design)
  49. design.patchMessages(messages, 0, messages.size)
  50. while (isActive) {
  51. when (design.requests.receive()) {
  52. LogcatDesign.Request.Delete -> {
  53. withContext(Dispatchers.IO) {
  54. logsDir.resolve(file.fileName).delete()
  55. }
  56. finish()
  57. }
  58. LogcatDesign.Request.Export -> {
  59. val output = startActivityForResult(
  60. ActivityResultContracts.CreateDocument("text/plain"),
  61. file.fileName
  62. )
  63. if (output != null) {
  64. try {
  65. withContext(Dispatchers.IO) {
  66. writeLogTo(messages, file, output)
  67. }
  68. design.showToast(R.string.file_exported, ToastDuration.Long)
  69. } catch (e: Exception) {
  70. design.showExceptionToast(e)
  71. }
  72. }
  73. }
  74. else -> Unit
  75. }
  76. }
  77. }
  78. private suspend fun mainStreaming() {
  79. val design = LogcatDesign(this, true)
  80. setContentDesign(design)
  81. startForegroundServiceCompat(LogcatService::class.intent)
  82. val logcat = bindLogcatService()
  83. val ticker = ticker(500)
  84. var initial = true
  85. while (isActive) {
  86. select<Unit> {
  87. events.onReceive {
  88. }
  89. design.requests.onReceive {
  90. when (it) {
  91. LogcatDesign.Request.Close -> {
  92. stopService(LogcatService::class.intent)
  93. finish()
  94. }
  95. else -> Unit
  96. }
  97. }
  98. if (activityStarted) {
  99. ticker.onReceive {
  100. val snapshot = logcat.snapshot(initial) ?: return@onReceive
  101. design.patchMessages(snapshot.messages, snapshot.removed, snapshot.appended)
  102. initial = false
  103. }
  104. }
  105. }
  106. }
  107. }
  108. override fun onDestroy() {
  109. conn?.apply(this::unbindService)
  110. super.onDestroy()
  111. }
  112. private suspend fun bindLogcatService(): LogcatService {
  113. return suspendCoroutine { ctx ->
  114. bindService(LogcatService::class.intent, object : ServiceConnection {
  115. override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
  116. val srv = service!!.queryLocalInterface("") as LogcatService
  117. ctx.resume(srv)
  118. conn = this
  119. }
  120. override fun onServiceDisconnected(name: ComponentName?) {
  121. conn = null
  122. }
  123. }, Context.BIND_AUTO_CREATE)
  124. }
  125. }
  126. @Suppress("BlockingMethodInNonBlockingContext")
  127. private suspend fun writeLogTo(messages: List<LogMessage>, file: LogFile, uri: Uri) {
  128. LogcatFilter(OutputStreamWriter(contentResolver.openOutputStream(uri)), this).use {
  129. withContext(Dispatchers.Main) {
  130. withModelProgressBar {
  131. configure {
  132. isIndeterminate = true
  133. max = messages.size
  134. }
  135. withContext(Dispatchers.IO) {
  136. it.writeHeader(file.date)
  137. messages.forEachIndexed { idx, msg ->
  138. configure {
  139. isIndeterminate = false
  140. progress = idx
  141. }
  142. it.writeMessage(msg)
  143. }
  144. }
  145. }
  146. }
  147. }
  148. }
  149. private fun showInvalid() {
  150. Toast.makeText(this, R.string.invalid_log_file, Toast.LENGTH_LONG).show()
  151. }
  152. }