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
+593
-18
No files matched your search
+6
-5
@@ -75,10 +75,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation platform('androidx.compose:compose-bom:2023.10.01')
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation "androidx.compose.ui:ui:$compose_version"
|
implementation "androidx.compose.ui:ui"
|
||||||
implementation 'androidx.compose.material3:material3:1.1.2'
|
implementation 'androidx.compose.material3:material3'
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
|
||||||
implementation 'androidx.activity:activity-compose:1.8.2'
|
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-serialization-json:1.6.0'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.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-tooling"
|
||||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
|
debugImplementation "androidx.compose.ui:ui-test-manifest"
|
||||||
|
debugImplementation "androidx.compose.ui:ui-tooling-preview"
|
||||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
|
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
|
||||||
}
|
}
|
||||||
@@ -9,9 +9,8 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_LOGS" />
|
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.wifi"
|
android:name="android.hardware.wifi"
|
||||||
@@ -28,6 +27,11 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.GeodeLauncher"
|
android:theme="@style/Theme.GeodeLauncher"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".ApplicationLogsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_application_logs"
|
||||||
|
android:theme="@style/Theme.GeodeLauncher" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
@@ -58,11 +62,12 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:authorities="com.geode.launcher.user"
|
|
||||||
android:name=".UserDirectoryProvider"
|
android:name=".UserDirectoryProvider"
|
||||||
android:grantUriPermissions="true"
|
android:authorities="com.geode.launcher.user"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
@@ -78,4 +83,5 @@
|
|||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.export_folder_copied),
|
context.getString(R.string.text_copied),
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
@@ -267,7 +272,7 @@ fun SettingsScreen(
|
|||||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
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),
|
title = stringResource(R.string.preference_black_background_name),
|
||||||
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
||||||
)
|
)
|
||||||
SettingsCard(
|
|
||||||
title = context.getString(R.string.preference_load_testing_name),
|
|
||||||
preferenceKey = PreferenceUtils.Key.LOAD_TESTING,
|
|
||||||
)
|
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = context.getString(R.string.preference_load_automatically_name),
|
title = context.getString(R.string.preference_load_automatically_name),
|
||||||
description = context.getString(R.string.preference_load_automatically_description),
|
description = context.getString(R.string.preference_load_automatically_description),
|
||||||
@@ -320,6 +321,10 @@ fun SettingsScreen(
|
|||||||
title = stringResource(R.string.preferences_open_file_manager),
|
title = stringResource(R.string.preferences_open_file_manager),
|
||||||
onClick = { onOpenFileManager(context) }
|
onClick = { onOpenFileManager(context) }
|
||||||
)
|
)
|
||||||
|
OptionsButton(
|
||||||
|
title = stringResource(R.string.preferences_view_logs),
|
||||||
|
onClick = { onOpenLogs(context) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(context.getString(R.string.preference_category_updater)) {
|
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 -->
|
<!-- SettingsActivity strings -->
|
||||||
<string name="title_activity_settings">Settings</string>
|
<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_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_category_testing">General</string>
|
||||||
<string name="preference_load_testing_name">Load files from /test</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="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_copy_external_button">Copy external files directory</string>
|
||||||
<string name="preferences_open_file_manager">Open file manager</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_name">Theme</string>
|
||||||
<string name="preference_theme_select">Select 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_downloading">Downloading update…</string>
|
||||||
<string name="preference_check_for_updates_no_internet">No internet found.</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>
|
<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>
|
</resources>
|
||||||
Reference in new issue
Block a user