mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-01 06:24:26 +02:00
fix patcher screen
Remaining WIP: update dashboard screen to feature a dialog
This commit is contained in:
parent
625abd72b0
commit
00c61b6adc
@ -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
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
interface InstallerModel {
|
||||
fun reinstall()
|
||||
fun install()
|
||||
}
|
@ -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) {
|
||||
|
@ -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 = {
|
||||
|
@ -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()
|
||||
|
@ -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,25 +393,35 @@ 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 {
|
||||
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")
|
||||
override fun install() {
|
||||
// InstallType.ROOT is never used here since this overload is for the package installer status dialog.
|
||||
install(InstallType.DEFAULT)
|
||||
}
|
||||
|
||||
pm.installApp(listOf(outputFile))
|
||||
isInstalling = true
|
||||
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")
|
||||
|
||||
pm.installApp(listOf(outputFile))
|
||||
isInstalling = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissPackageInstallerDialog() {
|
||||
packageInstallerStatus = null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "ReVanced Patcher"
|
||||
|
||||
|
@ -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 ->
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user