Add log viewer screen

This commit is contained in:
MM20 2022-11-19 20:12:44 +01:00
parent 6f6636e1dc
commit 6085841adc
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
4 changed files with 182 additions and 18 deletions

View File

@ -570,6 +570,8 @@
<string name="preference_restore_summary">Import a previously created backup</string>
<string name="preference_crash_reporter">Crash reporter</string>
<string name="preference_crash_reporter_summary">Error and crash reports</string>
<string name="preference_logs">Logs</string>
<string name="preference_logs_summary">View and export application logs</string>
<string name="preference_export_log">Export log file</string>
<string name="preference_screen_search">Search</string>
<string name="preference_screen_search_summary">Configure the search</string>

View File

@ -41,6 +41,7 @@ import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsScreen
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
import de.mm20.launcher2.ui.settings.hiddenitems.HiddenItemsSettingsScreen
import de.mm20.launcher2.ui.settings.license.LicenseScreen
import de.mm20.launcher2.ui.settings.log.LogScreen
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
@ -166,6 +167,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/debug/crashreporter") {
CrashReporterScreen()
}
composable("settings/debug/logs") {
LogScreen()
}
composable(
"settings/debug/crashreporter/report?fileName={fileName}",
arguments = listOf(navArgument("fileName") {

View File

@ -37,25 +37,10 @@ fun DebugSettingsScreen() {
})
Preference(
title = stringResource(R.string.preference_export_log),
title = stringResource(R.string.preference_logs),
summary = stringResource(R.string.preference_logs_summary),
onClick = {
scope.launch {
val path = DebugInformationDumper().dump(context)
context.tryStartActivity(
Intent.createChooser(
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
File(path)
)
)
}, null
)
)
}
navController?.navigate("settings/debug/logs")
})
}
PreferenceCategory(stringResource(R.string.preference_category_debug_tools)) {

View File

@ -0,0 +1,173 @@
package de.mm20.launcher2.ui.settings.log
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.Error
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import de.mm20.launcher2.debug.DebugInformationDumper
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.util.regex.Pattern
@Composable
fun LogScreen() {
var lines by remember { mutableStateOf(emptyList<LogcatLine>()) }
LaunchedEffect(null) {
val process = Runtime.getRuntime().exec("/system/bin/logcat -v time")
val pattern = Pattern.compile(
"^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" + /* timestamp [1] */
"(\\w)/(.+?)\\(\\s*(\\d+)\\): (.*)$") /* level, tag, pid, msg [2-5] */
launch(Dispatchers.IO) {
val inputStream = process.inputStream.bufferedReader()
while (isActive) {
val line = try {
val line = inputStream.readLine()
val matcher = pattern.matcher(line)
if (matcher.matches()) {
FormattedLogcatLine(
message = matcher.group(5) ?: "",
tag = matcher.group(3) ?: "",
level = matcher.group(2) ?: "",
timestamp = matcher.group(1) ?: "<no date>",
)
} else {
RawLogcatLine(line)
}
} catch (e: IOException) {
break
}
lines = (lines + line)
}
}
try {
awaitCancellation()
} finally {
process.destroy()
}
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
PreferenceScreen(
title = stringResource(id = R.string.preference_logs),
topBarActions = {
IconButton(onClick = {
scope.launch {
val path = DebugInformationDumper().dump(context)
context.tryStartActivity(
Intent.createChooser(
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
File(path)
)
)
}, null
)
)
}
}) {
Icon(Icons.Rounded.Share, contentDescription = null)
}
}
) {
items(lines) {
if (it is RawLogcatLine) {
Text(modifier = Modifier.padding(16.dp), text = it.line ?: "", style = MaterialTheme.typography.bodySmall)
} else if (it is FormattedLogcatLine) {
val contentColor = when(it.level) {
"E" -> MaterialTheme.colorScheme.onErrorContainer
"W" -> MaterialTheme.colorScheme.onPrimaryContainer
"D" -> MaterialTheme.colorScheme.onSurfaceVariant
else -> MaterialTheme.colorScheme.onSurface
}
val bgColor = when(it.level) {
"E" -> MaterialTheme.colorScheme.errorContainer
"W" -> MaterialTheme.colorScheme.primaryContainer
"D" -> MaterialTheme.colorScheme.surfaceVariant
else -> MaterialTheme.colorScheme.surface
}
Column(modifier = Modifier
.fillMaxWidth()
.background(bgColor)
.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
when(it.level) {
"E" -> Icons.Rounded.Error
"W" -> Icons.Rounded.Warning
"D" -> Icons.Rounded.BugReport
else -> Icons.Rounded.Info
},
null,
tint = contentColor
)
Text(
modifier = Modifier.padding(start = 8.dp),
text = it.timestamp + "" + it.tag,
style = MaterialTheme.typography.labelSmall,
color = contentColor
)
}
Text(
modifier = Modifier.padding(top = 8.dp),
text = it.message,
style = MaterialTheme.typography.bodySmall,
color = contentColor
)
}
}
}
}
}
sealed interface LogcatLine
data class FormattedLogcatLine(
val level: String,
val timestamp: String,
val tag: String,
val message: String,
) : LogcatLine
data class RawLogcatLine(val line: String): LogcatLine