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_restore_summary">Import a previously created backup</string>
|
||||||
<string name="preference_crash_reporter">Crash reporter</string>
|
<string name="preference_crash_reporter">Crash reporter</string>
|
||||||
<string name="preference_crash_reporter_summary">Error and crash reports</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_export_log">Export log file</string>
|
||||||
<string name="preference_screen_search">Search</string>
|
<string name="preference_screen_search">Search</string>
|
||||||
<string name="preference_screen_search_summary">Configure the 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.filesearch.FileSearchSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.hiddenitems.HiddenItemsSettingsScreen
|
import de.mm20.launcher2.ui.settings.hiddenitems.HiddenItemsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.license.LicenseScreen
|
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.main.MainSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
|
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
|
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
|
||||||
@ -166,6 +167,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/debug/crashreporter") {
|
composable("settings/debug/crashreporter") {
|
||||||
CrashReporterScreen()
|
CrashReporterScreen()
|
||||||
}
|
}
|
||||||
|
composable("settings/debug/logs") {
|
||||||
|
LogScreen()
|
||||||
|
}
|
||||||
composable(
|
composable(
|
||||||
"settings/debug/crashreporter/report?fileName={fileName}",
|
"settings/debug/crashreporter/report?fileName={fileName}",
|
||||||
arguments = listOf(navArgument("fileName") {
|
arguments = listOf(navArgument("fileName") {
|
||||||
|
|||||||
@ -37,25 +37,10 @@ fun DebugSettingsScreen() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Preference(
|
Preference(
|
||||||
title = stringResource(R.string.preference_export_log),
|
title = stringResource(R.string.preference_logs),
|
||||||
|
summary = stringResource(R.string.preference_logs_summary),
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
navController?.navigate("settings/debug/logs")
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
PreferenceCategory(stringResource(R.string.preference_category_debug_tools)) {
|
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