feat: show stacktrace in installer ui (#36)

This commit is contained in:
Ax333l 2023-06-17 13:45:52 +02:00 committed by GitHub
parent 6309e8bdf5
commit 5681c917c5
9 changed files with 236 additions and 172 deletions

View File

@ -89,7 +89,9 @@ dependencies {
implementation("me.zhanghai.android.appiconloader:appiconloader-coil:1.5.0") implementation("me.zhanghai.android.appiconloader:appiconloader-coil:1.5.0")
// KotlinX // KotlinX
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") val serializationVersion = "1.5.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
// Room // Room

View File

@ -25,9 +25,6 @@ class Session(
private val input: File, private val input: File,
private val onProgress: suspend (Progress) -> Unit = { } private val onProgress: suspend (Progress) -> Unit = { }
) : Closeable { ) : Closeable {
class PatchFailedException(val patchName: String, cause: Throwable?) :
Exception("Got exception while executing $patchName", cause)
private val logger = LogcatLogger private val logger = LogcatLogger
private val temporary = File(cacheDir).resolve("manager").also { it.mkdirs() } private val temporary = File(cacheDir).resolve("manager").also { it.mkdirs() }
private val patcher = Patcher( private val patcher = Patcher(
@ -48,9 +45,11 @@ class Session(
return@forEach return@forEach
} }
logger.error("$patch failed:") logger.error("$patch failed:")
result.exceptionOrNull()!!.printStackTrace() result.exceptionOrNull()!!.let {
logger.error(result.exceptionOrNull()!!.stackTraceToString())
throw PatchFailedException(patch, result.exceptionOrNull()) throw it
}
} }
} }

View File

@ -2,13 +2,11 @@ package app.revanced.manager.patcher.worker
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.work.Data
import androidx.work.workDataOf
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.util.serialize
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
sealed class Progress { sealed class Progress {
object Unpacking : Progress() object Unpacking : Progress()
@ -21,117 +19,116 @@ sealed class Progress {
} }
@Serializable @Serializable
enum class StepStatus { enum class State {
WAITING, WAITING, COMPLETED, FAILED
COMPLETED,
FAILURE,
} }
@Serializable @Serializable
class Step(val name: String, val status: StepStatus = StepStatus.WAITING) class SubStep(
val name: String,
val state: State = State.WAITING,
@SerialName("msg")
val message: String? = null
)
@Serializable @Serializable
class StepGroup( class Step(
@StringRes val name: Int, @StringRes val name: Int,
val steps: List<Step>, val substeps: List<SubStep>,
val status: StepStatus = StepStatus.WAITING val state: State = State.WAITING
) )
class PatcherProgressManager(context: Context, selectedPatches: List<String>) { class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
val stepGroups = generateGroupsList(context, selectedPatches) val steps = generateSteps(context, selectedPatches)
private var currentStep: StepKey? = StepKey(0, 0)
companion object { private fun update(key: StepKey, state: State, message: String? = null) {
private const val WORK_DATA_KEY = "progress" val isLastSubStep: Boolean
steps[key.step] = steps[key.step].let { step ->
isLastSubStep = key.substep == step.substeps.lastIndex
/** val newStepState = when {
* A map of [Progress] to the corresponding position in [stepGroups] // This step failed because one of its sub-steps failed.
*/ state == State.FAILED -> State.FAILED
private val stepKeyMap = mapOf( // All sub-steps succeeded.
Progress.Unpacking to StepKey(0, 0), state == State.COMPLETED && isLastSubStep -> State.COMPLETED
Progress.Merging to StepKey(0, 1),
Progress.PatchingStart to StepKey(1, 0),
Progress.Saving to StepKey(2, 0),
)
fun generateGroupsList(context: Context, selectedPatches: List<String>) = mutableListOf(
StepGroup(
R.string.patcher_step_group_prepare,
persistentListOf(
Step(context.getString(R.string.patcher_step_unpack)),
Step(context.getString(R.string.patcher_step_integrations))
)
),
StepGroup(
R.string.patcher_step_group_patching,
selectedPatches.map { Step(it) }
),
StepGroup(
R.string.patcher_step_group_saving,
persistentListOf(Step(context.getString(R.string.patcher_step_write_patched)))
)
)
fun groupsFromWorkData(workData: Data) = workData.getString(WORK_DATA_KEY)
?.let { Json.decodeFromString<List<StepGroup>>(it) }
}
fun groupsToWorkData() = workDataOf(WORK_DATA_KEY to Json.Default.encodeToString(stepGroups))
private var currentStep: StepKey? = null
private fun <T> MutableList<T>.mutateIndex(index: Int, callback: (T) -> T) = apply {
this[index] = callback(this[index])
}
private fun updateStepStatus(key: StepKey, newStatus: StepStatus) {
var isLastStepOfGroup = false
stepGroups.mutateIndex(key.groupIndex) { group ->
isLastStepOfGroup = key.stepIndex == group.steps.lastIndex
val newGroupStatus = when {
// This group failed if a step in it failed.
newStatus == StepStatus.FAILURE -> StepStatus.FAILURE
// All steps in the group succeeded.
newStatus == StepStatus.COMPLETED && isLastStepOfGroup -> StepStatus.COMPLETED
// Keep the old status. // Keep the old status.
else -> group.status else -> step.state
} }
StepGroup(group.name, group.steps.toMutableList().mutateIndex(key.stepIndex) { step -> Step(step.name, step.substeps.mapIndexed { index, subStep ->
Step(step.name, newStatus) if (index != key.substep) subStep else SubStep(subStep.name, state, message)
}, newGroupStatus) }, newStepState)
} }
val isFinalStep = isLastStepOfGroup && key.groupIndex == stepGroups.lastIndex val isFinal = isLastSubStep && key.step == steps.lastIndex
if (newStatus == StepStatus.COMPLETED) { if (state == State.COMPLETED) {
// Move the cursor to the next step. // Move the cursor to the next step.
currentStep = when { currentStep = when {
isFinalStep -> null // Final step has been completed. isFinal -> null // Final step has been completed.
isLastStepOfGroup -> StepKey(key.groupIndex + 1, 0) // Move to the next group. isLastSubStep -> StepKey(key.step + 1, 0) // Move to the next step.
else -> StepKey( else -> StepKey(
key.groupIndex, key.step,
key.stepIndex + 1 key.substep + 1
) // Move to the next step of this group. ) // Move to the next sub-step.
} }
} }
} }
private fun setCurrentStepStatus(newStatus: StepStatus) = fun replacePatchesList(newList: List<String>) {
currentStep?.let { updateStepStatus(it, newStatus) } steps[stepKeyMap[Progress.PatchingStart]!!.step] = generatePatchesStep(newList)
}
private fun updateCurrent(newState: State, message: String? = null) =
currentStep?.let { update(it, newState, message) }
private data class StepKey(val groupIndex: Int, val stepIndex: Int)
fun handle(progress: Progress) = success().also { fun handle(progress: Progress) = success().also {
stepKeyMap[progress]?.let { currentStep = it } stepKeyMap[progress]?.let { currentStep = it }
} }
fun failure() { fun failure(error: Throwable) = updateCurrent(
// TODO: associate the exception with the step that just failed. State.FAILED,
setCurrentStepStatus(StepStatus.FAILURE) error.stackTraceToString()
)
fun success() = updateCurrent(State.COMPLETED)
fun workData() = steps.serialize()
companion object {
/**
* A map of [Progress] to the corresponding position in [steps]
*/
private val stepKeyMap = mapOf(
Progress.Unpacking to StepKey(0, 1),
Progress.Merging to StepKey(0, 2),
Progress.PatchingStart to StepKey(1, 0),
Progress.Saving to StepKey(2, 0),
)
private fun generatePatchesStep(selectedPatches: List<String>) = Step(
R.string.patcher_step_group_patching,
selectedPatches.map { SubStep(it) }
)
fun generateSteps(context: Context, selectedPatches: List<String>) = mutableListOf(
Step(
R.string.patcher_step_group_prepare,
persistentListOf(
SubStep(context.getString(R.string.patcher_step_load_patches)),
SubStep(context.getString(R.string.patcher_step_unpack)),
SubStep(context.getString(R.string.patcher_step_integrations))
)
),
generatePatchesStep(selectedPatches),
Step(
R.string.patcher_step_group_saving,
persistentListOf(SubStep(context.getString(R.string.patcher_step_write_patched)))
)
)
} }
fun success() { private data class StepKey(val step: Int, val substep: Int)
setCurrentStepStatus(StepStatus.COMPLETED)
}
} }

View File

@ -19,11 +19,11 @@ import app.revanced.manager.domain.repository.SourceRepository
import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.aapt.Aapt import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.deserialize
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
@ -44,7 +44,6 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
) )
companion object { companion object {
const val ARGS_KEY = "args"
private const val logPrefix = "[Worker]:" private const val logPrefix = "[Worker]:"
private fun String.logFmt() = "$logPrefix $this" private fun String.logFmt() = "$logPrefix $this"
} }
@ -76,7 +75,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
return Result.failure() return Result.failure()
} }
val args = Json.decodeFromString<Args>(inputData.getString(ARGS_KEY)!!) val args = inputData.deserialize<Args>()!!
try { try {
// This does not always show up for some reason. // This does not always show up for some reason.
@ -105,29 +104,38 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
Aapt.binary(applicationContext)?.absolutePath Aapt.binary(applicationContext)?.absolutePath
?: throw FileNotFoundException("Could not resolve aapt.") ?: throw FileNotFoundException("Could not resolve aapt.")
val frameworkPath = applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath val frameworkPath =
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
val bundles = sourceRepository.bundles.first() val bundles = sourceRepository.bundles.first()
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations } val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
val patchList = args.selectedPatches.flatMap { (bundleName, selected) ->
bundles[bundleName]?.loadPatchesFiltered(args.packageName)
?.filter { selected.contains(it.patchName) }
?: throw IllegalArgumentException("Patch bundle $bundleName does not exist")
}
val progressManager = val progressManager =
PatcherProgressManager(applicationContext, patchList.map { it.patchName }) PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { it.value })
suspend fun updateProgress(progress: Progress) { suspend fun updateProgress(progress: Progress) {
progressManager.handle(progress) progressManager.handle(progress)
setProgress(progressManager.groupsToWorkData()) setProgress(progressManager.workData())
} }
updateProgress(Progress.Unpacking)
return try { return try {
Session(applicationContext.cacheDir.absolutePath, frameworkPath, aaptPath, File(args.input)) { val patchList = args.selectedPatches.flatMap { (bundleName, selected) ->
bundles[bundleName]?.loadPatchesFiltered(args.packageName)
?.filter { selected.contains(it.patchName) }
?: throw IllegalArgumentException("Patch bundle $bundleName does not exist")
}
// Ensure they are in the correct order so we can track progress properly.
progressManager.replacePatchesList(patchList.map { it.patchName })
updateProgress(Progress.Unpacking)
Session(
applicationContext.cacheDir.absolutePath,
frameworkPath,
aaptPath,
File(args.input)
) {
updateProgress(it) updateProgress(it)
}.use { session -> }.use { session ->
session.run(File(args.output), patchList, integrations) session.run(File(args.output), patchList, integrations)
@ -135,11 +143,11 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
Log.i(tag, "Patching succeeded".logFmt()) Log.i(tag, "Patching succeeded".logFmt())
progressManager.success() progressManager.success()
Result.success(progressManager.groupsToWorkData()) Result.success(progressManager.workData())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "Got exception while patching".logFmt(), e) Log.e(tag, "Got exception while patching".logFmt(), e)
progressManager.failure() progressManager.failure(e)
Result.failure(progressManager.groupsToWorkData()) Result.failure(progressManager.workData())
} }
} }
} }

View File

@ -0,0 +1,22 @@
package app.revanced.manager.ui.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
@Composable
fun ArrowButton(expanded: Boolean, onClick: () -> Unit) {
IconButton(onClick = onClick) {
val (icon, string) = if (expanded) Icons.Filled.KeyboardArrowUp to R.string.collapse_content else Icons.Filled.KeyboardArrowDown to R.string.expand_content
Icon(
imageVector = icon,
contentDescription = stringResource(string)
)
}
}

View File

@ -5,7 +5,9 @@ import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
@ -32,12 +34,14 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.patcher.worker.StepGroup import app.revanced.manager.patcher.worker.Step
import app.revanced.manager.patcher.worker.StepStatus import app.revanced.manager.patcher.worker.State
import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ArrowButton
import app.revanced.manager.ui.viewmodel.InstallerViewModel import app.revanced.manager.ui.viewmodel.InstallerViewModel
import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.APK_MIMETYPE
import kotlin.math.exp
import kotlin.math.floor import kotlin.math.floor
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -46,9 +50,10 @@ fun InstallerScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: InstallerViewModel vm: InstallerViewModel
) { ) {
val exportApkLauncher = rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export) val exportApkLauncher =
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
val patcherState by vm.patcherState.observeAsState(vm.initialState) val patcherState by vm.patcherState.observeAsState(vm.initialState)
val canInstall by remember { derivedStateOf { patcherState.status == true && (vm.installedPackageName != null || !vm.isInstalling) } } val canInstall by remember { derivedStateOf { patcherState.succeeded == true && (vm.installedPackageName != null || !vm.isInstalling) } }
AppScaffold( AppScaffold(
topBar = { topBar = {
@ -71,8 +76,8 @@ fun InstallerScreen(
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize() .fillMaxSize()
) { ) {
patcherState.stepGroups.forEach { patcherState.steps.forEach {
InstallGroup(it) InstallStep(it)
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Row( Row(
@ -103,7 +108,7 @@ fun InstallerScreen(
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt // Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
@Composable @Composable
fun InstallGroup(group: StepGroup) { fun InstallStep(step: Step) {
var expanded by rememberSaveable { mutableStateOf(true) } var expanded by rememberSaveable { mutableStateOf(true) }
Column( Column(
modifier = Modifier modifier = Modifier
@ -122,48 +127,39 @@ fun InstallGroup(group: StepGroup) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, end = 16.dp) .padding(start = 16.dp, end = 16.dp)
.run { if (expanded) { .background(if (expanded) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
background(MaterialTheme.colorScheme.secondaryContainer)
} else
background(MaterialTheme.colorScheme.surface)
}
) { ) {
StepIcon(group.status, 24.dp) StepIcon(step.state, 24.dp)
Text(text = stringResource(group.name), style = MaterialTheme.typography.titleMedium) Text(text = stringResource(step.name), style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = { expanded = !expanded }) { ArrowButton(expanded = expanded) {
if (expanded) { expanded = !expanded
Icon(
imageVector = Icons.Filled.KeyboardArrowUp,
contentDescription = "collapse"
)
} else {
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = "expand"
)
}
} }
} }
AnimatedVisibility(visible = expanded) { AnimatedVisibility(visible = expanded) {
val scrollState = rememberScrollState()
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.background.copy(0.6f)) .background(MaterialTheme.colorScheme.background.copy(0.6f))
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(scrollState)
.padding(16.dp) .padding(16.dp)
.padding(start = 4.dp) .padding(start = 4.dp)
) { ) {
group.steps.forEach { step.substeps.forEach {
var messageExpanded by rememberSaveable { mutableStateOf(true) }
val stacktrace = it.message
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
StepIcon(it.status, size = 18.dp) StepIcon(it.state, size = 18.dp)
Text( Text(
text = it.name, text = it.name,
@ -172,6 +168,20 @@ fun InstallGroup(group: StepGroup) {
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, true), modifier = Modifier.weight(1f, true),
) )
if (stacktrace != null) {
ArrowButton(expanded = messageExpanded) {
messageExpanded = !messageExpanded
}
}
}
AnimatedVisibility(visible = messageExpanded && stacktrace != null) {
Text(
text = stacktrace ?: "",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.secondary
)
} }
} }
} }
@ -180,31 +190,33 @@ fun InstallGroup(group: StepGroup) {
} }
@Composable @Composable
fun StepIcon(status: StepStatus, size: Dp) { fun StepIcon(status: State, size: Dp) {
val strokeWidth = Dp(floor(size.value / 10) + 1) val strokeWidth = Dp(floor(size.value / 10) + 1)
when (status) { when (status) {
StepStatus.COMPLETED -> Icon( State.COMPLETED -> Icon(
Icons.Filled.CheckCircle, Icons.Filled.CheckCircle,
contentDescription = "success", contentDescription = stringResource(R.string.step_completed),
tint = MaterialTheme.colorScheme.surfaceTint, tint = MaterialTheme.colorScheme.surfaceTint,
modifier = Modifier.size(size) modifier = Modifier.size(size)
) )
StepStatus.FAILURE -> Icon( State.FAILED -> Icon(
Icons.Filled.Cancel, Icons.Filled.Cancel,
contentDescription = "failed", contentDescription = stringResource(R.string.step_failed),
tint = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(size) modifier = Modifier.size(size)
) )
StepStatus.WAITING -> CircularProgressIndicator( State.WAITING -> CircularProgressIndicator(
strokeWidth = strokeWidth, strokeWidth = strokeWidth,
modifier = Modifier modifier = stringResource(R.string.step_running).let { description ->
.size(size) Modifier
.semantics { .size(size)
contentDescription = "waiting" .semantics {
} contentDescription = description
}
}
) )
} }
} }

View File

@ -20,16 +20,16 @@ import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.patcher.worker.PatcherProgressManager import app.revanced.manager.patcher.worker.PatcherProgressManager
import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.patcher.worker.StepGroup import app.revanced.manager.patcher.worker.Step
import app.revanced.manager.service.InstallService import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.AppInfo import app.revanced.manager.util.AppInfo
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.deserialize
import app.revanced.manager.util.serialize
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
@ -56,26 +56,22 @@ class InstallerViewModel(
val appButtonText by derivedStateOf { if (installedPackageName == null) R.string.install_app else R.string.open_app } val appButtonText by derivedStateOf { if (installedPackageName == null) R.string.install_app else R.string.open_app }
private val workManager = WorkManager.getInstance(app) private val workManager = WorkManager.getInstance(app)
private val patcherWorker = private val patcherWorker =
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData( .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData(
workDataOf( PatcherWorker.Args(
PatcherWorker.ARGS_KEY to input.path!!.absolutePath,
Json.Default.encodeToString( outputFile.path,
PatcherWorker.Args( selectedPatches,
input.path!!.absolutePath, input.packageName,
outputFile.path, input.packageInfo!!.versionName,
selectedPatches, ).serialize()
input.packageName,
input.packageInfo!!.versionName,
)
)
)
).build() ).build()
val initialState = PatcherState( val initialState = PatcherState(
status = null, succeeded = null,
stepGroups = PatcherProgressManager.generateGroupsList( steps = PatcherProgressManager.generateSteps(
app, app,
selectedPatches.flatMap { (_, selected) -> selected } selectedPatches.flatMap { (_, selected) -> selected }
) )
@ -83,16 +79,16 @@ class InstallerViewModel(
val patcherState = val patcherState =
workManager.getWorkInfoByIdLiveData(patcherWorker.id).map { workInfo: WorkInfo -> workManager.getWorkInfoByIdLiveData(patcherWorker.id).map { workInfo: WorkInfo ->
var status: Boolean? = null var status: Boolean? = null
val stepGroups = when (workInfo.state) { val steps = when (workInfo.state) {
WorkInfo.State.RUNNING -> workInfo.progress WorkInfo.State.RUNNING -> workInfo.progress
WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData.also { WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData.also {
status = workInfo.state == WorkInfo.State.SUCCEEDED status = workInfo.state == WorkInfo.State.SUCCEEDED
} }
else -> null else -> null
}?.let { PatcherProgressManager.groupsFromWorkData(it) } }?.deserialize<List<Step>>()
PatcherState(status, stepGroups ?: initialState.stepGroups) PatcherState(status, steps ?: initialState.steps)
} }
private val installBroadcastReceiver = object : BroadcastReceiver() { private val installBroadcastReceiver = object : BroadcastReceiver() {
@ -170,6 +166,5 @@ class InstallerViewModel(
} }
} }
data class PatcherState(val succeeded: Boolean?, val steps: List<Step>)
data class PatcherState(val status: Boolean?, val stepGroups: List<StepGroup>)
} }

View File

@ -12,9 +12,15 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.work.Data
import androidx.work.workDataOf
import io.ktor.http.Url import io.ktor.http.Url
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
typealias PatchesSelection = Map<String, List<String>> typealias PatchesSelection = Map<String, List<String>>
@ -55,7 +61,12 @@ inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, bl
try { try {
block() block()
} catch (error: Exception) { } catch (error: Exception) {
context.toast(context.getString(toastMsg, error.message ?: error.cause?.message ?: error::class.simpleName)) context.toast(
context.getString(
toastMsg,
error.message ?: error.cause?.message ?: error::class.simpleName
)
)
Log.e(tag, logMsg, error) Log.e(tag, logMsg, error)
} }
} }
@ -69,4 +80,14 @@ inline fun LifecycleOwner.launchAndRepeatWithViewLifecycle(
block() block()
} }
} }
} }
const val workDataKey = "payload"
@OptIn(ExperimentalSerializationApi::class)
inline fun <reified T> T.serialize(): Data =
workDataOf(workDataKey to Cbor.Default.encodeToByteArray(this))
@OptIn(ExperimentalSerializationApi::class)
inline fun <reified T> Data.deserialize(): T? =
getByteArray(workDataKey)?.let { Cbor.Default.decodeFromByteArray(it) }

View File

@ -85,6 +85,7 @@
<string name="sign_fail">Failed to sign Apk: %s</string> <string name="sign_fail">Failed to sign Apk: %s</string>
<string name="patcher_step_group_prepare">Preparation</string> <string name="patcher_step_group_prepare">Preparation</string>
<string name="patcher_step_load_patches">Load patches</string>
<string name="patcher_step_unpack">Unpack Apk</string> <string name="patcher_step_unpack">Unpack Apk</string>
<string name="patcher_step_integrations">Merge Integrations</string> <string name="patcher_step_integrations">Merge Integrations</string>
<string name="patcher_step_group_patching">Patching</string> <string name="patcher_step_group_patching">Patching</string>
@ -92,6 +93,13 @@
<string name="patcher_step_write_patched">Write patched Apk</string> <string name="patcher_step_write_patched">Write patched Apk</string>
<string name="patcher_notification_message">Patching in progress…</string> <string name="patcher_notification_message">Patching in progress…</string>
<string name="step_completed">completed</string>
<string name="step_failed">failed</string>
<string name="step_running">running</string>
<string name="expand_content">expand</string>
<string name="collapse_content">collapse</string>
<string name="more">More</string> <string name="more">More</string>
<string name="donate">Donate</string> <string name="donate">Donate</string>
<string name="website">Website</string> <string name="website">Website</string>