mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
fix(installer): properly track worker state (#32)
This commit is contained in:
parent
7ce4de7a8b
commit
971277ed39
@ -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")
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>)
|
||||
}
|
@ -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,
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user