fix(installer): properly track worker state (#32)

This commit is contained in:
Ax333l 2023-06-09 17:34:10 +02:00 committed by GitHub
parent 7ce4de7a8b
commit 971277ed39
8 changed files with 94 additions and 78 deletions

View File

@ -72,6 +72,7 @@ dependencies {
implementation(platform("androidx.compose:compose-bom:2023.05.01")) implementation(platform("androidx.compose:compose-bom:2023.05.01"))
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.runtime:runtime-livedata")
implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3")

View File

@ -5,10 +5,7 @@ import androidx.annotation.StringRes
import androidx.work.Data import androidx.work.Data
import androidx.work.workDataOf import androidx.work.workDataOf
import app.revanced.manager.R 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.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -34,22 +31,25 @@ enum class StepStatus {
class Step(val name: String, val status: StepStatus = StepStatus.WAITING) class Step(val name: String, val status: StepStatus = StepStatus.WAITING)
@Serializable @Serializable
class StepGroup(@StringRes val name: Int, val steps: ImmutableList<Step>, val status: StepStatus = StepStatus.WAITING) class StepGroup(
@StringRes val name: Int,
val steps: List<Step>,
val status: StepStatus = StepStatus.WAITING
)
class PatcherProgressManager(context: Context, selectedPatches: List<String>) { class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
val stepGroups = generateGroupsList(context, selectedPatches) val stepGroups = generateGroupsList(context, selectedPatches)
companion object { companion object {
private const val PATCHES = 1
private const val WORK_DATA_KEY = "progress" 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( private val stepKeyMap = mapOf(
Progress.Unpacking to StepKey(0, 0), Progress.Unpacking to StepKey(0, 0),
Progress.Merging to StepKey(0, 1), Progress.Merging to StepKey(0, 1),
Progress.PatchingStart to StepKey(PATCHES, 0), Progress.PatchingStart to StepKey(1, 0),
Progress.Saving to StepKey(2, 0), Progress.Saving to StepKey(2, 0),
) )
@ -63,7 +63,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
), ),
StepGroup( StepGroup(
R.string.patcher_step_group_patching, R.string.patcher_step_group_patching,
selectedPatches.map { Step(it) }.toImmutableList() selectedPatches.map { Step(it) }
), ),
StepGroup( StepGroup(
R.string.patcher_step_group_saving, R.string.patcher_step_group_saving,
@ -86,7 +86,8 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
private fun updateStepStatus(key: StepKey, newStatus: StepStatus) { private fun updateStepStatus(key: StepKey, newStatus: StepStatus) {
var isLastStepOfGroup = false var isLastStepOfGroup = false
stepGroups.mutateIndex(key.groupIndex) { group -> stepGroups.mutateIndex(key.groupIndex) { group ->
isLastStepOfGroup = key.stepIndex == group.steps.size - 1 isLastStepOfGroup = key.stepIndex == group.steps.lastIndex
val newGroupStatus = when { val newGroupStatus = when {
// This group failed if a step in it failed. // This group failed if a step in it failed.
newStatus == StepStatus.FAILURE -> StepStatus.FAILURE newStatus == StepStatus.FAILURE -> StepStatus.FAILURE
@ -98,37 +99,31 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
StepGroup(group.name, group.steps.toMutableList().mutateIndex(key.stepIndex) { step -> StepGroup(group.name, group.steps.toMutableList().mutateIndex(key.stepIndex) { step ->
Step(step.name, newStatus) 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) { if (newStatus == StepStatus.COMPLETED) {
// Move the cursor to the next step. // Move the cursor to the next step.
currentStep = when { currentStep = when {
isFinalStep -> null // Final step has been completed. isFinalStep -> null // Final step has been completed.
isLastStepOfGroup -> StepKey(key.groupIndex + 1, 0) // Move to the next group. 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) private data class StepKey(val groupIndex: Int, val stepIndex: Int)
fun handle(progress: Progress) { fun handle(progress: Progress) = success().also {
if (progress is Progress.PatchSuccess) { stepKeyMap[progress]?.let { currentStep = it }
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 failure() { fun failure() {

View File

@ -60,7 +60,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineW
} }
val progressManager = val progressManager =
PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { (_, selected) -> selected }) PatcherProgressManager(applicationContext, patchList.map { it.patchName })
suspend fun updateProgress(progress: Progress) { suspend fun updateProgress(progress: Progress) {
progressManager.handle(progress) progressManager.handle(progress)

View File

@ -14,6 +14,7 @@ class InstallService : Service() {
): Int { ): Int {
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
when (extraStatus) { when (extraStatus) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> { PackageInstaller.STATUS_PENDING_USER_ACTION -> {
startActivity(if (Build.VERSION.SDK_INT >= 33) { startActivity(if (Build.VERSION.SDK_INT >= 33) {
@ -30,6 +31,7 @@ class InstallService : Service() {
action = APP_INSTALL_ACTION action = APP_INSTALL_ACTION
putExtra(EXTRA_INSTALL_STATUS, extraStatus) putExtra(EXTRA_INSTALL_STATUS, extraStatus)
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage) 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 = "EXTRA_INSTALL_STATUS"
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE" const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
} }
} }

View File

@ -15,8 +15,11 @@ 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.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -44,6 +47,8 @@ fun InstallerScreen(
vm: InstallerViewModel 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) } }
AppScaffold( AppScaffold(
topBar = { topBar = {
@ -66,7 +71,7 @@ fun InstallerScreen(
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize() .fillMaxSize()
) { ) {
vm.stepGroups.forEach { patcherState.stepGroups.forEach {
InstallGroup(it) InstallGroup(it)
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@ -79,16 +84,16 @@ fun InstallerScreen(
) { ) {
Button( Button(
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
enabled = vm.canInstall enabled = canInstall
) { ) {
Text(stringResource(R.string.export_app)) Text(stringResource(R.string.export_app))
} }
Button( Button(
onClick = vm::installApk, onClick = vm::installOrOpen,
enabled = vm.canInstall enabled = canInstall
) { ) {
Text(stringResource(R.string.install_app)) Text(stringResource(vm.appButtonText))
} }
} }
} }

View File

@ -12,8 +12,8 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.work.* import androidx.work.*
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.patcher.SignerService import app.revanced.manager.patcher.SignerService
@ -42,30 +42,18 @@ class InstallerViewModel(
private val app: Application by inject() private val app: Application by inject()
private val pm: PM by inject() private val pm: PM by inject()
var stepGroups by mutableStateOf<List<StepGroup>>(
PatcherProgressManager.generateGroupsList(
app,
selectedPatches.flatMap { (_, selected) -> selected })
)
private set
val packageName: String = input.packageName val packageName: String = input.packageName
private val workManager = WorkManager.getInstance(app)
// TODO: get rid of these and use stepGroups instead.
var installStatus by mutableStateOf<Boolean?>(null)
var pmStatus by mutableStateOf(-999)
var extra by mutableStateOf("")
private val outputFile = File(app.cacheDir, "output.apk") private val outputFile = File(app.cacheDir, "output.apk")
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() } private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
private var hasSigned = false private var hasSigned = false
private var patcherStatus by mutableStateOf<Boolean?>(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<String?>(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 = private val patcherWorker =
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData( .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData(
@ -83,26 +71,41 @@ class InstallerViewModel(
) )
).build() ).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 else -> null
when (workInfo.state) { }?.let { PatcherProgressManager.groupsFromWorkData(it) }
WorkInfo.State.RUNNING -> workInfo.progress
WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData.also {
patcherStatus = workInfo.state == WorkInfo.State.SUCCEEDED
}
else -> null PatcherState(status, stepGroups ?: initialState.stepGroups)
}?.let { PatcherProgressManager.groupsFromWorkData(it) }?.let { stepGroups = it } }
}
private val installBroadcastReceiver = object : BroadcastReceiver() { private val installBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) { when (intent?.action) {
InstallService.APP_INSTALL_ACTION -> { InstallService.APP_INSTALL_ACTION -> {
pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! val extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
postInstallStatus()
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 -> { UninstallService.APP_UNINSTALL_ACTION -> {
@ -113,13 +116,21 @@ class InstallerViewModel(
init { init {
workManager.enqueueUniqueWork("patching", ExistingWorkPolicy.KEEP, patcherWorker) workManager.enqueueUniqueWork("patching", ExistingWorkPolicy.KEEP, patcherWorker)
liveData.observeForever(observer)
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)
}) })
} }
override fun onCleared() {
super.onCleared()
app.unregisterReceiver(installBroadcastReceiver)
workManager.cancelWorkById(patcherWorker.id)
outputFile.delete()
signedFile.delete()
}
private fun signApk(): Boolean { private fun signApk(): Boolean {
if (!hasSigned) { if (!hasSigned) {
try { try {
@ -141,7 +152,12 @@ class InstallerViewModel(
} }
} }
fun installApk() { fun installOrOpen() {
installedPackageName?.let {
pm.launch(it)
return
}
isInstalling = true isInstalling = true
try { try {
if (!signApk()) return if (!signApk()) return
@ -151,18 +167,6 @@ class InstallerViewModel(
} }
} }
fun postInstallStatus() {
installStatus = pmStatus == PackageInstaller.STATUS_SUCCESS
}
override fun onCleared() { data class PatcherState(val status: Boolean?, val stepGroups: List<StepGroup>)
super.onCleared()
liveData.removeObserver(observer)
app.unregisterReceiver(installBroadcastReceiver)
workManager.cancelWorkById(patcherWorker.id)
// logs.clear()
outputFile.delete()
signedFile.delete()
}
} }

View File

@ -120,6 +120,11 @@ class PM(
packageInstaller.uninstall(pkg, app.uninstallIntentSender) 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 { fun getApkInfo(apk: File) = app.packageManager.getPackageArchiveInfo(apk.path, 0)!!.let {
AppInfo( AppInfo(
it.packageName, it.packageName,

View File

@ -66,6 +66,9 @@
<string name="installer">Installer</string> <string name="installer">Installer</string>
<string name="install_app">Install</string> <string name="install_app">Install</string>
<string name="install_app_success">App installed</string>
<string name="install_app_fail">Failed to install app: %s</string>
<string name="open_app">Open</string>
<string name="export_app">Export</string> <string name="export_app">Export</string>
<string name="export_app_success">Apk exported</string> <string name="export_app_success">Apk exported</string>
<string name="sign_fail">Failed to sign Apk: %s</string> <string name="sign_fail">Failed to sign Apk: %s</string>