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 be79c99135
commit f180f20905
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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("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")

View File

@ -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<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>) {
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<String>) {
),
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<String>) {
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<String>) {
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() {

View File

@ -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)

View File

@ -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"
}
}

View File

@ -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))
}
}
}

View File

@ -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<List<StepGroup>>(
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<Boolean?>(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<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 =
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<StepGroup>)
}

View File

@ -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,

View File

@ -66,6 +66,9 @@
<string name="installer">Installer</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_success">Apk exported</string>
<string name="sign_fail">Failed to sign Apk: %s</string>