Add log viewer screen
This commit is contained in:
parent
6f6636e1dc
commit
6085841adc
@ -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>
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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)) {
|
||||
|
||||
173
ui/src/main/java/de/mm20/launcher2/ui/settings/log/LogScreen.kt
Normal file
173
ui/src/main/java/de/mm20/launcher2/ui/settings/log/LogScreen.kt
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user