mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-21 00:18:18 +00:00
add in app log viewer
This commit is contained in:
8 files changed
+591
-16
No files matched your search
+6
-5
@@ -75,10 +75,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation platform('androidx.compose:compose-bom:2023.10.01')
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation "androidx.compose.ui:ui:$compose_version"
|
||||
implementation 'androidx.compose.material3:material3:1.1.2'
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation "androidx.compose.ui:ui"
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
|
||||
implementation 'androidx.activity:activity-compose:1.8.2'
|
||||
@@ -90,7 +90,8 @@ dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.0"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling"
|
||||
debugImplementation "androidx.compose.ui:ui-test-manifest"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling-preview"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_LOGS" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.wifi"
|
||||
@@ -28,6 +27,11 @@
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.GeodeLauncher"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".ApplicationLogsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_application_logs"
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:exported="false"
|
||||
@@ -58,11 +62,12 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:authorities="com.geode.launcher.user"
|
||||
android:name=".UserDirectoryProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:authorities="com.geode.launcher.user"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
@@ -78,4 +83,5 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,275 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.geode.launcher.log.LogLine
|
||||
import com.geode.launcher.log.LogPriority
|
||||
import com.geode.launcher.log.LogViewModel
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ApplicationLogsActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
ApplicationLogsScreen(onBackPressedDispatcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getLogColor(priority: LogPriority): Color {
|
||||
val isDark = LocalTheme.current != Theme.LIGHT
|
||||
|
||||
// i just copied these from my ide tbh
|
||||
return if (isDark) {
|
||||
when (priority) {
|
||||
LogPriority.ERROR -> Color(0xffdf5151)
|
||||
LogPriority.WARN -> Color(0xffbcb500)
|
||||
LogPriority.FATAL -> Color(0xffb8b6fc)
|
||||
else -> Color.Unspecified
|
||||
}
|
||||
} else {
|
||||
when (priority) {
|
||||
LogPriority.ERROR -> Color(0xffdf5151)
|
||||
LogPriority.WARN -> Color(0xffeab700)
|
||||
LogPriority.FATAL -> Color(0xffa142f4)
|
||||
else -> Color.Unspecified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun copyLogText(context: Context, logMessage: String) {
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
context.getString(R.string.application_log_copy_label),
|
||||
logMessage
|
||||
)
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.text_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(8.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
copyLogText(context, logLine.asSimpleString)
|
||||
}
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
|
||||
style = Typography.labelSmall,
|
||||
color = getLogColor(logLine.priority),
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
Text(
|
||||
"[${logLine.tag}]: ${logLine.message}",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ApplicationLogsScreen(
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||
logViewModel: LogViewModel = viewModel()
|
||||
) {
|
||||
val logLines = logViewModel.lineState
|
||||
val isLoading by logViewModel.isLoading.collectAsState()
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(logLines.size) {
|
||||
listState.scrollToItem(logLines.size)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
if (!logLines.isEmpty()) {
|
||||
coroutineScope.launch {
|
||||
val data = logViewModel.getLogData()
|
||||
|
||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, data)
|
||||
type = "text/plain"
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
|
||||
context.startActivity(shareIntent)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = stringResource(R.string.application_logs_share)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
logViewModel.clearLogs()
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_delete),
|
||||
contentDescription = stringResource(R.string.application_logs_delete)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.title_activity_application_logs),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
state = listState
|
||||
) {
|
||||
if (!isLoading && logLines.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.application_logs_no_logs),
|
||||
textAlign = TextAlign.Center,
|
||||
style = Typography.bodyLarge,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
logLines
|
||||
) { line ->
|
||||
LogCard(line)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun LogsPreview() {
|
||||
GeodeLauncherTheme {
|
||||
ApplicationLogsScreen(null)
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ fun onOpenFolder(context: Context) {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.export_folder_copied),
|
||||
context.getString(R.string.text_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
@@ -224,6 +224,11 @@ fun updateTheme(context: Context, theme: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenLogs(context: Context) {
|
||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
@@ -267,7 +272,7 @@ fun SettingsScreen(
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = context.getString(R.string.settings_back_icon_alt)
|
||||
contentDescription = context.getString(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -302,10 +307,6 @@ fun SettingsScreen(
|
||||
title = stringResource(R.string.preference_black_background_name),
|
||||
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
||||
)
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_load_testing_name),
|
||||
preferenceKey = PreferenceUtils.Key.LOAD_TESTING,
|
||||
)
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_load_automatically_name),
|
||||
description = context.getString(R.string.preference_load_automatically_description),
|
||||
@@ -320,6 +321,10 @@ fun SettingsScreen(
|
||||
title = stringResource(R.string.preferences_open_file_manager),
|
||||
onClick = { onOpenFileManager(context) }
|
||||
)
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_view_logs),
|
||||
onClick = { onOpenLogs(context) }
|
||||
)
|
||||
}
|
||||
|
||||
OptionsGroup(context.getString(R.string.preference_category_updater)) {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package com.geode.launcher.log
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toJavaInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.io.DataInputStream
|
||||
import java.io.IOException
|
||||
|
||||
fun DataInputStream.readCChar(): Char {
|
||||
return this.readByte().toInt().toChar()
|
||||
}
|
||||
|
||||
fun DataInputStream.readCString(): String {
|
||||
val buffer = StringBuilder()
|
||||
|
||||
var lastByte = this.readCChar()
|
||||
while (lastByte != '\u0000') {
|
||||
buffer.append(lastByte)
|
||||
lastByte = this.readCChar()
|
||||
}
|
||||
|
||||
return buffer.toString()
|
||||
}
|
||||
|
||||
fun DataInputStream.readLeUShort(): UShort {
|
||||
val byteA = this.readByte().toInt() and 0xff
|
||||
val byteB = this.readByte().toInt() and 0xff
|
||||
|
||||
val shortValue = (byteB shl 8) or byteA
|
||||
return shortValue.toUShort()
|
||||
}
|
||||
|
||||
fun DataInputStream.readLeInt(): Int {
|
||||
val byteA = this.readByte().toInt() and 0xff
|
||||
val byteB = this.readByte().toInt() and 0xff
|
||||
val byteC = this.readByte().toInt() and 0xff
|
||||
val byteD = this.readByte().toInt() and 0xff
|
||||
|
||||
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
|
||||
}
|
||||
|
||||
fun DataInputStream.readLeUInt(): UInt {
|
||||
val byteA = this.readByte().toUInt() and 0xffu
|
||||
val byteB = this.readByte().toUInt() and 0xffu
|
||||
val byteC = this.readByte().toUInt() and 0xffu
|
||||
val byteD = this.readByte().toUInt() and 0xffu
|
||||
|
||||
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
|
||||
}
|
||||
|
||||
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
|
||||
|
||||
enum class LogPriority {
|
||||
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
|
||||
|
||||
companion object {
|
||||
fun fromByte(byte: Byte): LogPriority {
|
||||
return when (byte.toInt()) {
|
||||
0x1 -> DEFAULT
|
||||
0x2 -> VERBOSE
|
||||
0x3 -> DEBUG
|
||||
0x4 -> INFO
|
||||
0x5 -> WARN
|
||||
0x6 -> ERROR
|
||||
0x7 -> FATAL
|
||||
0x8 -> SILENT
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toChar(): Char {
|
||||
return when (this) {
|
||||
VERBOSE -> 'V'
|
||||
DEBUG -> 'D'
|
||||
INFO -> 'I'
|
||||
WARN -> 'W'
|
||||
ERROR -> 'E'
|
||||
FATAL -> 'F'
|
||||
SILENT -> 'S'
|
||||
else -> '?'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a log entry from logcat.
|
||||
*/
|
||||
data class LogLine(
|
||||
val process: ProcessInformation,
|
||||
val time: Instant,
|
||||
val logId: Int,
|
||||
val priority: LogPriority,
|
||||
val tag: String,
|
||||
val message: String
|
||||
) {
|
||||
companion object {
|
||||
private enum class EntryVersion {
|
||||
V3, V4
|
||||
}
|
||||
|
||||
private fun headerSizeToVersion(size: UShort) = when (size.toInt()) {
|
||||
0x18 -> EntryVersion.V3
|
||||
0x1c -> EntryVersion.V4
|
||||
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
|
||||
}
|
||||
|
||||
fun fromInputStream(readStream: DataInputStream): LogLine {
|
||||
/*
|
||||
// from android <liblog/include/log/log_read.h>
|
||||
// there are multiple logger entry formats
|
||||
// use the header_size to determine the one you have
|
||||
|
||||
struct logger_entry_v3 {
|
||||
uint16_t len; // length of the payload
|
||||
uint16_t hdr_size; // sizeof(struct logger_entry_v3)
|
||||
int32_t pid; // generating process's pid
|
||||
int32_t tid; // generating process's tid
|
||||
int32_t sec; // seconds since Epoch
|
||||
int32_t nsec; // nanoseconds
|
||||
uint32_t lid; // log id of the payload
|
||||
}
|
||||
|
||||
struct logger_entry_v4 {
|
||||
uint16_t len; // length of the payload
|
||||
uint16_t hdr_size; // sizeof(struct logger_entry_v4 = 28)
|
||||
int32_t pid; // generating process's pid
|
||||
uint32_t tid; // generating process's tid
|
||||
uint32_t sec; // seconds since Epoch
|
||||
uint32_t nsec; // nanoseconds
|
||||
uint32_t lid; // log id of the payload, bottom 4 bits currently
|
||||
uint32_t uid; // generating process's uid
|
||||
};
|
||||
*/
|
||||
|
||||
/* val payloadLength = */ readStream.readLeUShort()
|
||||
val headerSize = readStream.readLeUShort()
|
||||
|
||||
val entryVersion = headerSizeToVersion(headerSize)
|
||||
|
||||
val pid = readStream.readLeInt()
|
||||
val tid = readStream.readLeUInt().toInt()
|
||||
val sec = readStream.readLeUInt().toLong()
|
||||
val nSec = readStream.readLeUInt().toInt()
|
||||
val lid = readStream.readLeUInt().toInt()
|
||||
val uid = if (entryVersion == EntryVersion.V4)
|
||||
readStream.readLeUInt().toInt() else 0
|
||||
|
||||
val processInformation = ProcessInformation(pid, tid, uid)
|
||||
val time = Instant.fromEpochSeconds(sec, nSec)
|
||||
|
||||
val priorityByte = readStream.readByte()
|
||||
val priority = LogPriority.fromByte(priorityByte)
|
||||
|
||||
val tag = readStream.readCString()
|
||||
val message = readStream.readCString()
|
||||
|
||||
return LogLine(
|
||||
process = processInformation,
|
||||
priority = priority,
|
||||
time = time,
|
||||
logId = lid,
|
||||
tag = tag,
|
||||
message = message
|
||||
)
|
||||
}
|
||||
|
||||
fun showException(exception: Exception) = LogLine(
|
||||
process = ProcessInformation(0, 0, 0),
|
||||
time = Clock.System.now(),
|
||||
logId = 0,
|
||||
priority = LogPriority.FATAL,
|
||||
tag = "GeodeLauncher",
|
||||
message = "Failed to parse log entry with ${exception.stackTraceToString()}"
|
||||
)
|
||||
}
|
||||
|
||||
val identifier = time.toJavaInstant()
|
||||
|
||||
val formattedTime = this.time.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
val asSimpleString = "$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.message}"
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.geode.launcher.log
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import java.io.DataInputStream
|
||||
import java.io.EOFException
|
||||
import java.util.LinkedList
|
||||
|
||||
/**
|
||||
* ViewModel to manage log output from logcat.
|
||||
* Currently, it loads the list of logs at init with no ability to read additional logs.
|
||||
*/
|
||||
class LogViewModel: ViewModel() {
|
||||
// the original version of this code did support streaming btw
|
||||
// the states were way too annoying to manage across multiple threads
|
||||
// yay!
|
||||
|
||||
private val _lineState = mutableStateListOf<LogLine>()
|
||||
val lineState = _lineState
|
||||
|
||||
private var _isLoading = MutableStateFlow(false)
|
||||
var isLoading = _isLoading.asStateFlow()
|
||||
|
||||
private suspend fun logOutput(): List<LogLine> {
|
||||
val logLines = LinkedList<LogLine>()
|
||||
|
||||
val logStream = DataInputStream(
|
||||
// -B = binary format, -d = dump logs
|
||||
Runtime.getRuntime().exec("logcat -B -d").inputStream
|
||||
)
|
||||
|
||||
try {
|
||||
coroutineScope {
|
||||
// this runs until the stream is exhausted
|
||||
while (true) {
|
||||
val line = LogLine.fromInputStream(logStream)
|
||||
|
||||
// skip debug/verbose lines
|
||||
if (line.priority >= LogPriority.INFO) {
|
||||
logLines += line
|
||||
}
|
||||
|
||||
yield()
|
||||
}
|
||||
}
|
||||
} catch (_: EOFException) {
|
||||
// ignore, end of file reached
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logLines += LogLine.showException(e)
|
||||
}
|
||||
|
||||
return logLines
|
||||
}
|
||||
|
||||
fun clearLogs() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// -c = clear
|
||||
Runtime.getRuntime().exec("logcat -c")
|
||||
.waitFor()
|
||||
}
|
||||
|
||||
_lineState.clear()
|
||||
}
|
||||
|
||||
fun getLogData(): String {
|
||||
return _lineState.joinToString("\n") { it.asSimpleString }
|
||||
}
|
||||
|
||||
init {
|
||||
_isLoading.value = true
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val lines = logOutput()
|
||||
_lineState.addAll(lines)
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z"/>
|
||||
</vector>
|
||||
@@ -48,10 +48,10 @@
|
||||
|
||||
<!-- SettingsActivity strings -->
|
||||
<string name="title_activity_settings">Settings</string>
|
||||
<string name="settings_back_icon_alt">back arrow icon</string>
|
||||
<string name="back_icon_alt">back</string>
|
||||
|
||||
<string name="export_folder_tag">Launcher External Folder</string>
|
||||
<string name="export_folder_copied">Copied!</string>
|
||||
<string name="text_copied">Copied to clipboard.</string>
|
||||
|
||||
<string name="preference_category_testing">General</string>
|
||||
<string name="preference_load_testing_name">Load files from /test</string>
|
||||
@@ -59,6 +59,7 @@
|
||||
<string name="preference_load_automatically_description">Launches the game after a short delay.</string>
|
||||
<string name="preferences_copy_external_button">Copy external files directory</string>
|
||||
<string name="preferences_open_file_manager">Open file manager</string>
|
||||
<string name="preferences_view_logs">View application logs</string>
|
||||
|
||||
<string name="preference_theme_name">Theme</string>
|
||||
<string name="preference_theme_select">Select theme</string>
|
||||
@@ -79,4 +80,11 @@
|
||||
<string name="preference_check_for_updates_downloading">Downloading update…</string>
|
||||
<string name="preference_check_for_updates_no_internet">No internet found.</string>
|
||||
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
|
||||
|
||||
<!-- ApplicationLogsActivity strings -->
|
||||
<string name="title_activity_application_logs">Application Logs</string>
|
||||
<string name="application_logs_no_logs">No logs available.</string>
|
||||
<string name="application_logs_delete">Clear</string>
|
||||
<string name="application_logs_share">Share</string>
|
||||
<string name="application_log_copy_label">Log Message</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user