forked from geode-sdk/android-launcher
411 lines
16 KiB
Kotlin
411 lines
16 KiB
Kotlin
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.rememberLauncherForActivityResult
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
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.Check
|
|
import androidx.compose.material.icons.filled.MoreVert
|
|
import androidx.compose.material.icons.filled.Share
|
|
import androidx.compose.material3.Divider
|
|
import androidx.compose.material3.DropdownMenu
|
|
import androidx.compose.material3.DropdownMenuItem
|
|
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.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
import androidx.compose.runtime.setValue
|
|
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?) {
|
|
enableEdgeToEdge()
|
|
|
|
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(0xff_df_51_51)
|
|
LogPriority.WARN -> Color(0xff_bc_b5_00)
|
|
LogPriority.FATAL -> Color(0xff_b8_b6_fc)
|
|
LogPriority.DEBUG -> Color(0xff_aa_aa_aa)
|
|
LogPriority.VERBOSE -> Color(0xff_77_77_77)
|
|
else -> Color.Unspecified
|
|
}
|
|
} else {
|
|
when (priority) {
|
|
LogPriority.ERROR -> Color(0xff_df_51_51)
|
|
LogPriority.WARN -> Color(0xff_ea_b7_00)
|
|
LogPriority.FATAL -> Color(0xff_a1_42_f4)
|
|
LogPriority.DEBUG -> Color(0xff_77_77_77)
|
|
LogPriority.VERBOSE -> Color(0xff_aa_aa_aa)
|
|
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 SelectLogLevelDialog(
|
|
onDismissRequest: () -> Unit,
|
|
onSelect: (LogPriority) -> Unit,
|
|
initialValue: LogPriority
|
|
) {
|
|
SelectDialog(
|
|
title = stringResource(R.string.application_logs_level_dialog_name),
|
|
onDismissRequest = onDismissRequest,
|
|
onSelect = { onSelect(it) },
|
|
initialValue = initialValue,
|
|
toLabel = {
|
|
// i'm lazy
|
|
it.name.lowercase().replaceFirstChar { c -> c.uppercaseChar() }
|
|
},
|
|
optionsCount = LogPriority.FATAL downTo LogPriority.VERBOSE
|
|
)
|
|
}
|
|
|
|
@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.messageTrimmed}",
|
|
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()
|
|
|
|
var showMoreOptions by remember { mutableStateOf(false) }
|
|
val saveLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
|
if (uri != null) {
|
|
coroutineScope.launch {
|
|
val logs = logViewModel.getLogData()
|
|
|
|
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
|
outputStream.bufferedWriter().use { writer ->
|
|
writer.write(logs)
|
|
}
|
|
}
|
|
|
|
Toast.makeText(context, R.string.application_logs_export_completed, Toast.LENGTH_SHORT)
|
|
.show()
|
|
}
|
|
}
|
|
}
|
|
|
|
LaunchedEffect(logLines.size) {
|
|
listState.scrollToItem(logLines.size)
|
|
}
|
|
|
|
var showLogLevelSelect by remember { mutableStateOf(false) }
|
|
if (showLogLevelSelect) {
|
|
SelectLogLevelDialog(
|
|
onDismissRequest = { showLogLevelSelect = false },
|
|
onSelect = {
|
|
logViewModel.logLevel = it
|
|
showLogLevelSelect = false
|
|
},
|
|
initialValue = logViewModel.logLevel
|
|
)
|
|
}
|
|
|
|
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 = {
|
|
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)
|
|
}
|
|
}, enabled = !logLines.isEmpty()) {
|
|
Icon(
|
|
Icons.Filled.Share,
|
|
contentDescription = stringResource(R.string.application_logs_share)
|
|
)
|
|
}
|
|
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
|
Icon(
|
|
Icons.Filled.MoreVert,
|
|
contentDescription = stringResource(R.string.application_logs_more)
|
|
)
|
|
}
|
|
|
|
DropdownMenu(
|
|
expanded = showMoreOptions,
|
|
onDismissRequest = { showMoreOptions = false }
|
|
) {
|
|
DropdownMenuItem(
|
|
leadingIcon = {
|
|
Icon(
|
|
painterResource(R.drawable.icon_bug_report),
|
|
contentDescription = null,
|
|
)
|
|
},
|
|
trailingIcon = {
|
|
if (logViewModel.filterCrashes) {
|
|
Icon(
|
|
Icons.Filled.Check,
|
|
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
|
|
)
|
|
}
|
|
},
|
|
text = {
|
|
Text(stringResource(R.string.application_logs_crash_only))
|
|
}, onClick = {
|
|
logViewModel.toggleCrashBuffer()
|
|
showMoreOptions = false
|
|
}
|
|
)
|
|
|
|
DropdownMenuItem(
|
|
leadingIcon = {
|
|
Icon(
|
|
painterResource(R.drawable.icon_filter_list),
|
|
contentDescription = null,
|
|
)
|
|
},
|
|
text = {
|
|
Text(stringResource(R.string.application_logs_level))
|
|
},
|
|
onClick = {
|
|
showLogLevelSelect = true
|
|
showMoreOptions = false
|
|
}
|
|
)
|
|
|
|
DropdownMenuItem(
|
|
leadingIcon = {
|
|
Icon(
|
|
painterResource(R.drawable.icon_save),
|
|
contentDescription = null,
|
|
)
|
|
},
|
|
text = {
|
|
Text(stringResource(R.string.application_logs_export))
|
|
},
|
|
onClick = {
|
|
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
|
|
showMoreOptions = false
|
|
},
|
|
enabled = !logLines.isEmpty()
|
|
)
|
|
|
|
DropdownMenuItem(
|
|
leadingIcon = {
|
|
Icon(
|
|
painterResource(R.drawable.icon_delete),
|
|
contentDescription = null
|
|
)
|
|
},
|
|
text = {
|
|
Text(stringResource(R.string.application_logs_delete))
|
|
}, onClick = {
|
|
logViewModel.clearLogs()
|
|
showMoreOptions = false
|
|
}
|
|
)
|
|
}
|
|
},
|
|
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)
|
|
}
|
|
} |