mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
+36
-36
@@ -1,50 +1,50 @@
|
||||
plugins {
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.zaneschepke.logcatter"
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
namespace = "com.zaneschepke.logcatter"
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Constants.MIN_SDK
|
||||
defaultConfig {
|
||||
minSdk = Constants.MIN_SDK
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
|
||||
create(Constants.NIGHTLY) {
|
||||
initWith(getByName(Constants.RELEASE))
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Constants.JVM_TARGET
|
||||
}
|
||||
create(Constants.NIGHTLY) {
|
||||
initWith(getByName(Constants.RELEASE))
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Constants.JVM_TARGET
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
// logging
|
||||
implementation(libs.timber)
|
||||
// logging
|
||||
implementation(libs.timber)
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ import org.junit.runner.RunWith
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.zaneschepke.test", appContext.packageName)
|
||||
}
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.zaneschepke.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import kotlinx.coroutines.flow.Flow
|
||||
import java.io.File
|
||||
|
||||
interface LocalLogCollector {
|
||||
fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null)
|
||||
fun stop()
|
||||
suspend fun getLogFile(): Result<File>
|
||||
val bufferedLogs: Flow<LogMessage>
|
||||
}
|
||||
fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null)
|
||||
|
||||
fun stop()
|
||||
|
||||
suspend fun getLogFile(): Result<File>
|
||||
|
||||
val bufferedLogs: Flow<LogMessage>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.zaneschepke.logcatter
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.logcatter.model.LogLevel
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
object LogcatUtil {
|
||||
private const val MAX_FILE_SIZE = 2097152L // 2MB
|
||||
private const val MAX_FOLDER_SIZE = 10485760L // 10MB
|
||||
private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex()
|
||||
private val findIpv6AddressRegex =
|
||||
"""(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)
|
||||
|{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:)
|
||||
|{1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]
|
||||
|{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)
|
||||
|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9])
|
||||
|{0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1
|
||||
|{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))
|
||||
""".trimMargin().toRegex()
|
||||
private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex()
|
||||
private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex()
|
||||
private const val CHORE = "Choreographer"
|
||||
|
||||
private object LogcatHelperInit {
|
||||
var maxFileSize: Long = MAX_FILE_SIZE
|
||||
var maxFolderSize: Long = MAX_FOLDER_SIZE
|
||||
var pID: Int = 0
|
||||
var publicAppDirectory = ""
|
||||
var logcatPath = ""
|
||||
}
|
||||
|
||||
fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, context: Context): LocalLogCollector {
|
||||
if (maxFileSize > maxFolderSize) {
|
||||
throw IllegalStateException("maxFileSize must be less than maxFolderSize")
|
||||
}
|
||||
synchronized(LogcatHelperInit) {
|
||||
LogcatHelperInit.maxFileSize = maxFileSize
|
||||
LogcatHelperInit.maxFolderSize = maxFolderSize
|
||||
LogcatHelperInit.pID = android.os.Process.myPid()
|
||||
context.getExternalFilesDir(null)?.let {
|
||||
LogcatHelperInit.publicAppDirectory = it.absolutePath
|
||||
LogcatHelperInit.logcatPath =
|
||||
LogcatHelperInit.publicAppDirectory + File.separator + "logs"
|
||||
val logDirectory = File(LogcatHelperInit.logcatPath)
|
||||
if (!logDirectory.exists()) {
|
||||
logDirectory.mkdir()
|
||||
}
|
||||
}
|
||||
return Logcat
|
||||
}
|
||||
}
|
||||
|
||||
internal object Logcat : LocalLogCollector {
|
||||
private var logcatReader: LogcatReader? = null
|
||||
|
||||
override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
|
||||
logcatReader ?: run {
|
||||
logcatReader =
|
||||
LogcatReader(
|
||||
LogcatHelperInit.pID.toString(),
|
||||
LogcatHelperInit.logcatPath,
|
||||
onLogMessage,
|
||||
)
|
||||
}
|
||||
logcatReader?.let { logReader ->
|
||||
if (!logReader.isAlive) logReader.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
logcatReader?.stopLogs()
|
||||
logcatReader = null
|
||||
}
|
||||
|
||||
private fun mergeLogsApi26(sourceDir: String, outputFile: File) {
|
||||
val outputFilePath = Paths.get(outputFile.absolutePath)
|
||||
val logcatPath = Paths.get(sourceDir)
|
||||
|
||||
Files.list(logcatPath).use {
|
||||
it.sorted { o1, o2 ->
|
||||
Files.getLastModifiedTime(o1).compareTo(Files.getLastModifiedTime(o2))
|
||||
}
|
||||
.flatMap(Files::lines).use { lines ->
|
||||
lines.forEach { line ->
|
||||
Files.write(
|
||||
outputFilePath,
|
||||
(line + System.lineSeparator()).toByteArray(),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLogFile(): Result<File> {
|
||||
stop()
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val outputDir =
|
||||
File(LogcatHelperInit.publicAppDirectory + File.separator + "output")
|
||||
val outputFile = File(outputDir.absolutePath + File.separator + "logs.txt")
|
||||
|
||||
if (!outputDir.exists()) outputDir.mkdir()
|
||||
if (outputFile.exists()) outputFile.delete()
|
||||
|
||||
mergeLogsApi26(LogcatHelperInit.logcatPath, outputFile)
|
||||
Result.success(outputFile)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
} finally {
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _bufferedLogs =
|
||||
MutableSharedFlow<LogMessage>(
|
||||
replay = 10_000,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
override val bufferedLogs: Flow<LogMessage> = _bufferedLogs.asSharedFlow()
|
||||
|
||||
private class LogcatReader(
|
||||
pID: String,
|
||||
private val logcatPath: String,
|
||||
private val callback: ((input: LogMessage) -> Unit)?,
|
||||
) : Thread() {
|
||||
private var logcatProc: Process? = null
|
||||
private var reader: BufferedReader? = null
|
||||
private var mRunning = true
|
||||
private var command = ""
|
||||
private var clearLogCommand = ""
|
||||
private var outputStream: FileOutputStream? = null
|
||||
|
||||
init {
|
||||
try {
|
||||
outputStream = FileOutputStream(createLogFile(logcatPath))
|
||||
} catch (e: FileNotFoundException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
|
||||
command = "logcat -v epoch | grep \"($pID)\""
|
||||
clearLogCommand = "logcat -c"
|
||||
}
|
||||
|
||||
fun stopLogs() {
|
||||
mRunning = false
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
Runtime.getRuntime().exec(clearLogCommand)
|
||||
}
|
||||
|
||||
private fun obfuscator(log: String): String {
|
||||
return findKeyRegex.replace(log, "<crypto-key>").let { first ->
|
||||
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
|
||||
findTunnelNameRegex.replace(second, "<tunnel>")
|
||||
}
|
||||
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (outputStream == null) return
|
||||
try {
|
||||
clear()
|
||||
logcatProc = Runtime.getRuntime().exec(command)
|
||||
reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024)
|
||||
var line: String? = null
|
||||
|
||||
while (mRunning && run {
|
||||
line = reader!!.readLine()
|
||||
line
|
||||
} != null
|
||||
) {
|
||||
if (!mRunning) {
|
||||
break
|
||||
}
|
||||
if (line!!.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (outputStream!!.channel.size() >= LogcatHelperInit.maxFileSize) {
|
||||
outputStream!!.close()
|
||||
outputStream = FileOutputStream(createLogFile(logcatPath))
|
||||
}
|
||||
if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) {
|
||||
deleteOldestFile(logcatPath)
|
||||
}
|
||||
line?.let { text ->
|
||||
val obfuscated = obfuscator(text)
|
||||
outputStream!!.write(
|
||||
(obfuscated + System.lineSeparator()).toByteArray(),
|
||||
)
|
||||
try {
|
||||
val logMessage = LogMessage.from(obfuscated)
|
||||
when (logMessage.level) {
|
||||
LogLevel.VERBOSE -> Unit
|
||||
else -> {
|
||||
if (!logMessage.tag.contains(CHORE)) {
|
||||
_bufferedLogs.tryEmit(logMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
callback?.let {
|
||||
it(logMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
logcatProc?.destroy()
|
||||
logcatProc = null
|
||||
|
||||
try {
|
||||
reader?.close()
|
||||
outputStream?.close()
|
||||
reader = null
|
||||
outputStream = null
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFolderSize(path: String): Long {
|
||||
File(path).run {
|
||||
var size = 0L
|
||||
if (this.isDirectory && this.listFiles() != null) {
|
||||
for (file in this.listFiles()!!) {
|
||||
size += getFolderSize(file.absolutePath)
|
||||
}
|
||||
} else {
|
||||
size = this.length()
|
||||
}
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLogFile(dir: String): File {
|
||||
return File(dir, "logcat_" + System.currentTimeMillis() + ".txt")
|
||||
}
|
||||
|
||||
private fun deleteOldestFile(path: String) {
|
||||
val directory = File(path)
|
||||
if (directory.isDirectory) {
|
||||
directory.listFiles()?.toMutableList()?.run {
|
||||
this.sortBy { it.lastModified() }
|
||||
this.first().delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
package com.zaneschepke.logcatter
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.logcatter.model.LogLevel
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
object LogcatHelper {
|
||||
|
||||
private const val MAX_FILE_SIZE = 2097152L // 2MB
|
||||
private const val MAX_FOLDER_SIZE = 10485760L // 10MB
|
||||
private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex()
|
||||
private val findIpv6AddressRegex =
|
||||
"""(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))""".toRegex()
|
||||
private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex()
|
||||
private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex()
|
||||
private const val CHORE = "Choreographer"
|
||||
|
||||
private object LogcatHelperInit {
|
||||
var maxFileSize: Long = MAX_FILE_SIZE
|
||||
var maxFolderSize: Long = MAX_FOLDER_SIZE
|
||||
var pID: Int = 0
|
||||
var publicAppDirectory = ""
|
||||
var logcatPath = ""
|
||||
}
|
||||
|
||||
fun init(
|
||||
maxFileSize: Long = MAX_FILE_SIZE,
|
||||
maxFolderSize: Long = MAX_FOLDER_SIZE,
|
||||
context: Context
|
||||
): LocalLogCollector {
|
||||
if (maxFileSize > maxFolderSize) {
|
||||
throw IllegalStateException("maxFileSize must be less than maxFolderSize")
|
||||
}
|
||||
synchronized(LogcatHelperInit) {
|
||||
LogcatHelperInit.maxFileSize = maxFileSize
|
||||
LogcatHelperInit.maxFolderSize = maxFolderSize
|
||||
LogcatHelperInit.pID = android.os.Process.myPid()
|
||||
context.getExternalFilesDir(null)?.let {
|
||||
LogcatHelperInit.publicAppDirectory = it.absolutePath
|
||||
LogcatHelperInit.logcatPath =
|
||||
LogcatHelperInit.publicAppDirectory + File.separator + "logs"
|
||||
val logDirectory = File(LogcatHelperInit.logcatPath)
|
||||
if (!logDirectory.exists()) {
|
||||
logDirectory.mkdir()
|
||||
}
|
||||
}
|
||||
return Logcat
|
||||
}
|
||||
}
|
||||
|
||||
internal object Logcat : LocalLogCollector {
|
||||
|
||||
private var logcatReader: LogcatReader? = null
|
||||
|
||||
override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
|
||||
logcatReader ?: run {
|
||||
logcatReader = LogcatReader(
|
||||
LogcatHelperInit.pID.toString(),
|
||||
LogcatHelperInit.logcatPath,
|
||||
onLogMessage,
|
||||
)
|
||||
}
|
||||
logcatReader?.let { logReader ->
|
||||
if (!logReader.isAlive) logReader.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
logcatReader?.stopLogs()
|
||||
logcatReader = null
|
||||
}
|
||||
|
||||
private fun mergeLogsApi26(sourceDir: String, outputFile: File) {
|
||||
val outputFilePath = Paths.get(outputFile.absolutePath)
|
||||
val logcatPath = Paths.get(sourceDir)
|
||||
|
||||
Files.list(logcatPath).use {
|
||||
it.sorted { o1, o2 ->
|
||||
Files.getLastModifiedTime(o1).compareTo(Files.getLastModifiedTime(o2))
|
||||
}
|
||||
.flatMap(Files::lines).use { lines ->
|
||||
lines.forEach { line ->
|
||||
Files.write(
|
||||
outputFilePath,
|
||||
(line + System.lineSeparator()).toByteArray(),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLogFile(): Result<File> {
|
||||
stop()
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val outputDir =
|
||||
File(LogcatHelperInit.publicAppDirectory + File.separator + "output")
|
||||
val outputFile = File(outputDir.absolutePath + File.separator + "logs.txt")
|
||||
|
||||
if (!outputDir.exists()) outputDir.mkdir()
|
||||
if (outputFile.exists()) outputFile.delete()
|
||||
|
||||
mergeLogsApi26(LogcatHelperInit.logcatPath, outputFile)
|
||||
Result.success(outputFile)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
} finally {
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _bufferedLogs = MutableSharedFlow<LogMessage>(
|
||||
replay = 10_000,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
override val bufferedLogs: Flow<LogMessage> = _bufferedLogs.asSharedFlow()
|
||||
|
||||
private class LogcatReader(
|
||||
pID: String,
|
||||
private val logcatPath: String,
|
||||
private val callback: ((input: LogMessage) -> Unit)?,
|
||||
) : Thread() {
|
||||
private var logcatProc: Process? = null
|
||||
private var reader: BufferedReader? = null
|
||||
private var mRunning = true
|
||||
private var command = ""
|
||||
private var clearLogCommand = ""
|
||||
private var outputStream: FileOutputStream? = null
|
||||
|
||||
init {
|
||||
try {
|
||||
outputStream = FileOutputStream(createLogFile(logcatPath))
|
||||
} catch (e: FileNotFoundException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
|
||||
command = "logcat -v epoch | grep \"($pID)\""
|
||||
clearLogCommand = "logcat -c"
|
||||
}
|
||||
|
||||
fun stopLogs() {
|
||||
mRunning = false
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
Runtime.getRuntime().exec(clearLogCommand)
|
||||
}
|
||||
|
||||
private fun obfuscator(log: String): String {
|
||||
return findKeyRegex.replace(log, "<crypto-key>").let { first ->
|
||||
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
|
||||
findTunnelNameRegex.replace(second, "<tunnel>")
|
||||
}
|
||||
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (outputStream == null) return
|
||||
try {
|
||||
clear()
|
||||
logcatProc = Runtime.getRuntime().exec(command)
|
||||
reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024)
|
||||
var line: String? = null
|
||||
|
||||
while (mRunning && run {
|
||||
line = reader!!.readLine()
|
||||
line
|
||||
} != null
|
||||
) {
|
||||
if (!mRunning) {
|
||||
break
|
||||
}
|
||||
if (line!!.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (outputStream!!.channel.size() >= LogcatHelperInit.maxFileSize) {
|
||||
outputStream!!.close()
|
||||
outputStream = FileOutputStream(createLogFile(logcatPath))
|
||||
}
|
||||
if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) {
|
||||
deleteOldestFile(logcatPath)
|
||||
}
|
||||
line?.let { text ->
|
||||
val obfuscated = obfuscator(text)
|
||||
outputStream!!.write((obfuscated + System.lineSeparator()).toByteArray())
|
||||
try {
|
||||
val logMessage = LogMessage.from(obfuscated)
|
||||
when (logMessage.level) {
|
||||
LogLevel.VERBOSE -> Unit
|
||||
else -> {
|
||||
if (!logMessage.tag.contains(CHORE)) {
|
||||
_bufferedLogs.tryEmit(logMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
callback?.let {
|
||||
it(logMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
logcatProc?.destroy()
|
||||
logcatProc = null
|
||||
|
||||
try {
|
||||
reader?.close()
|
||||
outputStream?.close()
|
||||
reader = null
|
||||
outputStream = null
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFolderSize(path: String): Long {
|
||||
File(path).run {
|
||||
var size = 0L
|
||||
if (this.isDirectory && this.listFiles() != null) {
|
||||
for (file in this.listFiles()!!) {
|
||||
size += getFolderSize(file.absolutePath)
|
||||
}
|
||||
} else {
|
||||
size = this.length()
|
||||
}
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLogFile(dir: String): File {
|
||||
return File(dir, "logcat_" + System.currentTimeMillis() + ".txt")
|
||||
}
|
||||
|
||||
private fun deleteOldestFile(path: String) {
|
||||
val directory = File(path)
|
||||
if (directory.isDirectory) {
|
||||
directory.listFiles()?.toMutableList()?.run {
|
||||
this.sortBy { it.lastModified() }
|
||||
this.first().delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,50 @@
|
||||
package com.zaneschepke.logcatter.model
|
||||
|
||||
enum class LogLevel(val signifier: String) {
|
||||
DEBUG("D") {
|
||||
override fun color(): Long {
|
||||
return 0xFF2196F3
|
||||
}
|
||||
},
|
||||
INFO("I") {
|
||||
override fun color(): Long {
|
||||
return 0xFF4CAF50
|
||||
}
|
||||
},
|
||||
ASSERT("A") {
|
||||
override fun color(): Long {
|
||||
return 0xFF9C27B0
|
||||
}
|
||||
},
|
||||
WARNING("W") {
|
||||
override fun color(): Long {
|
||||
return 0xFFFFC107
|
||||
}
|
||||
},
|
||||
ERROR("E") {
|
||||
override fun color(): Long {
|
||||
return 0xFFF44336
|
||||
}
|
||||
},
|
||||
VERBOSE("V") {
|
||||
override fun color(): Long {
|
||||
return 0xFF000000
|
||||
}
|
||||
};
|
||||
DEBUG("D") {
|
||||
override fun color(): Long {
|
||||
return 0xFF2196F3
|
||||
}
|
||||
},
|
||||
INFO("I") {
|
||||
override fun color(): Long {
|
||||
return 0xFF4CAF50
|
||||
}
|
||||
},
|
||||
ASSERT("A") {
|
||||
override fun color(): Long {
|
||||
return 0xFF9C27B0
|
||||
}
|
||||
},
|
||||
WARNING("W") {
|
||||
override fun color(): Long {
|
||||
return 0xFFFFC107
|
||||
}
|
||||
},
|
||||
ERROR("E") {
|
||||
override fun color(): Long {
|
||||
return 0xFFF44336
|
||||
}
|
||||
},
|
||||
VERBOSE("V") {
|
||||
override fun color(): Long {
|
||||
return 0xFF000000
|
||||
}
|
||||
}, ;
|
||||
|
||||
abstract fun color(): Long
|
||||
abstract fun color(): Long
|
||||
|
||||
companion object {
|
||||
fun fromSignifier(signifier: String): LogLevel {
|
||||
return when (signifier) {
|
||||
DEBUG.signifier -> DEBUG
|
||||
INFO.signifier -> INFO
|
||||
WARNING.signifier -> WARNING
|
||||
VERBOSE.signifier -> VERBOSE
|
||||
ASSERT.signifier -> ASSERT
|
||||
ERROR.signifier -> ERROR
|
||||
else -> VERBOSE
|
||||
}
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
fun fromSignifier(signifier: String): LogLevel {
|
||||
return when (signifier) {
|
||||
DEBUG.signifier -> DEBUG
|
||||
INFO.signifier -> INFO
|
||||
WARNING.signifier -> WARNING
|
||||
VERBOSE.signifier -> VERBOSE
|
||||
ASSERT.signifier -> ASSERT
|
||||
ERROR.signifier -> ERROR
|
||||
else -> VERBOSE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,42 +3,42 @@ package com.zaneschepke.logcatter.model
|
||||
import java.time.Instant
|
||||
|
||||
data class LogMessage(
|
||||
val time: String,
|
||||
val pid: String,
|
||||
val tid: String,
|
||||
val level: LogLevel,
|
||||
val tag: String,
|
||||
val message: String,
|
||||
val time: String,
|
||||
val pid: String,
|
||||
val tid: String,
|
||||
val level: LogLevel,
|
||||
val tag: String,
|
||||
val message: String,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "$time $pid $tid $level $tag message= $message"
|
||||
}
|
||||
override fun toString(): String {
|
||||
return "$time $pid $tid $level $tag message= $message"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(logcatLine: String): LogMessage {
|
||||
return if (logcatLine.contains("---------")) {
|
||||
LogMessage(
|
||||
Instant.now().toString(),
|
||||
"0",
|
||||
"0",
|
||||
LogLevel.VERBOSE,
|
||||
"System",
|
||||
logcatLine,
|
||||
)
|
||||
} else {
|
||||
// TODO improve this
|
||||
val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() }
|
||||
val epochParts = parts[0].split(".").map { it.toLong() }
|
||||
val message = parts.subList(5, parts.size).joinToString(" ")
|
||||
LogMessage(
|
||||
Instant.ofEpochSecond(epochParts[0], epochParts[1]).toString(),
|
||||
parts[1],
|
||||
parts[2],
|
||||
LogLevel.fromSignifier(parts[3]),
|
||||
parts[4],
|
||||
message,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
fun from(logcatLine: String): LogMessage {
|
||||
return if (logcatLine.contains("---------")) {
|
||||
LogMessage(
|
||||
Instant.now().toString(),
|
||||
"0",
|
||||
"0",
|
||||
LogLevel.VERBOSE,
|
||||
"System",
|
||||
logcatLine,
|
||||
)
|
||||
} else {
|
||||
// TODO improve this
|
||||
val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() }
|
||||
val epochParts = parts[0].split(".").map { it.toLong() }
|
||||
val message = parts.subList(5, parts.size).joinToString(" ")
|
||||
LogMessage(
|
||||
Instant.ofEpochSecond(epochParts[0], epochParts[1]).toString(),
|
||||
parts[1],
|
||||
parts[2],
|
||||
LogLevel.fromSignifier(parts[3]),
|
||||
parts[4],
|
||||
message,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import org.junit.Test
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user