feat: error log info screen
This commit is contained in:
parent
95ff30684d
commit
3a9e3562ca
14 changed files with 468 additions and 88 deletions
2
.github/workflows/crowdin.yml
vendored
2
.github/workflows/crowdin.yml
vendored
|
|
@ -36,4 +36,4 @@ jobs:
|
|||
git add *
|
||||
git merge l10n
|
||||
git push
|
||||
git push origin --delete l10n
|
||||
git push origin --delete l10n || true
|
||||
|
|
|
|||
|
|
@ -150,8 +150,9 @@ dependencies {
|
|||
debugImplementation(libs.compose.ui.tooling)
|
||||
debugImplementation(libs.compose.runtime.tracing)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.immutable)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(libs.apksig)
|
||||
implementation(libs.axml)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.aliucord.manager.network.services.*
|
|||
import com.aliucord.manager.ui.screens.about.AboutModel
|
||||
import com.aliucord.manager.ui.screens.home.HomeModel
|
||||
import com.aliucord.manager.ui.screens.iconopts.IconOptionsModel
|
||||
import com.aliucord.manager.ui.screens.log.LogScreenModel
|
||||
import com.aliucord.manager.ui.screens.patching.PatchingScreenModel
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel
|
||||
import com.aliucord.manager.ui.screens.plugins.PluginsModel
|
||||
|
|
@ -54,6 +55,7 @@ class ManagerApplication : Application() {
|
|||
factoryOf(::SettingsModel)
|
||||
factoryOf(::PatchOptionsModel)
|
||||
factoryOf(::IconOptionsModel)
|
||||
factoryOf(::LogScreenModel)
|
||||
viewModelOf(::UpdaterViewModel)
|
||||
})
|
||||
|
||||
|
|
@ -63,6 +65,7 @@ class ManagerApplication : Application() {
|
|||
singleOf(::PathManager)
|
||||
singleOf(::InstallerManager)
|
||||
singleOf(::OverlayManager)
|
||||
singleOf(::InstallLogManager)
|
||||
|
||||
singleOf(::AndroidDownloadManager)
|
||||
singleOf(::KtorDownloadManager)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
package com.aliucord.manager.manager
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||
import com.aliucord.manager.util.IS_PROBABLY_EMULATOR
|
||||
import com.aliucord.manager.util.isPlayProtectEnabled
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
|
||||
/**
|
||||
* Central manager for storing all attempted installations and
|
||||
* their associated logs/crashes (not including manager crashes themselves).
|
||||
*/
|
||||
class InstallLogManager(
|
||||
private val application: Application,
|
||||
private val json: Json,
|
||||
) {
|
||||
val logsDir = application.filesDir.resolve("install-logs").apply { mkdir() }
|
||||
|
||||
/**
|
||||
* Loads the install log from disk, if it exists.
|
||||
*/
|
||||
fun fetchInstallData(id: String): InstallLogData? {
|
||||
val path = logsDir.resolve("$id.json")
|
||||
if (!path.exists()) return null
|
||||
|
||||
return try {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
json.decodeFromStream(path.inputStream())
|
||||
} catch (t: Throwable) {
|
||||
Log.e(BuildConfig.TAG, "Failed to open install log $id", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an install log entry to disk.
|
||||
*/
|
||||
suspend fun storeInstallData(
|
||||
id: String,
|
||||
installDate: Instant,
|
||||
options: PatchOptions,
|
||||
error: Throwable?,
|
||||
) {
|
||||
val path = logsDir.resolve("$id.json")
|
||||
|
||||
val data = InstallLogData(
|
||||
id = id,
|
||||
installDate = installDate,
|
||||
installOptions = options,
|
||||
environmentInfo = getEnvironmentInfo(),
|
||||
installationLog = "", // TODO: install log
|
||||
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
|
||||
)
|
||||
|
||||
path.writeText(json.encodeToString(data))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of details about the current installation environment.
|
||||
*/
|
||||
suspend fun getEnvironmentInfo(): String {
|
||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||
val gitChanges = if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes present)" else ""
|
||||
val soc = if (Build.VERSION.SDK_INT >= 31) (Build.SOC_MANUFACTURER + ' ' + Build.SOC_MODEL) else "Unavailable"
|
||||
val playProtect = when (application.isPlayProtectEnabled()) {
|
||||
null -> "Unavailable"
|
||||
true -> "Enabled"
|
||||
false -> "Disabled"
|
||||
}
|
||||
|
||||
return """
|
||||
Aliucord Manager v${BuildConfig.VERSION_NAME}
|
||||
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges
|
||||
|
||||
Android API: ${Build.VERSION.SDK_INT}
|
||||
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
|
||||
ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH})
|
||||
Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})
|
||||
Emulator: $IS_PROBABLY_EMULATOR (guess)
|
||||
Play Protect: $playProtect
|
||||
SOC: $soc
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@Serializable
|
||||
data class InstallLogData(
|
||||
val id: String,
|
||||
val installDate: Instant,
|
||||
val installOptions: PatchOptions,
|
||||
val environmentInfo: String,
|
||||
val installationLog: String,
|
||||
val errorStacktrace: String?,
|
||||
) {
|
||||
val isError: Boolean
|
||||
get() = errorStacktrace != null
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package com.aliucord.manager.ui.screens.log
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.koin.getScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.InstallLogData
|
||||
import com.aliucord.manager.ui.components.*
|
||||
import com.aliucord.manager.ui.screens.log.components.SelectableTextArea
|
||||
import com.aliucord.manager.ui.screens.patchopts.components.options.PatchOption
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Parcelize
|
||||
class LogScreen(private val installId: String) : Screen, Parcelable {
|
||||
@IgnoredOnParcel
|
||||
override val key: ScreenKey
|
||||
get() = "LogScreen-$installId"
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val model = getScreenModel<LogScreenModel> { parametersOf(installId) }
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
if (model.shouldCloseScreen) {
|
||||
navigator.back(currentActivity = null)
|
||||
}
|
||||
|
||||
model.data?.let {
|
||||
LogScreenContent(
|
||||
data = it,
|
||||
onSaveLog = model::saveLog,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogScreenContent(
|
||||
data: InstallLogData,
|
||||
onSaveLog: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.log_title)) },
|
||||
navigationIcon = { BackButton() },
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(28.dp),
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.padding(vertical = 10.dp, horizontal = 22.dp)
|
||||
) {
|
||||
item("INSTALL_INFO") {
|
||||
PatchOption(
|
||||
name = stringResource(R.string.log_section_install_info),
|
||||
description = null,
|
||||
) {
|
||||
SelectableTextArea(
|
||||
text = """
|
||||
Installation ID: ${data.id}
|
||||
Installation Date: ${data.installDate}
|
||||
""".trimIndent(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item("ENVIRONMENT_INFO") {
|
||||
PatchOption(
|
||||
name = stringResource(R.string.log_section_env_info),
|
||||
description = stringResource(R.string.log_section_env_info_desc),
|
||||
) {
|
||||
SelectableTextArea(
|
||||
text = data.environmentInfo,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.errorStacktrace != null) {
|
||||
item("ERROR_STACKTRACE") {
|
||||
PatchOption(
|
||||
name = stringResource(R.string.log_section_error),
|
||||
description = null,
|
||||
) {
|
||||
SelectableTextArea(
|
||||
text = data.errorStacktrace,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// item("LOG") {
|
||||
// PatchOption(
|
||||
// name = stringResource(R.string.log_section_log),
|
||||
// description = null,
|
||||
// ) {
|
||||
// SelectableTextArea(
|
||||
// text = data.installationLog,
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
item("SAVE_BUTTON") {
|
||||
MainActionButton(
|
||||
text = stringResource(R.string.log_action_export),
|
||||
icon = painterResource(R.drawable.ic_download),
|
||||
onClick = onSaveLog,
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.aliucord.manager.ui.screens.log
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.*
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.InstallLogData
|
||||
import com.aliucord.manager.manager.InstallLogManager
|
||||
import com.aliucord.manager.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class LogScreenModel(
|
||||
private val installId: String,
|
||||
private val logs: InstallLogManager,
|
||||
private val application: Application,
|
||||
) : ScreenModel {
|
||||
var shouldCloseScreen by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var data by mutableStateOf<InstallLogData?>(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
loadLogData()
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the log data into a file and writes it to the downloads folder.
|
||||
*/
|
||||
fun saveLog() = screenModelScope.launchBlock {
|
||||
val data = data ?: return@launchBlock
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val formattedDate = SimpleDateFormat("yyyy-MM-dd hh-mm-s a", Locale.ENGLISH)
|
||||
.format(Date(data.installDate.toEpochMilliseconds()))
|
||||
|
||||
val content = buildString {
|
||||
appendLine("////////////////// Installation Info //////////////////")
|
||||
append("Install ID: ")
|
||||
appendLine(data.id)
|
||||
append("Install time: ")
|
||||
appendLine(formattedDate)
|
||||
append("Result: ")
|
||||
appendLine(if (data.isError) "Failure" else "Success")
|
||||
|
||||
append("\n\n")
|
||||
appendLine("////////////////// Environment Info //////////////////")
|
||||
appendLine(data.environmentInfo)
|
||||
|
||||
append("\n\n")
|
||||
appendLine("////////////////// Error Stacktrace //////////////////")
|
||||
appendLine(data.errorStacktrace ?: "None")
|
||||
|
||||
append("\n\n")
|
||||
appendLine("////////////////// Installation Log //////////////////")
|
||||
appendLine(data.installationLog)
|
||||
}
|
||||
|
||||
application.saveFile("Aliucord Manager $formattedDate.log", content)
|
||||
}
|
||||
|
||||
private fun loadLogData() = screenModelScope.launchBlock {
|
||||
val result = logs.fetchInstallData(installId)
|
||||
|
||||
if (result != null) {
|
||||
data = result
|
||||
} else {
|
||||
shouldCloseScreen = true
|
||||
application.showToast(R.string.network_load_fail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.aliucord.manager.ui.screens.log.components
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aliucord.manager.ui.util.horizontalScrollbar
|
||||
import com.aliucord.manager.ui.util.thenIf
|
||||
|
||||
@Composable
|
||||
fun SelectableTextArea(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier.Companion,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val scrollable by remember { derivedStateOf { scrollState.maxValue > 0 } }
|
||||
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontFamily = FontFamily.Companion.Monospace,
|
||||
softWrap = false,
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
.padding(start = 18.dp, end = 18.dp, top = 14.dp, bottom = 14.dp)
|
||||
.horizontalScroll(scrollState)
|
||||
.horizontalScrollbar(scrollState)
|
||||
.thenIf(scrollable) { padding(bottom = 10.dp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -220,12 +220,13 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
|
|||
enter = fadeIn() + slideInVertically(),
|
||||
exit = fadeOut() + slideOutVertically { it * -2 },
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(VERTICAL_PADDING / 2),
|
||||
) {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.padding(bottom = VERTICAL_PADDING)
|
||||
.padding(bottom = VERTICAL_PADDING / 2)
|
||||
)
|
||||
|
||||
when (filteredState) {
|
||||
|
|
@ -244,7 +245,13 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
|
|||
MainActionButton(
|
||||
text = stringResource(R.string.action_retry_install),
|
||||
icon = painterResource(R.drawable.ic_refresh),
|
||||
onClick = model::restart,
|
||||
onClick = model::launchApp,
|
||||
)
|
||||
|
||||
MainActionButton(
|
||||
text = stringResource(R.string.action_open_error_log),
|
||||
icon = painterResource(R.drawable.ic_launch),
|
||||
onClick = { model.openLog(navigator) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -260,9 +267,6 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
|
|||
cacheCleared = true
|
||||
model.clearCache()
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(top = 14.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -303,21 +307,3 @@ private fun BannerSection(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: error log
|
||||
// item(key = "ERROR_LOG") {
|
||||
// SelectionContainer {
|
||||
// Text(
|
||||
// text = failureLog,
|
||||
// style = MaterialTheme.typography.labelSmall,
|
||||
// fontFamily = FontFamily.Monospace,
|
||||
// softWrap = false,
|
||||
// modifier = Modifier
|
||||
// .padding(top = VERTICAL_PADDING)
|
||||
// .clip(RoundedCornerShape(10.dp))
|
||||
// .background(MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp))
|
||||
// .padding(24.dp)
|
||||
// .horizontalScroll(rememberScrollState())
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -2,39 +2,42 @@ package com.aliucord.manager.ui.screens.patching
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.*
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.manager.PreferencesManager
|
||||
import com.aliucord.manager.manager.*
|
||||
import com.aliucord.manager.patcher.KotlinPatchRunner
|
||||
import com.aliucord.manager.patcher.StepRunner
|
||||
import com.aliucord.manager.patcher.steps.StepGroup
|
||||
import com.aliucord.manager.patcher.steps.base.Step
|
||||
import com.aliucord.manager.patcher.steps.base.StepState
|
||||
import com.aliucord.manager.patcher.steps.install.InstallStep
|
||||
import com.aliucord.manager.ui.screens.log.LogScreen
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
||||
import com.aliucord.manager.util.*
|
||||
import com.aliucord.manager.util.showToast
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.coroutines.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class PatchingScreenModel(
|
||||
private val application: Application,
|
||||
private val paths: PathManager,
|
||||
private val options: PatchOptions,
|
||||
private val paths: PathManager,
|
||||
private val prefs: PreferencesManager,
|
||||
private val application: Application,
|
||||
private val installLogs: InstallLogManager,
|
||||
) : StateScreenModel<PatchingScreenState>(PatchingScreenState.Working) {
|
||||
private var startTime: Date? = null
|
||||
private var installId: String? = null
|
||||
private var startTime: Instant? = null
|
||||
private var runnerJob: Job? = null
|
||||
private var stepRunner: StepRunner? = null
|
||||
|
||||
|
|
@ -73,26 +76,6 @@ class PatchingScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun copyDebugToClipboard() {
|
||||
val content = (state.value as? PatchingScreenState.Failed)?.failureLog
|
||||
?: return
|
||||
|
||||
application.copyToClipboard(content)
|
||||
application.showToast(R.string.action_copied)
|
||||
}
|
||||
|
||||
fun saveFailureLog() {
|
||||
val startTime = startTime ?: return
|
||||
val failureLog = (state.value as? PatchingScreenState.Failed)?.failureLog
|
||||
?: return
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val formattedDate = SimpleDateFormat("yyyy-MM-dd hh-mm-s a").format(startTime)
|
||||
val fileName = "Aliucord Manager $formattedDate.log"
|
||||
|
||||
application.saveFile(fileName, failureLog)
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
screenModelScope.launch { paths.clearCache() }
|
||||
application.showToast(R.string.action_cleared_cache)
|
||||
|
|
@ -102,10 +85,14 @@ class PatchingScreenModel(
|
|||
runnerJob?.cancel("Manual cancellation")
|
||||
steps = null
|
||||
|
||||
startTime = Date()
|
||||
@SuppressLint("MemberExtensionConflict")
|
||||
installId = UUID.randomUUID().toString()
|
||||
startTime = Clock.System.now()
|
||||
mutableState.value = PatchingScreenState.Working
|
||||
|
||||
runnerJob = screenModelScope.launch {
|
||||
Log.i(BuildConfig.TAG, "Starting installation with environment:\n" + installLogs.getEnvironmentInfo())
|
||||
|
||||
try {
|
||||
startPatchRunner()
|
||||
} catch (_: CancellationException) {
|
||||
|
|
@ -113,11 +100,22 @@ class PatchingScreenModel(
|
|||
mutableState.value = PatchingScreenState.CloseScreen
|
||||
} catch (error: Throwable) {
|
||||
Log.e(BuildConfig.TAG, "Failed to orchestrate patch runner", error)
|
||||
mutableState.value = PatchingScreenState.Failed(failureLog = getFailureInfo(error))
|
||||
mutableState.value = PatchingScreenState.Failed(installId = installId!!)
|
||||
installLogs.storeInstallData(
|
||||
id = installId!!,
|
||||
installDate = startTime!!,
|
||||
options = options,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openLog(navigator: Navigator) {
|
||||
val id = installId ?: return
|
||||
navigator.push(LogScreen(installId = id))
|
||||
}
|
||||
|
||||
private suspend fun startPatchRunner() {
|
||||
val runner = KotlinPatchRunner(options)
|
||||
.also { stepRunner = it }
|
||||
|
|
@ -141,41 +139,28 @@ class PatchingScreenModel(
|
|||
// At this point, the installation has successfully completed
|
||||
else {
|
||||
mutableState.value = PatchingScreenState.Success
|
||||
installLogs.storeInstallData(
|
||||
id = installId!!,
|
||||
installDate = startTime!!,
|
||||
options = options,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.e(BuildConfig.TAG, "Failed to perform installation process", error)
|
||||
mutableState.value = PatchingScreenState.Failed(failureLog = getFailureInfo(error))
|
||||
mutableState.value = PatchingScreenState.Failed(installId = installId!!)
|
||||
installLogs.storeInstallData(
|
||||
id = installId!!,
|
||||
installDate = startTime!!,
|
||||
options = options,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getFailureInfo(stacktrace: Throwable): String {
|
||||
val gitChanges = if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes present)" else ""
|
||||
val soc = if (Build.VERSION.SDK_INT >= 31) (Build.SOC_MANUFACTURER + ' ' + Build.SOC_MODEL) else "Unavailable"
|
||||
val playProtect = when (application.isPlayProtectEnabled()) {
|
||||
null -> "Unavailable"
|
||||
true -> "Enabled"
|
||||
false -> "Disabled"
|
||||
}
|
||||
|
||||
val header = """
|
||||
Aliucord Manager v${BuildConfig.VERSION_NAME}
|
||||
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges
|
||||
|
||||
Android API: ${Build.VERSION.SDK_INT}
|
||||
Emulator: $IS_PROBABLY_EMULATOR (guess)
|
||||
ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH})
|
||||
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
|
||||
Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})
|
||||
Play Protect: $playProtect
|
||||
SOC: $soc
|
||||
""".trimIndent()
|
||||
|
||||
return header + "\n\n" + Log.getStackTraceString(stacktrace).trimEnd()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/**
|
||||
* Random fun facts to show on the installation screen.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.aliucord.manager.ui.screens.patching.PatchingScreenState.CloseScreen
|
|||
sealed interface PatchingScreenState {
|
||||
data object Working : PatchingScreenState
|
||||
data object Success : PatchingScreenState
|
||||
data class Failed(val failureLog: String) : PatchingScreenState
|
||||
data class Failed(val installId: String) : PatchingScreenState
|
||||
data object CloseScreen : PatchingScreenState
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
// TODO: rename this to a generic titled component
|
||||
@Composable
|
||||
fun PatchOption(
|
||||
name: String,
|
||||
|
|
@ -17,7 +18,7 @@ fun PatchOption(
|
|||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.aliucord.manager.ui.util
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.max
|
||||
|
||||
// Based on https://gist.github.com/XFY9326/2067efcc3c5899557cc6a334d76a92c8
|
||||
|
||||
fun Modifier.horizontalScrollbar(
|
||||
scrollState: ScrollState,
|
||||
scrollBarHeight: Dp = 4.dp,
|
||||
minScrollBarWidth: Dp = 5.dp,
|
||||
scrollBarColor: Color = Color.White.copy(alpha = .4f),
|
||||
cornerRadius: Dp = 2.dp,
|
||||
): Modifier = composed {
|
||||
drawWithContent {
|
||||
drawContent()
|
||||
|
||||
if (scrollState.maxValue <= 0) return@drawWithContent
|
||||
|
||||
val visibleWidth: Float = this.size.width - scrollState.maxValue
|
||||
val scrollBarWidth: Float = max(visibleWidth * (visibleWidth / this.size.width), minScrollBarWidth.toPx())
|
||||
val scrollPercent: Float = scrollState.value.toFloat() / scrollState.maxValue
|
||||
val scrollBarOffsetX: Float = scrollState.value + (visibleWidth - scrollBarWidth) * scrollPercent
|
||||
|
||||
drawRoundRect(
|
||||
color = scrollBarColor,
|
||||
topLeft = Offset(scrollBarOffsetX, this.size.height - scrollBarHeight.toPx()),
|
||||
size = Size(scrollBarWidth, scrollBarHeight.toPx()),
|
||||
cornerRadius = CornerRadius(cornerRadius.toPx())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
|
||||
<string name="action_cancel">Cancel</string>
|
||||
<string name="action_retry">Retry</string>
|
||||
<string name="action_retry_install">Retry Installation</string>
|
||||
<string name="action_retry_install">Retry installation</string>
|
||||
<string name="action_open_error_log">Open error log</string>
|
||||
<string name="action_apply">Apply</string>
|
||||
<string name="action_confirm">Confirm</string>
|
||||
<string name="action_dismiss">Dismiss</string>
|
||||
|
|
@ -184,6 +185,14 @@
|
|||
<string name="iconopts_image_preview_desc">Uncropped preview of the selected image</string>
|
||||
<string name="iconopts_image_preview_none">No image selected</string>
|
||||
|
||||
<string name="log_title">Log</string>
|
||||
<string name="log_section_install_info">Installation Info</string>
|
||||
<string name="log_section_env_info">Environment Info</string>
|
||||
<string name="log_section_env_info_desc">Information about your device that may be crucial for debugging.</string>
|
||||
<string name="log_section_error">Error Stacktrace</string>
|
||||
<string name="log_section_log">Installation Log</string>
|
||||
<string name="log_action_export">Export log</string>
|
||||
|
||||
<string name="notif_group_install_title">Active installation</string>
|
||||
<string name="notif_group_install_desc">Progress notifications sent during a minimized installation</string>
|
||||
<string name="notif_install_ready_title">Aliucord installation ready!</string>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ compose-pipette = "1.0.0-beta01"
|
|||
#diff = "4.12"
|
||||
koin = "4.0.2"
|
||||
kotlin = "2.1.10"
|
||||
kotlinx-datetime = "0.6.2"
|
||||
kotlinx-immutable = "0.3.8"
|
||||
kotlinx-serialization = "1.8.0"
|
||||
ktor = "3.0.3"
|
||||
|
|
@ -63,8 +64,9 @@ koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
|
|||
koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
|
||||
|
||||
# KotlinX
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
kotlinx-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-immutable" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
|
||||
# Ktor
|
||||
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue