From f180f20905dbe00824cb66fb41b039b982f702e8 Mon Sep 17 00:00:00 2001 From: Ax333l Date: Fri, 9 Jun 2023 17:34:10 +0200 Subject: [PATCH] fix(installer): properly track worker state (#32) --- app/build.gradle.kts | 1 + .../patcher/worker/PatcherProgressManager.kt | 45 ++++----- .../manager/patcher/worker/PatcherWorker.kt | 2 +- .../manager/service/InstallService.kt | 3 + .../manager/ui/screen/InstallerScreen.kt | 15 ++- .../ui/viewmodel/InstallerViewModel.kt | 98 ++++++++++--------- .../main/java/app/revanced/manager/util/PM.kt | 5 + app/src/main/res/values/strings.xml | 3 + 8 files changed, 94 insertions(+), 78 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 826a072..32afea8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,6 +72,7 @@ dependencies { implementation(platform("androidx.compose:compose-bom:2023.05.01")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.runtime:runtime-livedata") implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material3:material3") 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 6bb46b3..04032e1 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 @@ -5,10 +5,7 @@ import androidx.annotation.StringRes import androidx.work.Data import androidx.work.workDataOf import app.revanced.manager.R -import app.revanced.manager.patcher.Session -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -34,22 +31,25 @@ enum class StepStatus { class Step(val name: String, val status: StepStatus = StepStatus.WAITING) @Serializable -class StepGroup(@StringRes val name: Int, val steps: ImmutableList, val status: StepStatus = StepStatus.WAITING) +class StepGroup( + @StringRes val name: Int, + val steps: List, + val status: StepStatus = StepStatus.WAITING +) class PatcherProgressManager(context: Context, selectedPatches: List) { val stepGroups = generateGroupsList(context, selectedPatches) companion object { - private const val PATCHES = 1 private const val WORK_DATA_KEY = "progress" /** - * A map of [Session.Progress] to the corresponding position in [stepGroups] + * 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(PATCHES, 0), + Progress.PatchingStart to StepKey(1, 0), Progress.Saving to StepKey(2, 0), ) @@ -63,7 +63,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List) { ), StepGroup( R.string.patcher_step_group_patching, - selectedPatches.map { Step(it) }.toImmutableList() + selectedPatches.map { Step(it) } ), StepGroup( R.string.patcher_step_group_saving, @@ -86,7 +86,8 @@ class PatcherProgressManager(context: Context, selectedPatches: List) { private fun updateStepStatus(key: StepKey, newStatus: StepStatus) { var isLastStepOfGroup = false stepGroups.mutateIndex(key.groupIndex) { group -> - isLastStepOfGroup = key.stepIndex == group.steps.size - 1 + isLastStepOfGroup = key.stepIndex == group.steps.lastIndex + val newGroupStatus = when { // This group failed if a step in it failed. newStatus == StepStatus.FAILURE -> StepStatus.FAILURE @@ -98,37 +99,31 @@ class PatcherProgressManager(context: Context, selectedPatches: List) { StepGroup(group.name, group.steps.toMutableList().mutateIndex(key.stepIndex) { step -> Step(step.name, newStatus) - }.toImmutableList(), newGroupStatus) + }, newGroupStatus) } - val isFinalStep = isLastStepOfGroup && key.groupIndex == stepGroups.size - 1 + val isFinalStep = isLastStepOfGroup && key.groupIndex == stepGroups.lastIndex if (newStatus == StepStatus.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. - else -> StepKey(key.groupIndex, key.stepIndex + 1) // Move to the next step of this group. + else -> StepKey( + key.groupIndex, + key.stepIndex + 1 + ) // Move to the next step of this group. } } } - private fun setCurrentStepStatus(newStatus: StepStatus) = currentStep?.let { updateStepStatus(it, newStatus) } + private fun setCurrentStepStatus(newStatus: StepStatus) = + currentStep?.let { updateStepStatus(it, newStatus) } private data class StepKey(val groupIndex: Int, val stepIndex: Int) - fun handle(progress: Progress) { - if (progress is Progress.PatchSuccess) { - val patchStepKey = StepKey( - PATCHES, - stepGroups[PATCHES].steps.indexOfFirst { it.name == progress.patchName }) - - updateStepStatus(patchStepKey, StepStatus.COMPLETED) - } else { - currentStep?.let { updateStepStatus(it, StepStatus.COMPLETED) } - - currentStep = stepKeyMap[progress]!! - } + fun handle(progress: Progress) = success().also { + stepKeyMap[progress]?.let { currentStep = it } } fun failure() { 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 ed272db..664b921 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 @@ -60,7 +60,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineW } val progressManager = - PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { (_, selected) -> selected }) + PatcherProgressManager(applicationContext, patchList.map { it.patchName }) suspend fun updateProgress(progress: Progress) { progressManager.handle(progress) diff --git a/app/src/main/java/app/revanced/manager/service/InstallService.kt b/app/src/main/java/app/revanced/manager/service/InstallService.kt index d7b0f49..420a5dc 100644 --- a/app/src/main/java/app/revanced/manager/service/InstallService.kt +++ b/app/src/main/java/app/revanced/manager/service/InstallService.kt @@ -14,6 +14,7 @@ class InstallService : Service() { ): Int { val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) when (extraStatus) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { startActivity(if (Build.VERSION.SDK_INT >= 33) { @@ -30,6 +31,7 @@ class InstallService : Service() { action = APP_INSTALL_ACTION putExtra(EXTRA_INSTALL_STATUS, extraStatus) putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage) + putExtra(EXTRA_PACKAGE_NAME, extraPackageName) }) } } @@ -44,6 +46,7 @@ class InstallService : Service() { const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS" const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE" + const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME" } } \ 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 7a8ba4e..25dc6d4 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 @@ -15,8 +15,11 @@ import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -44,6 +47,8 @@ fun InstallerScreen( vm: InstallerViewModel ) { 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) } } AppScaffold( topBar = { @@ -66,7 +71,7 @@ fun InstallerScreen( .padding(paddingValues) .fillMaxSize() ) { - vm.stepGroups.forEach { + patcherState.stepGroups.forEach { InstallGroup(it) } Spacer(modifier = Modifier.weight(1f)) @@ -79,16 +84,16 @@ fun InstallerScreen( ) { Button( onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, - enabled = vm.canInstall + enabled = canInstall ) { Text(stringResource(R.string.export_app)) } Button( - onClick = vm::installApk, - enabled = vm.canInstall + onClick = vm::installOrOpen, + enabled = canInstall ) { - Text(stringResource(R.string.install_app)) + Text(stringResource(vm.appButtonText)) } } } 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 ed47626..096db32 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 @@ -12,8 +12,8 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel +import androidx.lifecycle.map import androidx.work.* import app.revanced.manager.R import app.revanced.manager.patcher.SignerService @@ -42,30 +42,18 @@ class InstallerViewModel( private val app: Application by inject() private val pm: PM by inject() - var stepGroups by mutableStateOf>( - PatcherProgressManager.generateGroupsList( - app, - selectedPatches.flatMap { (_, selected) -> selected }) - ) - private set - val packageName: String = input.packageName - - private val workManager = WorkManager.getInstance(app) - - // TODO: get rid of these and use stepGroups instead. - var installStatus by mutableStateOf(null) - var pmStatus by mutableStateOf(-999) - var extra by mutableStateOf("") - private val outputFile = File(app.cacheDir, "output.apk") private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() } private var hasSigned = false - private var patcherStatus by mutableStateOf(null) - private var isInstalling by mutableStateOf(false) - val canInstall by derivedStateOf { patcherStatus == true && !isInstalling } + var isInstalling by mutableStateOf(false) + private set + var installedPackageName by mutableStateOf(null) + private set + 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( @@ -83,26 +71,41 @@ class InstallerViewModel( ) ).build() - private val liveData = workManager.getWorkInfoByIdLiveData(patcherWorker.id) // get LiveData + val initialState = PatcherState( + status = null, + stepGroups = PatcherProgressManager.generateGroupsList( + app, + selectedPatches.flatMap { (_, selected) -> selected } + ) + ) + val patcherState = + workManager.getWorkInfoByIdLiveData(patcherWorker.id).map { workInfo: WorkInfo -> + var status: Boolean? = null + val stepGroups = when (workInfo.state) { + WorkInfo.State.RUNNING -> workInfo.progress + WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData.also { + status = workInfo.state == WorkInfo.State.SUCCEEDED + } - private val observer = Observer { workInfo: WorkInfo -> // observer for observing patch status - when (workInfo.state) { - WorkInfo.State.RUNNING -> workInfo.progress - WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData.also { - patcherStatus = workInfo.state == WorkInfo.State.SUCCEEDED - } + else -> null + }?.let { PatcherProgressManager.groupsFromWorkData(it) } - else -> null - }?.let { PatcherProgressManager.groupsFromWorkData(it) }?.let { stepGroups = it } - } + PatcherState(status, stepGroups ?: initialState.stepGroups) + } private val installBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { InstallService.APP_INSTALL_ACTION -> { - pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) - extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! - postInstallStatus() + val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) + val extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! + + if (pmStatus == PackageInstaller.STATUS_SUCCESS) { + app.toast(app.getString(R.string.install_app_success)) + installedPackageName = intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME) + } else { + app.toast(app.getString(R.string.install_app_fail, extra)) + } } UninstallService.APP_UNINSTALL_ACTION -> { @@ -113,13 +116,21 @@ class InstallerViewModel( init { workManager.enqueueUniqueWork("patching", ExistingWorkPolicy.KEEP, patcherWorker) - liveData.observeForever(observer) app.registerReceiver(installBroadcastReceiver, IntentFilter().apply { addAction(InstallService.APP_INSTALL_ACTION) addAction(UninstallService.APP_UNINSTALL_ACTION) }) } + override fun onCleared() { + super.onCleared() + app.unregisterReceiver(installBroadcastReceiver) + workManager.cancelWorkById(patcherWorker.id) + + outputFile.delete() + signedFile.delete() + } + private fun signApk(): Boolean { if (!hasSigned) { try { @@ -141,7 +152,12 @@ class InstallerViewModel( } } - fun installApk() { + fun installOrOpen() { + installedPackageName?.let { + pm.launch(it) + return + } + isInstalling = true try { if (!signApk()) return @@ -151,18 +167,6 @@ class InstallerViewModel( } } - fun postInstallStatus() { - installStatus = pmStatus == PackageInstaller.STATUS_SUCCESS - } - override fun onCleared() { - super.onCleared() - liveData.removeObserver(observer) - app.unregisterReceiver(installBroadcastReceiver) - workManager.cancelWorkById(patcherWorker.id) - // logs.clear() - - outputFile.delete() - signedFile.delete() - } + data class PatcherState(val status: Boolean?, val stepGroups: List) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index 54f3f95..9ff1a78 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -120,6 +120,11 @@ class PM( packageInstaller.uninstall(pkg, app.uninstallIntentSender) } + fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + app.startActivity(it) + } + fun getApkInfo(apk: File) = app.packageManager.getPackageArchiveInfo(apk.path, 0)!!.let { AppInfo( it.packageName, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a19a7c7..fc7300e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,6 +66,9 @@ Installer Install + App installed + Failed to install app: %s + Open Export Apk exported Failed to sign Apk: %s