mirror of
https://github.com/revanced/revanced-manager-compose.git
synced 2025-04-30 06:14:25 +02:00
feat: show stacktrace in installer ui (#36)
This commit is contained in:
parent
41c521876a
commit
d59c57a882
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.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
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>)
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,3 +81,13 @@ inline fun LifecycleOwner.launchAndRepeatWithViewLifecycle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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="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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user