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 add *
|
||||||
git merge l10n
|
git merge l10n
|
||||||
git push
|
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.ui.tooling)
|
||||||
debugImplementation(libs.compose.runtime.tracing)
|
debugImplementation(libs.compose.runtime.tracing)
|
||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.kotlinx.immutable)
|
implementation(libs.kotlinx.immutable)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
implementation(libs.apksig)
|
implementation(libs.apksig)
|
||||||
implementation(libs.axml)
|
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.about.AboutModel
|
||||||
import com.aliucord.manager.ui.screens.home.HomeModel
|
import com.aliucord.manager.ui.screens.home.HomeModel
|
||||||
import com.aliucord.manager.ui.screens.iconopts.IconOptionsModel
|
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.patching.PatchingScreenModel
|
||||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel
|
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel
|
||||||
import com.aliucord.manager.ui.screens.plugins.PluginsModel
|
import com.aliucord.manager.ui.screens.plugins.PluginsModel
|
||||||
|
|
@ -54,6 +55,7 @@ class ManagerApplication : Application() {
|
||||||
factoryOf(::SettingsModel)
|
factoryOf(::SettingsModel)
|
||||||
factoryOf(::PatchOptionsModel)
|
factoryOf(::PatchOptionsModel)
|
||||||
factoryOf(::IconOptionsModel)
|
factoryOf(::IconOptionsModel)
|
||||||
|
factoryOf(::LogScreenModel)
|
||||||
viewModelOf(::UpdaterViewModel)
|
viewModelOf(::UpdaterViewModel)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -63,6 +65,7 @@ class ManagerApplication : Application() {
|
||||||
singleOf(::PathManager)
|
singleOf(::PathManager)
|
||||||
singleOf(::InstallerManager)
|
singleOf(::InstallerManager)
|
||||||
singleOf(::OverlayManager)
|
singleOf(::OverlayManager)
|
||||||
|
singleOf(::InstallLogManager)
|
||||||
|
|
||||||
singleOf(::AndroidDownloadManager)
|
singleOf(::AndroidDownloadManager)
|
||||||
singleOf(::KtorDownloadManager)
|
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(),
|
enter = fadeIn() + slideInVertically(),
|
||||||
exit = fadeOut() + slideOutVertically { it * -2 },
|
exit = fadeOut() + slideOutVertically { it * -2 },
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(VERTICAL_PADDING / 2),
|
||||||
|
) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 4.dp)
|
.padding(bottom = VERTICAL_PADDING / 2)
|
||||||
.padding(bottom = VERTICAL_PADDING)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
when (filteredState) {
|
when (filteredState) {
|
||||||
|
|
@ -244,7 +245,13 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
|
||||||
MainActionButton(
|
MainActionButton(
|
||||||
text = stringResource(R.string.action_retry_install),
|
text = stringResource(R.string.action_retry_install),
|
||||||
icon = painterResource(R.drawable.ic_refresh),
|
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
|
cacheCleared = true
|
||||||
model.clearCache()
|
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.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import com.aliucord.manager.BuildConfig
|
import com.aliucord.manager.BuildConfig
|
||||||
import com.aliucord.manager.R
|
import com.aliucord.manager.R
|
||||||
import com.aliucord.manager.manager.PathManager
|
import com.aliucord.manager.manager.*
|
||||||
import com.aliucord.manager.manager.PreferencesManager
|
|
||||||
import com.aliucord.manager.patcher.KotlinPatchRunner
|
import com.aliucord.manager.patcher.KotlinPatchRunner
|
||||||
import com.aliucord.manager.patcher.StepRunner
|
import com.aliucord.manager.patcher.StepRunner
|
||||||
import com.aliucord.manager.patcher.steps.StepGroup
|
import com.aliucord.manager.patcher.steps.StepGroup
|
||||||
import com.aliucord.manager.patcher.steps.base.Step
|
import com.aliucord.manager.patcher.steps.base.Step
|
||||||
import com.aliucord.manager.patcher.steps.base.StepState
|
import com.aliucord.manager.patcher.steps.base.StepState
|
||||||
import com.aliucord.manager.patcher.steps.install.InstallStep
|
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.screens.patchopts.PatchOptions
|
||||||
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
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.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.text.SimpleDateFormat
|
import kotlinx.datetime.Clock
|
||||||
import java.util.Date
|
import kotlinx.datetime.Instant
|
||||||
|
import java.util.UUID
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class PatchingScreenModel(
|
class PatchingScreenModel(
|
||||||
private val application: Application,
|
|
||||||
private val paths: PathManager,
|
|
||||||
private val options: PatchOptions,
|
private val options: PatchOptions,
|
||||||
|
private val paths: PathManager,
|
||||||
private val prefs: PreferencesManager,
|
private val prefs: PreferencesManager,
|
||||||
|
private val application: Application,
|
||||||
|
private val installLogs: InstallLogManager,
|
||||||
) : StateScreenModel<PatchingScreenState>(PatchingScreenState.Working) {
|
) : 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 runnerJob: Job? = null
|
||||||
private var stepRunner: StepRunner? = 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() {
|
fun clearCache() {
|
||||||
screenModelScope.launch { paths.clearCache() }
|
screenModelScope.launch { paths.clearCache() }
|
||||||
application.showToast(R.string.action_cleared_cache)
|
application.showToast(R.string.action_cleared_cache)
|
||||||
|
|
@ -102,10 +85,14 @@ class PatchingScreenModel(
|
||||||
runnerJob?.cancel("Manual cancellation")
|
runnerJob?.cancel("Manual cancellation")
|
||||||
steps = null
|
steps = null
|
||||||
|
|
||||||
startTime = Date()
|
@SuppressLint("MemberExtensionConflict")
|
||||||
|
installId = UUID.randomUUID().toString()
|
||||||
|
startTime = Clock.System.now()
|
||||||
mutableState.value = PatchingScreenState.Working
|
mutableState.value = PatchingScreenState.Working
|
||||||
|
|
||||||
runnerJob = screenModelScope.launch {
|
runnerJob = screenModelScope.launch {
|
||||||
|
Log.i(BuildConfig.TAG, "Starting installation with environment:\n" + installLogs.getEnvironmentInfo())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startPatchRunner()
|
startPatchRunner()
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
|
|
@ -113,11 +100,22 @@ class PatchingScreenModel(
|
||||||
mutableState.value = PatchingScreenState.CloseScreen
|
mutableState.value = PatchingScreenState.CloseScreen
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to orchestrate patch runner", error)
|
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() {
|
private suspend fun startPatchRunner() {
|
||||||
val runner = KotlinPatchRunner(options)
|
val runner = KotlinPatchRunner(options)
|
||||||
.also { stepRunner = it }
|
.also { stepRunner = it }
|
||||||
|
|
@ -141,41 +139,28 @@ class PatchingScreenModel(
|
||||||
// At this point, the installation has successfully completed
|
// At this point, the installation has successfully completed
|
||||||
else {
|
else {
|
||||||
mutableState.value = PatchingScreenState.Success
|
mutableState.value = PatchingScreenState.Success
|
||||||
|
installLogs.storeInstallData(
|
||||||
|
id = installId!!,
|
||||||
|
installDate = startTime!!,
|
||||||
|
options = options,
|
||||||
|
error = null,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.e(BuildConfig.TAG, "Failed to perform installation process", error)
|
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 {
|
private companion object {
|
||||||
/**
|
/**
|
||||||
* Random fun facts to show on the installation screen.
|
* 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 {
|
sealed interface PatchingScreenState {
|
||||||
data object Working : PatchingScreenState
|
data object Working : PatchingScreenState
|
||||||
data object Success : PatchingScreenState
|
data object Success : PatchingScreenState
|
||||||
data class Failed(val failureLog: String) : PatchingScreenState
|
data class Failed(val installId: String) : PatchingScreenState
|
||||||
data object CloseScreen : PatchingScreenState
|
data object CloseScreen : PatchingScreenState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
// TODO: rename this to a generic titled component
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchOption(
|
fun PatchOption(
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -17,7 +18,7 @@ fun PatchOption(
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
modifier = modifier,
|
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_cancel">Cancel</string>
|
||||||
<string name="action_retry">Retry</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_apply">Apply</string>
|
||||||
<string name="action_confirm">Confirm</string>
|
<string name="action_confirm">Confirm</string>
|
||||||
<string name="action_dismiss">Dismiss</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_desc">Uncropped preview of the selected image</string>
|
||||||
<string name="iconopts_image_preview_none">No image selected</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_title">Active installation</string>
|
||||||
<string name="notif_group_install_desc">Progress notifications sent during a minimized 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>
|
<string name="notif_install_ready_title">Aliucord installation ready!</string>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ compose-pipette = "1.0.0-beta01"
|
||||||
#diff = "4.12"
|
#diff = "4.12"
|
||||||
koin = "4.0.2"
|
koin = "4.0.2"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.1.10"
|
||||||
|
kotlinx-datetime = "0.6.2"
|
||||||
kotlinx-immutable = "0.3.8"
|
kotlinx-immutable = "0.3.8"
|
||||||
kotlinx-serialization = "1.8.0"
|
kotlinx-serialization = "1.8.0"
|
||||||
ktor = "3.0.3"
|
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" }
|
koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
|
||||||
|
|
||||||
# KotlinX
|
# 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-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
|
||||||
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue