From 00c61b6adc7d6c451c0edd944af2318e5440902d Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sun, 29 Sep 2024 21:02:04 +0200 Subject: [PATCH] fix patcher screen Remaining WIP: update dashboard screen to feature a dialog --- .../ui/component/InstallerStatusDialog.kt | 30 +++---- .../manager/ui/model/InstallerModel.kt | 6 ++ .../revanced/manager/ui/model/PatcherStep.kt | 1 - .../manager/ui/screen/PatcherScreen.kt | 5 +- .../ui/viewmodel/DashboardViewModel.kt | 13 ++- .../manager/ui/viewmodel/PatcherViewModel.kt | 83 ++++++++++--------- .../main/java/app/revanced/manager/util/PM.kt | 2 + .../util/RequestInstallAppsContract.kt | 22 +++++ 8 files changed, 98 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt create mode 100644 app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt index a31a813e..2ae48ce6 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -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 } diff --git a/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt new file mode 100644 index 00000000..410b64c1 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt @@ -0,0 +1,6 @@ +package app.revanced.manager.ui.model + +interface InstallerModel { + fun reinstall() + fun install() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt index e2f3ea2b..e1662c14 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt @@ -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) { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 3ae7a437..04b0792c 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -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 = { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index ce68249d..48a6b02f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -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() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index a4c92c28..5e0900e0 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -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?>(null) } private set @@ -166,15 +156,19 @@ class PatcherViewModel( private val workManager = WorkManager.getInstance(app) - private val patcherWorkerId by savedStateHandle.saveable { - workerRepository.launchExpedited( + private val patcherWorkerId by savedStateHandle.saveable { + ParcelUuid(workerRepository.launchExpedited( "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" diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index 0d7a822b..1bd705d9 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -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 -> diff --git a/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt new file mode 100644 index 00000000..bb435a82 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt @@ -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(), 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() + } +} \ No newline at end of file