fix: pass worker inputs without serialization (#44)

Because androidx.work.Data sucks and causes our app to crash.
This commit is contained in:
Ax333l 2023-06-27 16:39:30 +02:00 committed by GitHub
parent bcdcdbe67a
commit 59a99c83a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 103 additions and 82 deletions

View File

@ -91,7 +91,6 @@ dependencies {
// KotlinX
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

View File

@ -5,6 +5,7 @@ import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.domain.repository.SourcePersistenceRepository
import app.revanced.manager.domain.repository.SourceRepository
import app.revanced.manager.domain.worker.WorkerRepository
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@ -14,4 +15,5 @@ val repositoryModule = module {
singleOf(::SourcePersistenceRepository)
singleOf(::PatchSelectionRepository)
singleOf(::SourceRepository)
singleOf(::WorkerRepository)
}

View File

@ -0,0 +1,7 @@
package app.revanced.manager.domain.worker
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
abstract class Worker<ARGS>(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters)

View File

@ -0,0 +1,36 @@
package app.revanced.manager.domain.worker
import android.app.Application
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import java.util.UUID
class WorkerRepository(app: Application) {
val workManager = WorkManager.getInstance(app)
/**
* The standard WorkManager communication APIs use [androidx.work.Data], which has too many limitations.
* We can get around those limits by passing inputs using global variables instead.
*/
val workerInputs = mutableMapOf<UUID, Any>()
@Suppress("UNCHECKED_CAST")
fun <A : Any, W : Worker<A>> claimInput(worker: W): A {
val data = workerInputs[worker.id] ?: throw IllegalStateException("Worker was not launched via WorkerRepository")
workerInputs.remove(worker.id)
return data as A
}
inline fun <reified W : Worker<A>, A : Any> launchExpedited(name: String, input: A): UUID {
val request =
OneTimeWorkRequest.Builder(W::class.java) // create Worker
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
workerInputs[request.id] = input
workManager.enqueueUniqueWork(name, ExistingWorkPolicy.REPLACE, request)
return request.id
}
}

View File

@ -3,10 +3,9 @@ package app.revanced.manager.patcher.worker
import android.content.Context
import androidx.annotation.StringRes
import app.revanced.manager.R
import app.revanced.manager.util.serialize
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.collections.immutable.toImmutableList
sealed class Progress {
object Unpacking : Progress()
@ -18,23 +17,19 @@ sealed class Progress {
object Saving : Progress()
}
@Serializable
enum class State {
WAITING, COMPLETED, FAILED
}
@Serializable
class SubStep(
val name: String,
val state: State = State.WAITING,
@SerialName("msg")
val message: String? = null
)
@Serializable
class Step(
@StringRes val name: Int,
val substeps: List<SubStep>,
val substeps: ImmutableList<SubStep>,
val state: State = State.WAITING
)
@ -58,7 +53,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
Step(step.name, step.substeps.mapIndexed { index, subStep ->
if (index != key.substep) subStep else SubStep(subStep.name, state, message)
}, newStepState)
}.toImmutableList(), newStepState)
}
val isFinal = isLastSubStep && key.step == steps.lastIndex
@ -95,7 +90,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
fun success() = updateCurrent(State.COMPLETED)
fun workData() = steps.serialize()
fun getProgress(): List<Step> = steps
companion object {
/**
@ -110,7 +105,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
private fun generatePatchesStep(selectedPatches: List<String>) = Step(
R.string.patcher_step_group_patching,
selectedPatches.map { SubStep(it) }
selectedPatches.map { SubStep(it) }.toImmutableList()
)
fun generateSteps(context: Context, selectedPatches: List<String>) = mutableListOf(

View File

@ -11,36 +11,39 @@ import android.os.PowerManager
import android.util.Log
import android.view.WindowManager
import androidx.core.content.ContextCompat
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import app.revanced.manager.R
import app.revanced.manager.domain.repository.SourceRepository
import app.revanced.manager.domain.worker.Worker
import app.revanced.manager.domain.worker.WorkerRepository
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.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
import java.io.FileNotFoundException
class PatcherWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters),
Worker<PatcherWorker.Args>(context, parameters),
KoinComponent {
private val sourceRepository: SourceRepository by inject()
private val workerRepository: WorkerRepository by inject()
@Serializable
data class Args(
val input: String,
val output: String,
val selectedPatches: PatchesSelection,
val packageName: String,
val packageVersion: String
val packageVersion: String,
val progress: MutableStateFlow<ImmutableList<Step>>
)
companion object {
@ -75,7 +78,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
return Result.failure()
}
val args = inputData.deserialize<Args>()!!
val args = workerRepository.claimInput(this)
try {
// This does not always show up for some reason.
@ -113,9 +116,11 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
val progressManager =
PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { it.value })
suspend fun updateProgress(progress: Progress) {
progressManager.handle(progress)
setProgress(progressManager.workData())
val progressFlow = args.progress
fun updateProgress(progress: Progress?) {
progress?.let { progressManager.handle(it) }
progressFlow.value = progressManager.getProgress().toImmutableList()
}
return try {
@ -143,11 +148,13 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
Log.i(tag, "Patching succeeded".logFmt())
progressManager.success()
Result.success(progressManager.workData())
Result.success()
} catch (e: Exception) {
Log.e(tag, "Got exception while patching".logFmt(), e)
progressManager.failure(e)
Result.failure(progressManager.workData())
Result.failure()
} finally {
updateProgress(null)
}
}
}

View File

@ -11,8 +11,6 @@ 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
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.*
@ -33,6 +31,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.patcher.worker.Step
import app.revanced.manager.patcher.worker.State
@ -41,7 +40,6 @@ 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)
@ -52,8 +50,9 @@ fun InstallerScreen(
) {
val exportApkLauncher =
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
val patcherState by vm.patcherState.observeAsState(vm.initialState)
val canInstall by remember { derivedStateOf { patcherState.succeeded == true && (vm.installedPackageName != null || !vm.isInstalling) } }
val patcherState by vm.patcherState.observeAsState(null)
val steps by vm.progress.collectAsStateWithLifecycle()
val canInstall by remember { derivedStateOf { patcherState == true && (vm.installedPackageName != null || !vm.isInstalling) } }
AppScaffold(
topBar = {
@ -77,7 +76,7 @@ fun InstallerScreen(
.verticalScroll(rememberScrollState())
.fillMaxSize()
) {
patcherState.steps.forEach {
steps.forEach {
InstallStep(it)
}
Spacer(modifier = Modifier.weight(1f))

View File

@ -18,18 +18,19 @@ import androidx.lifecycle.map
import androidx.work.*
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.R
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.worker.PatcherProgressManager
import app.revanced.manager.patcher.worker.PatcherWorker
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.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
@ -43,6 +44,7 @@ class InstallerViewModel(
private val keystoreManager: KeystoreManager by inject()
private val app: Application by inject()
private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject()
val packageName: String = input.packageName
private val outputFile = File(app.cacheDir, "output.apk")
@ -57,38 +59,31 @@ class InstallerViewModel(
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(
PatcherWorker.Args(
input.path!!.absolutePath,
outputFile.path,
selectedPatches,
input.packageName,
input.packageInfo!!.versionName,
).serialize()
).build()
private val _progress = MutableStateFlow(PatcherProgressManager.generateSteps(
app,
selectedPatches.flatMap { (_, selected) -> selected }
).toImmutableList())
val progress = _progress.asStateFlow()
val initialState = PatcherState(
succeeded = null,
steps = PatcherProgressManager.generateSteps(
app,
selectedPatches.flatMap { (_, selected) -> selected }
private val patcherWorkerId =
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args(
input.path!!.absolutePath,
outputFile.path,
selectedPatches,
input.packageName,
input.packageInfo!!.versionName,
_progress
)
)
)
val patcherState =
workManager.getWorkInfoByIdLiveData(patcherWorker.id).map { workInfo: WorkInfo ->
var status: Boolean? = null
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
}
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> true
WorkInfo.State.FAILED -> false
else -> null
}?.deserialize<List<Step>>()
PatcherState(status, steps ?: initialState.steps)
}
}
private val installBroadcastReceiver = object : BroadcastReceiver() {
@ -114,7 +109,6 @@ class InstallerViewModel(
}
init {
workManager.enqueueUniqueWork("patching", ExistingWorkPolicy.KEEP, patcherWorker)
app.registerReceiver(installBroadcastReceiver, IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION)
addAction(UninstallService.APP_UNINSTALL_ACTION)
@ -124,7 +118,7 @@ class InstallerViewModel(
override fun onCleared() {
super.onCleared()
app.unregisterReceiver(installBroadcastReceiver)
workManager.cancelWorkById(patcherWorker.id)
workManager.cancelWorkById(patcherWorkerId)
outputFile.delete()
signedFile.delete()
@ -165,6 +159,4 @@ class InstallerViewModel(
isInstalling = false
}
}
data class PatcherState(val succeeded: Boolean?, val steps: List<Step>)
}

View File

@ -12,8 +12,6 @@ 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.ExperimentalCoroutinesApi
@ -21,10 +19,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
typealias PatchesSelection = Map<Int, List<String>>
@ -98,14 +92,4 @@ inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
combine(iterable.map(transformer)) {
combiner(it)
}
}
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) }
}