mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-10 01:01:56 +01:00
feat: show stacktrace in installer ui (#36)
This commit is contained in:
parent
6309e8bdf5
commit
5681c917c5
9 changed files with 236 additions and 172 deletions
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
@ -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)
|
||||
val steps = generateSteps(context, selectedPatches)
|
||||
private var currentStep: StepKey? = StepKey(0, 0)
|
||||
|
||||
companion object {
|
||||
private const val WORK_DATA_KEY = "progress"
|
||||
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
|
||||
|
||||
/**
|
||||
* 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 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()
|
||||
|
||||
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() {
|
||||
setCurrentStepStatus(StepStatus.COMPLETED)
|
||||
}
|
||||
private data class StepKey(val step: Int, val substep: Int)
|
||||
}
|
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -105,29 +104,38 @@ 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)
|
||||
|
@ -135,11 +143,11 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,7 +5,9 @@ import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
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 app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.worker.StepGroup
|
||||
import app.revanced.manager.patcher.worker.StepStatus
|
||||
import app.revanced.manager.patcher.worker.Step
|
||||
import app.revanced.manager.patcher.worker.State
|
||||
import app.revanced.manager.ui.component.AppScaffold
|
||||
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.util.APK_MIMETYPE
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.floor
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
@ -46,9 +50,10 @@ fun InstallerScreen(
|
|||
onBackClick: () -> Unit,
|
||||
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 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(
|
||||
topBar = {
|
||||
|
@ -71,8 +76,8 @@ fun InstallerScreen(
|
|||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
patcherState.stepGroups.forEach {
|
||||
InstallGroup(it)
|
||||
patcherState.steps.forEach {
|
||||
InstallStep(it)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun InstallGroup(group: StepGroup) {
|
||||
fun InstallStep(step: Step) {
|
||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
@ -122,48 +127,39 @@ fun InstallGroup(group: StepGroup) {
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp)
|
||||
.run { if (expanded) {
|
||||
background(MaterialTheme.colorScheme.secondaryContainer)
|
||||
} else
|
||||
background(MaterialTheme.colorScheme.surface)
|
||||
}
|
||||
.background(if (expanded) MaterialTheme.colorScheme.secondaryContainer else 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))
|
||||
|
||||
IconButton(onClick = { expanded = !expanded }) {
|
||||
if (expanded) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||
contentDescription = "collapse"
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.KeyboardArrowDown,
|
||||
contentDescription = "expand"
|
||||
)
|
||||
}
|
||||
ArrowButton(expanded = expanded) {
|
||||
expanded = !expanded
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background.copy(0.6f))
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(16.dp)
|
||||
.padding(start = 4.dp)
|
||||
) {
|
||||
group.steps.forEach {
|
||||
step.substeps.forEach {
|
||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||
val stacktrace = it.message
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
StepIcon(it.status, size = 18.dp)
|
||||
StepIcon(it.state, size = 18.dp)
|
||||
|
||||
Text(
|
||||
text = it.name,
|
||||
|
@ -172,6 +168,20 @@ fun InstallGroup(group: StepGroup) {
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
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
|
||||
fun StepIcon(status: StepStatus, size: Dp) {
|
||||
fun StepIcon(status: State, size: Dp) {
|
||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||
|
||||
when (status) {
|
||||
StepStatus.COMPLETED -> Icon(
|
||||
State.COMPLETED -> Icon(
|
||||
Icons.Filled.CheckCircle,
|
||||
contentDescription = "success",
|
||||
contentDescription = stringResource(R.string.step_completed),
|
||||
tint = MaterialTheme.colorScheme.surfaceTint,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
StepStatus.FAILURE -> Icon(
|
||||
State.FAILED -> Icon(
|
||||
Icons.Filled.Cancel,
|
||||
contentDescription = "failed",
|
||||
contentDescription = stringResource(R.string.step_failed),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
StepStatus.WAITING -> CircularProgressIndicator(
|
||||
State.WAITING -> CircularProgressIndicator(
|
||||
strokeWidth = strokeWidth,
|
||||
modifier = Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = "waiting"
|
||||
}
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,16 +20,16 @@ import app.revanced.manager.domain.manager.KeystoreManager
|
|||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.worker.PatcherProgressManager
|
||||
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.UninstallService
|
||||
import app.revanced.manager.util.AppInfo
|
||||
import app.revanced.manager.util.PM
|
||||
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.toast
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
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 }
|
||||
|
||||
private val workManager = WorkManager.getInstance(app)
|
||||
|
||||
private val patcherWorker =
|
||||
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData(
|
||||
workDataOf(
|
||||
PatcherWorker.ARGS_KEY to
|
||||
Json.Default.encodeToString(
|
||||
PatcherWorker.Args(
|
||||
input.path!!.absolutePath,
|
||||
outputFile.path,
|
||||
selectedPatches,
|
||||
input.packageName,
|
||||
input.packageInfo!!.versionName,
|
||||
)
|
||||
)
|
||||
)
|
||||
PatcherWorker.Args(
|
||||
input.path!!.absolutePath,
|
||||
outputFile.path,
|
||||
selectedPatches,
|
||||
input.packageName,
|
||||
input.packageInfo!!.versionName,
|
||||
).serialize()
|
||||
).build()
|
||||
|
||||
val initialState = PatcherState(
|
||||
status = null,
|
||||
stepGroups = PatcherProgressManager.generateGroupsList(
|
||||
succeeded = null,
|
||||
steps = PatcherProgressManager.generateSteps(
|
||||
app,
|
||||
selectedPatches.flatMap { (_, selected) -> selected }
|
||||
)
|
||||
|
@ -83,16 +79,16 @@ class InstallerViewModel(
|
|||
val patcherState =
|
||||
workManager.getWorkInfoByIdLiveData(patcherWorker.id).map { workInfo: WorkInfo ->
|
||||
var status: Boolean? = null
|
||||
val stepGroups = when (workInfo.state) {
|
||||
val steps = when (workInfo.state) {
|
||||
WorkInfo.State.RUNNING -> workInfo.progress
|
||||
WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData.also {
|
||||
status = workInfo.state == WorkInfo.State.SUCCEEDED
|
||||
}
|
||||
|
||||
else -> null
|
||||
}?.let { PatcherProgressManager.groupsFromWorkData(it) }
|
||||
}?.deserialize<List<Step>>()
|
||||
|
||||
PatcherState(status, stepGroups ?: initialState.stepGroups)
|
||||
PatcherState(status, steps ?: initialState.steps)
|
||||
}
|
||||
|
||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
||||
|
@ -170,6 +166,5 @@ class InstallerViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
data class PatcherState(val status: Boolean?, val stepGroups: List<StepGroup>)
|
||||
data class PatcherState(val succeeded: Boolean?, val steps: List<Step>)
|
||||
}
|
|
@ -12,9 +12,15 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.work.Data
|
||||
import androidx.work.workDataOf
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
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>>
|
||||
|
||||
|
@ -55,7 +61,12 @@ inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, bl
|
|||
try {
|
||||
block()
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
@ -69,4 +80,14 @@ inline fun LifecycleOwner.launchAndRepeatWithViewLifecycle(
|
|||
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) }
|
|
@ -85,6 +85,7 @@
|
|||
<string name="sign_fail">Failed to sign Apk: %s</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_integrations">Merge Integrations</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_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="donate">Donate</string>
|
||||
<string name="website">Website</string>
|
||||
|
|
Loading…
Reference in a new issue