diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 74b533f6..00313a49 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt index c7f458af..4c929bb3 100644 --- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt +++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt b/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt new file mode 100644 index 00000000..85dc3df3 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt @@ -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(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt new file mode 100644 index 00000000..222a31c4 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt @@ -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() + + @Suppress("UNCHECKED_CAST") + fun > 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 , 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 + } +} \ No newline at end of file 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 f038f504..99379e71 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 @@ -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, + val substeps: ImmutableList, val state: State = State.WAITING ) @@ -58,7 +53,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List) { 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) { fun success() = updateCurrent(State.COMPLETED) - fun workData() = steps.serialize() + fun getProgress(): List = steps companion object { /** @@ -110,7 +105,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List) { private fun generatePatchesStep(selectedPatches: List) = Step( R.string.patcher_step_group_patching, - selectedPatches.map { SubStep(it) } + selectedPatches.map { SubStep(it) }.toImmutableList() ) fun generateSteps(context: Context, selectedPatches: List) = mutableListOf( 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 18802c62..8a31a482 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 @@ -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(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> ) companion object { @@ -75,7 +78,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : return Result.failure() } - val args = inputData.deserialize()!! + 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) } } } \ 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 984620dc..3d48dd87 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 @@ -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)) 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 1546d2db..a5739bdb 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 @@ -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( + "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>() - - 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) } \ 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 7d233b01..2aa5daba 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -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> @@ -98,14 +92,4 @@ inline fun Flow>.flatMapLatestAndCombine( combine(iterable.map(transformer)) { combiner(it) } -} - -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 +} \ No newline at end of file