diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 32afea86..4d73b30d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt index c52d4859..b10fc483 100644 --- a/app/src/main/java/app/revanced/manager/patcher/Session.kt +++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt @@ -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 + } } } diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherProgressManager.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherProgressManager.kt index 04032e17..f038f504 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherProgressManager.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherProgressManager.kt @@ -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, - val status: StepStatus = StepStatus.WAITING + val substeps: List, + val state: State = State.WAITING ) class PatcherProgressManager(context: Context, selectedPatches: List) { - 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) = 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>(it) } - } - - fun groupsToWorkData() = workDataOf(WORK_DATA_KEY to Json.Default.encodeToString(stepGroups)) - - private var currentStep: StepKey? = null - - private fun MutableList.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) { + 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) = Step( + R.string.patcher_step_group_patching, + selectedPatches.map { SubStep(it) } + ) + + fun generateSteps(context: Context, selectedPatches: List) = 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) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 61ca91b2..a279e9c8 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -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(inputData.getString(ARGS_KEY)!!) + val args = inputData.deserialize()!! 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()) } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt b/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt new file mode 100644 index 00000000..0909b2c5 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt index 25dc6d4a..0e86b1cf 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt @@ -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 + } + } ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt index 029163b8..1546d2db 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt @@ -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>() - 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) + data class PatcherState(val succeeded: Boolean?, val steps: List) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index 22bdf3a3..c5aec637 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -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> @@ -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() } } -} \ No newline at end of file +} + +const val workDataKey = "payload" + +@OptIn(ExperimentalSerializationApi::class) +inline fun T.serialize(): Data = + workDataOf(workDataKey to Cbor.Default.encodeToByteArray(this)) + +@OptIn(ExperimentalSerializationApi::class) +inline fun Data.deserialize(): T? = + getByteArray(workDataKey)?.let { Cbor.Default.decodeFromByteArray(it) } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d315df69..1e5c7a7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,6 +85,7 @@ Failed to sign Apk: %s Preparation + Load patches Unpack Apk Merge Integrations Patching @@ -92,6 +93,13 @@ Write patched Apk Patching in progress… + completed + failed + running + + expand + collapse + More Donate Website