fix patcher screen

Remaining WIP: update dashboard screen to feature a dialog
This commit is contained in:
Ax333l 2024-09-29 21:02:04 +02:00
parent 625abd72b0
commit 00c61b6adc
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
8 changed files with 98 additions and 64 deletions

View File

@ -6,7 +6,6 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
@ -21,35 +20,25 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.model.InstallerModel
import com.github.materiiapps.enumutil.FromValue
private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit)
private typealias InstallerStatusDialogButton = @Composable (model: InstallerStatusDialogModel) -> Unit
interface InstallerModel {
fun reinstall()
fun install()
}
interface InstallerStatusDialogModel : InstallerModel {
var packageInstallerStatus: Int?
}
private typealias InstallerStatusDialogButton = @Composable (model: InstallerModel, dismiss: () -> Unit) -> Unit
@Composable
fun InstallerStatusDialog(model: InstallerStatusDialogModel) {
fun InstallerStatusDialog(installerStatus: Int, model: InstallerModel, onDismiss: () -> Unit) {
val dialogKind = remember {
DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE
DialogKind.fromValue(installerStatus) ?: DialogKind.FAILURE
}
AlertDialog(
onDismissRequest = {
model.packageInstallerStatus = null
},
onDismissRequest = onDismiss,
confirmButton = {
dialogKind.confirmButton(model)
dialogKind.confirmButton(model, onDismiss)
},
dismissButton = {
dialogKind.dismissButton?.invoke(model)
dialogKind.dismissButton?.invoke(model, onDismiss)
},
icon = {
Icon(dialogKind.icon, null)
@ -75,10 +64,10 @@ fun InstallerStatusDialog(model: InstallerStatusDialogModel) {
private fun installerStatusDialogButton(
@StringRes buttonStringResId: Int,
buttonHandler: InstallerStatusDialogButtonHandler = { },
): InstallerStatusDialogButton = { model ->
): InstallerStatusDialogButton = { model, dismiss ->
TextButton(
onClick = {
model.packageInstallerStatus = null
dismiss()
buttonHandler(model)
}
) {
@ -154,6 +143,7 @@ enum class DialogKind(
model.install()
},
);
// Needed due to the @FromValue annotation.
companion object
}

View File

@ -0,0 +1,6 @@
package app.revanced.manager.ui.model
interface InstallerModel {
fun reinstall()
fun install()
}

View File

@ -3,7 +3,6 @@ package app.revanced.manager.ui.model
import android.os.Parcelable
import androidx.annotation.StringRes
import app.revanced.manager.R
import kotlinx.coroutines.flow.StateFlow
import kotlinx.parcelize.Parcelize
enum class StepCategory(@StringRes val displayName: Int) {

View File

@ -74,8 +74,9 @@ fun PatcherScreen(
onConfirm = vm::install
)
if (vm.installerStatusDialogModel.packageInstallerStatus != null)
InstallerStatusDialog(vm.installerStatusDialogModel)
vm.packageInstallerStatus?.let {
InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog)
}
AppScaffold(
topBar = {

View File

@ -3,6 +3,7 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.ContentResolver
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
@ -19,6 +20,7 @@ import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.PM
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.flow.first
@ -30,7 +32,8 @@ class DashboardViewModel(
private val patchBundleRepository: PatchBundleRepository,
private val reVancedAPI: ReVancedAPI,
private val networkInfo: NetworkInfo,
val prefs: PreferencesManager
val prefs: PreferencesManager,
private val pm: PM,
) : ViewModel() {
val availablePatches =
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } }
@ -44,6 +47,14 @@ class DashboardViewModel(
var showBatteryOptimizationsWarning by mutableStateOf(false)
private set
/**
* Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen.
* This value is true when the conditions that trigger the bug are met.
*
* See: https://github.com/ReVanced/revanced-manager/issues/2138
*/
val android11BugActive get() = Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !pm.canInstallPackages()
init {
viewModelScope.launch {
checkForManagerUpdates()

View File

@ -7,6 +7,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.net.Uri
import android.os.ParcelUuid
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
@ -36,8 +37,8 @@ import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import app.revanced.manager.ui.component.InstallerStatusDialogModel
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.model.InstallerModel
import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State
@ -62,15 +63,12 @@ import org.koin.core.component.inject
import java.io.File
import java.nio.file.Files
import java.time.Duration
import java.util.UUID
// @SuppressLint("AutoboxingStateCreation")
@Stable
@OptIn(SavedStateHandleSaveableApi::class)
class PatcherViewModel(
private val input: Destination.Patcher
) : ViewModel(), KoinComponent, StepProgressProvider {
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
private val app: Application by inject()
private val fs: Filesystem by inject()
private val pm: PM by inject()
@ -79,20 +77,6 @@ class PatcherViewModel(
private val rootInstaller: RootInstaller by inject()
private val savedStateHandle: SavedStateHandle by inject()
val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel {
override var packageInstallerStatus: Int? by mutableStateOf(null)
override fun reinstall() {
this@PatcherViewModel.reinstall()
}
override fun install() {
// Since this is a package installer status dialog,
// InstallType.ROOT is never used here.
install(InstallType.DEFAULT)
}
}
private var installedApp: InstalledApp? = null
val packageName = input.selectedApp.packageName
@ -105,6 +89,13 @@ class PatcherViewModel(
}
private set
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
var packageInstallerStatus: Int? by savedStateHandle.saveable(
key = "packageInstallerStatus",
stateSaver = autoSaver()
) {
mutableStateOf(null)
}
private set
var isInstalling by mutableStateOf(ongoingPmSession)
private set
@ -142,7 +133,6 @@ class PatcherViewModel(
key = "downloadProgress",
stateSaver = autoSaver()
) {
viewModelScope
mutableStateOf<Pair<Float, Float>?>(null)
}
private set
@ -166,15 +156,19 @@ class PatcherViewModel(
private val workManager = WorkManager.getInstance(app)
private val patcherWorkerId by savedStateHandle.saveable<UUID> {
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args(
input.selectedApp,
outputFile.path,
input.selectedPatches,
input.options,
logger,
onDownloadProgress = { withContext(Dispatchers.Main) { downloadProgress = it } },
onDownloadProgress = {
withContext(Dispatchers.Main) {
downloadProgress = it
}
},
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
onProgress = { name, state, message ->
@ -196,11 +190,11 @@ class PatcherViewModel(
}
}
)
)
))
}
val patcherSucceeded =
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
workManager.getWorkInfoByIdLiveData(patcherWorkerId.uuid).map { workInfo: WorkInfo ->
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> true
WorkInfo.State.FAILED -> false
@ -234,7 +228,7 @@ class PatcherViewModel(
}
}
installerStatusDialogModel.packageInstallerStatus = pmStatus
packageInstallerStatus = pmStatus
isInstalling = false
}
@ -249,7 +243,7 @@ class PatcherViewModel(
?.let(logger::trace)
if (pmStatus != PackageInstaller.STATUS_SUCCESS) {
installerStatusDialogModel.packageInstallerStatus = pmStatus
packageInstallerStatus = pmStatus
}
}
}
@ -277,7 +271,7 @@ class PatcherViewModel(
override fun onCleared() {
super.onCleared()
app.unregisterReceiver(installerBroadcastReceiver)
workManager.cancelWorkById(patcherWorkerId)
workManager.cancelWorkById(patcherWorkerId.uuid)
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) {
GlobalScope.launch(Dispatchers.Main) {
@ -332,7 +326,7 @@ class PatcherViewModel(
// Check if the app version is less than the installed version
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
// Exit if the selected app version is less than the installed version
installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
return@launch
}
}
@ -357,8 +351,7 @@ class PatcherViewModel(
// If the app is not installed, check if the output file is a base apk
if (currentPackageInfo.splitNames != null) {
// Exit if there is no base APK package
installerStatusDialogModel.packageInstallerStatus =
PackageInstaller.STATUS_FAILURE_INVALID
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
return@launch
}
}
@ -400,16 +393,21 @@ class PatcherViewModel(
}
}
}
} catch(e: Exception) {
} catch (e: Exception) {
Log.e(tag, "Failed to install", e)
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
} finally {
if (!pmInstallStarted)
isInstalling = false
if (!pmInstallStarted) isInstalling = false
}
}
fun reinstall() = viewModelScope.launch {
override fun install() {
// InstallType.ROOT is never used here since this overload is for the package installer status dialog.
install(InstallType.DEFAULT)
}
override fun reinstall() {
viewModelScope.launch {
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
?: throw Exception("Failed to load application info")
@ -418,6 +416,11 @@ class PatcherViewModel(
isInstalling = true
}
}
}
fun dismissPackageInstallerDialog() {
packageInstallerStatus = null
}
private companion object {
const val TAG = "ReVanced Patcher"

View File

@ -136,6 +136,8 @@ class PM(
app.startActivity(it)
}
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
private fun PackageInstaller.Session.writeApk(apk: File) {
apk.inputStream().use { inputStream ->
openWrite(apk.name, 0, apk.length()).use { outputStream ->

View File

@ -0,0 +1,22 @@
package app.revanced.manager.util
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.RequiresApi
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class RequestInstallAppsContract : ActivityResultContract<String, Boolean>(), KoinComponent {
private val pm: PM by inject()
override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.fromParts("package", input, null))
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
println("Finished")
return pm.canInstallPackages()
}
}