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 1eac42dab8
commit 4302ea8832
9 changed files with 103 additions and 82 deletions

View File

@ -91,7 +91,6 @@ dependencies {
// KotlinX // KotlinX
val serializationVersion = "1.5.1" val serializationVersion = "1.5.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion") 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

View File

@ -5,6 +5,7 @@ import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.network.api.ManagerAPI import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.domain.repository.SourcePersistenceRepository import app.revanced.manager.domain.repository.SourcePersistenceRepository
import app.revanced.manager.domain.repository.SourceRepository import app.revanced.manager.domain.repository.SourceRepository
import app.revanced.manager.domain.worker.WorkerRepository
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
@ -14,4 +15,5 @@ val repositoryModule = module {
singleOf(::SourcePersistenceRepository) singleOf(::SourcePersistenceRepository)
singleOf(::PatchSelectionRepository) singleOf(::PatchSelectionRepository)
singleOf(::SourceRepository) 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 android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.util.serialize import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.serialization.SerialName import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.Serializable
sealed class Progress { sealed class Progress {
object Unpacking : Progress() object Unpacking : Progress()
@ -18,23 +17,19 @@ sealed class Progress {
object Saving : Progress() object Saving : Progress()
} }
@Serializable
enum class State { enum class State {
WAITING, COMPLETED, FAILED WAITING, COMPLETED, FAILED
} }
@Serializable
class SubStep( class SubStep(
val name: String, val name: String,
val state: State = State.WAITING, val state: State = State.WAITING,
@SerialName("msg")
val message: String? = null val message: String? = null
) )
@Serializable
class Step( class Step(
@StringRes val name: Int, @StringRes val name: Int,
val substeps: List<SubStep>, val substeps: ImmutableList<SubStep>,
val state: State = State.WAITING val state: State = State.WAITING
) )
@ -58,7 +53,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
Step(step.name, step.substeps.mapIndexed { index, subStep -> Step(step.name, step.substeps.mapIndexed { index, subStep ->
if (index != key.substep) subStep else SubStep(subStep.name, state, message) if (index != key.substep) subStep else SubStep(subStep.name, state, message)
}, newStepState) }.toImmutableList(), newStepState)
} }
val isFinal = isLastSubStep && key.step == steps.lastIndex val isFinal = isLastSubStep && key.step == steps.lastIndex
@ -95,7 +90,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
fun success() = updateCurrent(State.COMPLETED) fun success() = updateCurrent(State.COMPLETED)
fun workData() = steps.serialize() fun getProgress(): List<Step> = steps
companion object { companion object {
/** /**
@ -110,7 +105,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
private fun generatePatchesStep(selectedPatches: List<String>) = Step( private fun generatePatchesStep(selectedPatches: List<String>) = Step(
R.string.patcher_step_group_patching, R.string.patcher_step_group_patching,
selectedPatches.map { SubStep(it) } selectedPatches.map { SubStep(it) }.toImmutableList()
) )
fun generateSteps(context: Context, selectedPatches: List<String>) = mutableListOf( fun generateSteps(context: Context, selectedPatches: List<String>) = mutableListOf(

View File

@ -11,36 +11,39 @@ import android.os.PowerManager
import android.util.Log import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.repository.SourceRepository 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.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.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
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
import java.io.FileNotFoundException import java.io.FileNotFoundException
class PatcherWorker(context: Context, parameters: WorkerParameters) : class PatcherWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters), Worker<PatcherWorker.Args>(context, parameters),
KoinComponent { KoinComponent {
private val sourceRepository: SourceRepository by inject() private val sourceRepository: SourceRepository by inject()
private val workerRepository: WorkerRepository by inject()
@Serializable
data class Args( data class Args(
val input: String, val input: String,
val output: String, val output: String,
val selectedPatches: PatchesSelection, val selectedPatches: PatchesSelection,
val packageName: String, val packageName: String,
val packageVersion: String val packageVersion: String,
val progress: MutableStateFlow<ImmutableList<Step>>
) )
companion object { companion object {
@ -75,7 +78,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
return Result.failure() return Result.failure()
} }
val args = inputData.deserialize<Args>()!! val args = workerRepository.claimInput(this)
try { try {
// This does not always show up for some reason. // This does not always show up for some reason.
@ -113,9 +116,11 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
val progressManager = val progressManager =
PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { it.value }) PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { it.value })
suspend fun updateProgress(progress: Progress) { val progressFlow = args.progress
progressManager.handle(progress)
setProgress(progressManager.workData()) fun updateProgress(progress: Progress?) {
progress?.let { progressManager.handle(it) }
progressFlow.value = progressManager.getProgress().toImmutableList()
} }
return try { return try {
@ -143,11 +148,13 @@ 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.workData()) Result.success()
} 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(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.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
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.HelpOutline
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.* 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.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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.patcher.worker.Step import app.revanced.manager.patcher.worker.Step
import app.revanced.manager.patcher.worker.State 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.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)
@ -52,8 +50,9 @@ fun InstallerScreen(
) { ) {
val exportApkLauncher = val exportApkLauncher =
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export) rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
val patcherState by vm.patcherState.observeAsState(vm.initialState) val patcherState by vm.patcherState.observeAsState(null)
val canInstall by remember { derivedStateOf { patcherState.succeeded == true && (vm.installedPackageName != null || !vm.isInstalling) } } val steps by vm.progress.collectAsStateWithLifecycle()
val canInstall by remember { derivedStateOf { patcherState == true && (vm.installedPackageName != null || !vm.isInstalling) } }
AppScaffold( AppScaffold(
topBar = { topBar = {
@ -77,7 +76,7 @@ fun InstallerScreen(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.fillMaxSize() .fillMaxSize()
) { ) {
patcherState.steps.forEach { steps.forEach {
InstallStep(it) InstallStep(it)
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))

View File

@ -18,18 +18,19 @@ import androidx.lifecycle.map
import androidx.work.* import androidx.work.*
import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.R 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.PatcherProgressManager
import app.revanced.manager.patcher.worker.PatcherWorker 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.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.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
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
@ -43,6 +44,7 @@ class InstallerViewModel(
private val keystoreManager: KeystoreManager by inject() private val keystoreManager: KeystoreManager by inject()
private val app: Application by inject() private val app: Application by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject()
val packageName: String = input.packageName val packageName: String = input.packageName
private val outputFile = File(app.cacheDir, "output.apk") private val outputFile = File(app.cacheDir, "output.apk")
@ -57,38 +59,31 @@ class InstallerViewModel(
private val workManager = WorkManager.getInstance(app) private val workManager = WorkManager.getInstance(app)
private val patcherWorker = private val _progress = MutableStateFlow(PatcherProgressManager.generateSteps(
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker app,
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData( selectedPatches.flatMap { (_, selected) -> selected }
PatcherWorker.Args( ).toImmutableList())
input.path!!.absolutePath, val progress = _progress.asStateFlow()
outputFile.path,
selectedPatches,
input.packageName,
input.packageInfo!!.versionName,
).serialize()
).build()
val initialState = PatcherState( private val patcherWorkerId =
succeeded = null, workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
steps = PatcherProgressManager.generateSteps( "patching", PatcherWorker.Args(
app, input.path!!.absolutePath,
selectedPatches.flatMap { (_, selected) -> selected } outputFile.path,
selectedPatches,
input.packageName,
input.packageInfo!!.versionName,
_progress
)
) )
)
val patcherState = val patcherState =
workManager.getWorkInfoByIdLiveData(patcherWorker.id).map { workInfo: WorkInfo -> workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
var status: Boolean? = null when (workInfo.state) {
val steps = when (workInfo.state) { WorkInfo.State.SUCCEEDED -> true
WorkInfo.State.RUNNING -> workInfo.progress WorkInfo.State.FAILED -> false
WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData.also {
status = workInfo.state == WorkInfo.State.SUCCEEDED
}
else -> null else -> null
}?.deserialize<List<Step>>() }
PatcherState(status, steps ?: initialState.steps)
} }
private val installBroadcastReceiver = object : BroadcastReceiver() { private val installBroadcastReceiver = object : BroadcastReceiver() {
@ -114,7 +109,6 @@ class InstallerViewModel(
} }
init { init {
workManager.enqueueUniqueWork("patching", ExistingWorkPolicy.KEEP, patcherWorker)
app.registerReceiver(installBroadcastReceiver, IntentFilter().apply { app.registerReceiver(installBroadcastReceiver, IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION) addAction(InstallService.APP_INSTALL_ACTION)
addAction(UninstallService.APP_UNINSTALL_ACTION) addAction(UninstallService.APP_UNINSTALL_ACTION)
@ -124,7 +118,7 @@ class InstallerViewModel(
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
app.unregisterReceiver(installBroadcastReceiver) app.unregisterReceiver(installBroadcastReceiver)
workManager.cancelWorkById(patcherWorker.id) workManager.cancelWorkById(patcherWorkerId)
outputFile.delete() outputFile.delete()
signedFile.delete() signedFile.delete()
@ -165,6 +159,4 @@ class InstallerViewModel(
isInstalling = false 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.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.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -21,10 +19,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
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<Int, List<String>> typealias PatchesSelection = Map<Int, List<String>>
@ -98,14 +92,4 @@ inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
combine(iterable.map(transformer)) { combine(iterable.map(transformer)) {
combiner(it) 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) }