Skip to content

Commit

Permalink
feat: show stacktrace in installer ui (ReVanced#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
Axelen123 authored Jun 17, 2023
1 parent 6309e8b commit 5681c91
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 174 deletions.
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ dependencies {
implementation("me.zhanghai.android.appiconloader:appiconloader-coil:1.5.0")

// 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")

// Room
Expand Down
9 changes: 4 additions & 5 deletions app/src/main/java/app/revanced/manager/patcher/Session.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ class Session(
private val input: File,
private val onProgress: suspend (Progress) -> Unit = { }
) : Closeable {
class PatchFailedException(val patchName: String, cause: Throwable?) :
Exception("Got exception while executing $patchName", cause)

private val logger = LogcatLogger
private val temporary = File(cacheDir).resolve("manager").also { it.mkdirs() }
private val patcher = Patcher(
Expand All @@ -48,9 +45,11 @@ class Session(
return@forEach
}
logger.error("$patch failed:")
result.exceptionOrNull()!!.printStackTrace()
result.exceptionOrNull()!!.let {
logger.error(result.exceptionOrNull()!!.stackTraceToString())

throw PatchFailedException(patch, result.exceptionOrNull())
throw it
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ package app.revanced.manager.patcher.worker

import android.content.Context
import androidx.annotation.StringRes
import androidx.work.Data
import androidx.work.workDataOf
import app.revanced.manager.R
import app.revanced.manager.util.serialize
import kotlinx.collections.immutable.persistentListOf
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

sealed class Progress {
object Unpacking : Progress()
Expand All @@ -21,117 +19,116 @@ sealed class Progress {
}

@Serializable
enum class StepStatus {
WAITING,
COMPLETED,
FAILURE,
enum class State {
WAITING, COMPLETED, FAILED
}

@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
class StepGroup(
class Step(
@StringRes val name: Int,
val steps: List<Step>,
val status: StepStatus = StepStatus.WAITING
val substeps: List<SubStep>,
val state: State = State.WAITING
)

class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
val stepGroups = generateGroupsList(context, selectedPatches)

companion object {
private const val WORK_DATA_KEY = "progress"

/**
* A map of [Progress] to the corresponding position in [stepGroups]
*/
private val stepKeyMap = mapOf(
Progress.Unpacking to StepKey(0, 0),
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
val steps = generateSteps(context, selectedPatches)
private var currentStep: StepKey? = StepKey(0, 0)

private fun update(key: StepKey, state: State, message: String? = null) {
val isLastSubStep: Boolean
steps[key.step] = steps[key.step].let { step ->
isLastSubStep = key.substep == step.substeps.lastIndex

val newStepState = when {
// This step failed because one of its sub-steps failed.
state == State.FAILED -> State.FAILED
// All sub-steps succeeded.
state == State.COMPLETED && isLastSubStep -> State.COMPLETED
// Keep the old status.
else -> group.status
else -> step.state
}

StepGroup(group.name, group.steps.toMutableList().mutateIndex(key.stepIndex) { step ->
Step(step.name, newStatus)
}, newGroupStatus)
Step(step.name, step.substeps.mapIndexed { index, subStep ->
if (index != key.substep) subStep else SubStep(subStep.name, state, message)
}, 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.
currentStep = when {
isFinalStep -> null // Final step has been completed.
isLastStepOfGroup -> StepKey(key.groupIndex + 1, 0) // Move to the next group.
isFinal -> null // Final step has been completed.
isLastSubStep -> StepKey(key.step + 1, 0) // Move to the next step.
else -> StepKey(
key.groupIndex,
key.stepIndex + 1
) // Move to the next step of this group.
key.step,
key.substep + 1
) // Move to the next sub-step.
}
}
}

private fun setCurrentStepStatus(newStatus: StepStatus) =
currentStep?.let { updateStepStatus(it, newStatus) }
fun replacePatchesList(newList: List<String>) {
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 {
stepKeyMap[progress]?.let { currentStep = it }
}

fun failure() {
// TODO: associate the exception with the step that just failed.
setCurrentStepStatus(StepStatus.FAILURE)
}
fun failure(error: Throwable) = updateCurrent(
State.FAILED,
error.stackTraceToString()
)

fun success() = updateCurrent(State.COMPLETED)

fun workData() = steps.serialize()

fun success() {
setCurrentStepStatus(StepStatus.COMPLETED)
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)))
)
)
}

private data class StepKey(val step: Int, val substep: Int)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import app.revanced.manager.domain.repository.SourceRepository
import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.deserialize
import app.revanced.manager.util.tag
import app.revanced.patcher.extensions.PatchExtensions.patchName
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
Expand All @@ -44,7 +44,6 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
)

companion object {
const val ARGS_KEY = "args"
private const val logPrefix = "[Worker]:"
private fun String.logFmt() = "$logPrefix $this"
}
Expand Down Expand Up @@ -76,7 +75,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
return Result.failure()
}

val args = Json.decodeFromString<Args>(inputData.getString(ARGS_KEY)!!)
val args = inputData.deserialize<Args>()!!

try {
// This does not always show up for some reason.
Expand Down Expand Up @@ -105,41 +104,50 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
Aapt.binary(applicationContext)?.absolutePath
?: 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 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 =
PatcherProgressManager(applicationContext, patchList.map { it.patchName })
PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { it.value })

suspend fun updateProgress(progress: Progress) {
progressManager.handle(progress)
setProgress(progressManager.groupsToWorkData())
setProgress(progressManager.workData())
}

updateProgress(Progress.Unpacking)

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)
}.use { session ->
session.run(File(args.output), patchList, integrations)
}

Log.i(tag, "Patching succeeded".logFmt())
progressManager.success()
Result.success(progressManager.groupsToWorkData())
Result.success(progressManager.workData())
} catch (e: Exception) {
Log.e(tag, "Got exception while patching".logFmt(), e)
progressManager.failure()
Result.failure(progressManager.groupsToWorkData())
progressManager.failure(e)
Result.failure(progressManager.workData())
}
}
}
22 changes: 22 additions & 0 deletions app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
Loading

0 comments on commit 5681c91

Please sign in to comment.