diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 61e395d..e3c017b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -72,12 +72,14 @@ android {
"proguard-rules.pro",
)
signingConfig = signingConfigs.getByName(Constants.RELEASE)
+ resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"")
}
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
resValue("string", "app_name", "WG Tunnel - Debug")
isDebuggable = true
+ resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
}
create(Constants.PRERELEASE) {
@@ -85,6 +87,7 @@ android {
applicationIdSuffix = ".prerelease"
versionNameSuffix = "-pre"
resValue("string", "app_name", "WG Tunnel - Pre")
+ resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
}
create(Constants.NIGHTLY) {
@@ -92,6 +95,7 @@ android {
applicationIdSuffix = ".nightly"
versionNameSuffix = "-nightly"
resValue("string", "app_name", "WG Tunnel - Nightly")
+ resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
}
applicationVariants.all {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 736070b..35e6d72 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -98,6 +98,16 @@
android:launchMode="singleInstance"
android:theme="@android:style/Theme.NoDisplay" />
+
+
+
+
+ if (currentScrollPosition < lastScrollPosition && isAutoScrolling) {
+ isAutoScrolling = false
+ }
+ val visible = lazyColumnListState.layoutInfo.visibleItemsInfo
+ if (visible.isNotEmpty()) {
+ if (visible.last().index
+ == lazyColumnListState.layoutInfo.totalItemsCount - 1 && !isAutoScrolling
+ ) {
+ isAutoScrolling = true
+ }
+ }
+ lastScrollPosition = currentScrollPosition
+ }
+ }
+
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = {
- scope.launch {
- viewModel.saveLogsToFile().onSuccess {
- Toast.makeText(
- context,
- context.getString(R.string.logs_saved),
- Toast.LENGTH_SHORT,
- ).show()
- }
- }
+ viewModel.shareLogs(context)
},
shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary,
) {
- val icon = Icons.Filled.Save
+ val icon = Icons.Filled.Share
Icon(
imageVector = icon,
contentDescription = icon.name,
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt
index e619f11..17df5fe 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt
@@ -1,19 +1,25 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
+import android.content.Context
import androidx.compose.runtime.mutableStateListOf
+import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.zaneschepke.logcatter.LocalLogCollector
+import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
import com.zaneschepke.wireguardautotunnel.util.Constants
-import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.chunked
+import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Job
+import com.zaneschepke.wireguardautotunnel.R
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import timber.log.Timber
+import java.io.File
import java.time.Duration
import java.time.Instant
import javax.inject.Inject
@@ -22,8 +28,7 @@ import javax.inject.Inject
class LogsViewModel
@Inject
constructor(
- private val localLogCollector: LocalLogCollector,
- private val fileUtils: FileUtils,
+ private val localLogCollector: LogReader,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
) : ViewModel() {
@@ -44,13 +49,19 @@ constructor(
}
}
- suspend fun saveLogsToFile(): Result {
- val file =
- localLogCollector.getLogFile().getOrElse {
- return Result.failure(it)
- }
- val fileContent = fileUtils.readBytesFromFile(file)
- val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
- return fileUtils.saveByteArrayToDownloads(fileContent, fileName)
+ fun shareLogs(context: Context): Job = viewModelScope.launch(ioDispatcher) {
+ runCatching {
+ val sharePath = File(context.filesDir, "external_files")
+ if (sharePath.exists()) sharePath.delete()
+ sharePath.mkdir()
+ val file = File("${sharePath.path + "/" + Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip")
+ if (file.exists()) file.delete()
+ file.createNewFile()
+ localLogCollector.zipLogFiles(file.absolutePath)
+ val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), file)
+ context.launchShareFile(uri)
+ }.onFailure {
+ Timber.e(it)
+ }
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt
index 4c9add9..7089b8c 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt
@@ -26,6 +26,16 @@ fun Context.openWebUrl(url: String): Result {
}
}
+fun Context.launchShareFile(file: Uri) {
+ val shareIntent = Intent().apply {
+ setAction(Intent.ACTION_SEND)
+ setType("*/*")
+ putExtra(Intent.EXTRA_STREAM, file)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ this.startActivity(Intent.createChooser(shareIntent, ""))
+}
+
fun Context.showToast(resId: Int) {
Toast.makeText(
this,
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..be991f2
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt
similarity index 67%
rename from logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt
rename to logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt
index 83d17ec..56f44dd 100644
--- a/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt
+++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt
@@ -2,14 +2,12 @@ package com.zaneschepke.logcatter
import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.flow.Flow
-import java.io.File
-interface LocalLogCollector {
+interface LogReader {
suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null)
-
fun stop()
-
- suspend fun getLogFile(): Result
-
+ fun zipLogFiles(path: String)
+ suspend fun deleteAndClearLogs()
val bufferedLogs: Flow
+ val liveLogs: Flow
}
diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatCollector.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatCollector.kt
new file mode 100644
index 0000000..bbefde2
--- /dev/null
+++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatCollector.kt
@@ -0,0 +1,274 @@
+package com.zaneschepke.logcatter
+
+import android.content.Context
+import com.zaneschepke.logcatter.model.LogMessage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.BufferedOutputStream
+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.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+object LogcatCollector {
+
+ 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}[0-9A-Fa-f]{1,4}${'$'}""".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 val ioDispatcher = Dispatchers.IO
+
+ 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): LogReader {
+ 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 : LogReader {
+
+ private var logcatReader: LogcatReader? = null
+
+ private fun obfuscator(log: String): String {
+ return findKeyRegex.replace(log, "").let { first ->
+ findIpv6AddressRegex.replace(first, "").let { second ->
+ findTunnelNameRegex.replace(second, "")
+ }
+ }.let { last -> findIpv4AddressRegex.replace(last, "") }
+ }
+
+ override suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
+ logcatReader ?: run {
+ logcatReader = LogcatReader(LogcatHelperInit.pID.toString(), LogcatHelperInit.logcatPath, onLogMessage)
+ }
+ logcatReader?.run()
+ }
+
+ override fun stop() {
+ logcatReader?.stop()
+ logcatReader = null
+ }
+
+ override fun zipLogFiles(path: String) {
+ logcatReader?.pause()
+ zipAll(path)
+ logcatReader?.resume()
+ }
+
+ private fun zipAll(zipFilePath: String) {
+ val sourceFile = File(LogcatHelperInit.logcatPath)
+ val outputZipFile = File(zipFilePath)
+ ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
+ sourceFile.walkTopDown().forEach { file ->
+ val zipFileName = file.absolutePath.removePrefix(sourceFile.absolutePath).removePrefix("/")
+ val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
+ zos.putNextEntry(entry)
+ if (file.isFile) {
+ file.inputStream().use {
+ it.copyTo(zos)
+ }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override suspend fun deleteAndClearLogs() {
+ withContext(ioDispatcher) {
+ logcatReader?.pause()
+ _bufferedLogs.resetReplayCache()
+ logcatReader?.deleteAllFiles()
+ logcatReader?.resume()
+ }
+ }
+
+ private val _bufferedLogs = MutableSharedFlow(
+ replay = 10_000,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST,
+ )
+ private val _liveLogs = MutableSharedFlow(
+ replay = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST,
+ )
+
+ override val bufferedLogs: Flow = _bufferedLogs.asSharedFlow()
+
+ override val liveLogs: Flow = _liveLogs.asSharedFlow()
+
+ private class LogcatReader(
+ pID: String,
+ private val logcatPath: String,
+ private val callback: ((input: LogMessage) -> Unit)?,
+ ) {
+ private var logcatProc: Process? = null
+ private var reader: BufferedReader? = null
+
+ @get:Synchronized @set:Synchronized
+ private var paused = false
+
+ @get:Synchronized @set:Synchronized
+ private var stopped = false
+ 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 pause() {
+ paused = true
+ }
+ fun stop() {
+ stopped = true
+ }
+
+ fun resume() {
+ paused = false
+ }
+
+ fun clear() {
+ Runtime.getRuntime().exec(clearLogCommand)
+ }
+
+ suspend fun run() {
+ withContext(ioDispatcher) {
+ paused = false
+ stopped = false
+ if (outputStream == null) return@withContext
+ try {
+ clear()
+ logcatProc = Runtime.getRuntime().exec(command)
+ reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024)
+ var line: String? = null
+
+ while (!stopped) {
+ if (paused) continue
+ line = reader?.readLine()
+ if (line.isNullOrEmpty()) continue
+ outputStream?.let {
+ if (it.channel.size() >= LogcatHelperInit.maxFileSize) {
+ it.close()
+ outputStream = createNewLogFileStream()
+ }
+ if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) {
+ deleteOldestFile()
+ }
+ line.let { text ->
+ val obfuscated = obfuscator(text)
+ it.write((obfuscated + System.lineSeparator()).toByteArray())
+ try {
+ val logMessage = LogMessage.from(text)
+ _bufferedLogs.tryEmit(logMessage)
+ _liveLogs.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")
+ }
+
+ fun deleteOldestFile() {
+ val directory = File(logcatPath)
+ if (directory.isDirectory) {
+ directory.listFiles()?.toMutableList()?.run {
+ this.sortBy { it.lastModified() }
+ this.first().delete()
+ }
+ }
+ }
+
+ private fun createNewLogFileStream(): FileOutputStream {
+ return FileOutputStream(createLogFile(logcatPath))
+ }
+
+ fun deleteAllFiles() {
+ val directory = File(logcatPath)
+ directory.listFiles()?.toMutableList()?.run {
+ this.forEach { it.delete() }
+ }
+ outputStream = createNewLogFileStream()
+ }
+ }
+ }
+}
diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt
deleted file mode 100644
index 645b165..0000000
--- a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt
+++ /dev/null
@@ -1,274 +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 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 suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
- logcatReader ?: run {
- logcatReader =
- LogcatReader(
- LogcatHelperInit.pID.toString(),
- LogcatHelperInit.logcatPath,
- onLogMessage,
- )
- }
- logcatReader?.run()
- }
-
- 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 {
- 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(
- replay = 10_000,
- onBufferOverflow = BufferOverflow.DROP_OLDEST,
- )
-
- override val bufferedLogs: Flow = _bufferedLogs.asSharedFlow()
-
- private class LogcatReader(
- pID: String,
- private val logcatPath: String,
- private val callback: ((input: LogMessage) -> Unit)?,
- ) {
- 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, "").let { first ->
- findIpv6AddressRegex.replace(first, "").let { second ->
- findTunnelNameRegex.replace(second, "")
- }
- }.let { last -> findIpv4AddressRegex.replace(last, "") }
- }
-
- 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()
- }
- }
- }
- }
- }
-}