From 20554005652f050835940a6e7ff5b74848f57245 Mon Sep 17 00:00:00 2001 From: Ushie Date: Sun, 25 Aug 2024 01:23:13 +0300 Subject: [PATCH 1/4] feat: View bundle patches (#2065) --- .../component/bundle/BundlePatchesDialog.kt | 261 +++++++++++++++--- app/src/main/res/values/strings.xml | 4 + 2 files changed, 220 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt index a4fbce81..99201949 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt @@ -1,33 +1,35 @@ package app.revanced.manager.ui.component.bundle import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.outlined.Lightbulb -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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 import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.LazyColumnWithScrollbar -import app.revanced.manager.ui.component.NotificationCard @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -35,7 +37,8 @@ fun BundlePatchesDialog( onDismissRequest: () -> Unit, bundle: PatchBundleSource, ) { - var informationCardVisible by remember { mutableStateOf(true) } + var showAllVersions by rememberSaveable { mutableStateOf(false) } + var showOptions by rememberSaveable { mutableStateOf(false) } val state by bundle.state.collectAsStateWithLifecycle() Dialog( @@ -62,44 +65,212 @@ fun BundlePatchesDialog( LazyColumnWithScrollbar( modifier = Modifier .fillMaxWidth() - .padding(paddingValues) - .padding(16.dp) + .padding(paddingValues), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(16.dp) ) { - item { - AnimatedVisibility(visible = informationCardVisible) { - NotificationCard( - icon = Icons.Outlined.Lightbulb, - text = stringResource(R.string.tap_on_patches), - onDismiss = { informationCardVisible = false } - ) - } - } - state.patchBundleOrNull()?.let { bundle -> - items(bundle.patches.size) { bundleIndex -> - val patch = bundle.patches[bundleIndex] - ListItem( - headlineContent = { - Text( - text = patch.name, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface - ) - }, - supportingContent = { - patch.description?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + items(bundle.patches) { patch -> + PatchItem( + patch, + showAllVersions, + onExpandVersions = { showAllVersions = !showAllVersions }, + showOptions, + onExpandOptions = { showOptions = !showOptions } ) - HorizontalDivider() } } } } } } + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PatchItem( + patch: PatchInfo, + expandVersions: Boolean, + onExpandVersions: () -> Unit, + expandOptions: Boolean, + onExpandOptions: () -> Unit +) { + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .then( + if (patch.options.isNullOrEmpty()) Modifier else Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onExpandOptions), + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Absolute.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = patch.name, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + + if (!patch.options.isNullOrEmpty()) { + ArrowButton(expanded = expandOptions, onClick = null) + } + } + patch.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (patch.compatiblePackages.isNullOrEmpty()) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PatchInfoChip( + text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}" + ) + PatchInfoChip( + text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}" + ) + } + } else { + patch.compatiblePackages.forEach { compatiblePackage -> + val packageName = compatiblePackage.packageName + val versions = compatiblePackage.versions.orEmpty().reversed() + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$PACKAGE_ICON $packageName" + ) + + if (versions.isNotEmpty()) { + if (expandVersions) { + versions.forEach { version -> + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$VERSION_ICON $version" + ) + } + } else { + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$VERSION_ICON ${versions.first()}" + ) + } + if (versions.size > 1) { + PatchInfoChip( + onClick = onExpandVersions, + text = if (expandVersions) stringResource(R.string.less) else "+${versions.size - 1}" + ) + } + } + } + } + } + } + if (!patch.options.isNullOrEmpty()) { + AnimatedVisibility(visible = expandOptions) { + val options = patch.options + + Column { + options.forEachIndexed { i, option -> + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = Color.Transparent, + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), shape = when { + options.size == 1 -> RoundedCornerShape(8.dp) + i == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) + i == options.lastIndex -> RoundedCornerShape( + bottomStart = 8.dp, + bottomEnd = 8.dp + ) + + else -> RoundedCornerShape(0.dp) + } + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = option.title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = option.description, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + } + } + } + } +} + +@Composable +fun PatchInfoChip( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + text: String +) { + val shape = RoundedCornerShape(8.0.dp) + val cardModifier = if (onClick != null) { + Modifier + .clip(shape) + .clickable(onClick = onClick) + } else { + Modifier + } + + OutlinedCard( + modifier = modifier.then(cardModifier), + colors = CardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = Color.Transparent, + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), + shape = shape, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.20f)) + ) { + Row( + modifier = Modifier.padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text, + overflow = TextOverflow.Ellipsis, + softWrap = false, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +const val PACKAGE_ICON = "\uD83D\uDCE6" +const val VERSION_ICON = "\uD83C\uDFAF" \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36e2e43e..81522032 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -287,6 +287,7 @@ reorder More + Less Continue Dismiss Do not show this again @@ -305,6 +306,9 @@ Auto update Automatically update this bundle when ReVanced starts View patches + Any version + Any package + About ReVanced Manager ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance. An update is available From d201bdc422fcceb09040398c87edbaf2e97c7296 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 29 Aug 2024 20:28:13 +0400 Subject: [PATCH 2/4] feat: Add installer status dialog (#1473) Co-authored-by: Benjamin Halko Co-authored-by: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Co-authored-by: Ushie Co-authored-by: Ax333l --- app/build.gradle.kts | 4 + .../ui/component/InstallerStatusDialog.kt | 159 ++++++++++++++++++ .../manager/ui/screen/PatcherScreen.kt | 8 +- .../manager/ui/viewmodel/PatcherViewModel.kt | 120 +++++++++++-- .../main/java/app/revanced/manager/util/PM.kt | 5 +- app/src/main/res/values/strings.xml | 19 +++ gradle/libs.versions.toml | 5 + 7 files changed, 303 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ee855884..70281945 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -191,6 +191,10 @@ dependencies { // Scrollbars implementation(libs.scrollbars) + // EnumUtil + implementation(libs.enumutil) + ksp(libs.enumutil.ksp) + // Reorderable lists implementation(libs.reorderable) 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 new file mode 100644 index 00000000..a31a813e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -0,0 +1,159 @@ +package app.revanced.manager.ui.component + +import android.content.pm.PackageInstaller +import androidx.annotation.RequiresApi +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 +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.vector.ImageVector +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 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? +} + +@Composable +fun InstallerStatusDialog(model: InstallerStatusDialogModel) { + val dialogKind = remember { + DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE + } + + AlertDialog( + onDismissRequest = { + model.packageInstallerStatus = null + }, + confirmButton = { + dialogKind.confirmButton(model) + }, + dismissButton = { + dialogKind.dismissButton?.invoke(model) + }, + icon = { + Icon(dialogKind.icon, null) + }, + title = { + Text( + text = stringResource(dialogKind.title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text(stringResource(dialogKind.contentStringResId)) + } + } + ) +} + +private fun installerStatusDialogButton( + @StringRes buttonStringResId: Int, + buttonHandler: InstallerStatusDialogButtonHandler = { }, +): InstallerStatusDialogButton = { model -> + TextButton( + onClick = { + model.packageInstallerStatus = null + buttonHandler(model) + } + ) { + Text(stringResource(buttonStringResId)) + } +} + +@FromValue("flag") +enum class DialogKind( + val flag: Int, + val title: Int, + @StringRes val contentStringResId: Int, + val icon: ImageVector = Icons.Outlined.ErrorOutline, + val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok), + val dismissButton: InstallerStatusDialogButton? = null, +) { + FAILURE( + flag = PackageInstaller.STATUS_FAILURE, + title = R.string.installation_failed_dialog_title, + contentStringResId = R.string.installation_failed_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + } + ), + FAILURE_ABORTED( + flag = PackageInstaller.STATUS_FAILURE_ABORTED, + title = R.string.installation_cancelled_dialog_title, + contentStringResId = R.string.installation_aborted_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + } + ), + FAILURE_BLOCKED( + flag = PackageInstaller.STATUS_FAILURE_BLOCKED, + title = R.string.installation_blocked_dialog_title, + contentStringResId = R.string.installation_blocked_description, + ), + FAILURE_CONFLICT( + flag = PackageInstaller.STATUS_FAILURE_CONFLICT, + title = R.string.installation_conflict_dialog_title, + contentStringResId = R.string.installation_conflict_description, + confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> + model.reinstall() + }, + dismissButton = installerStatusDialogButton(R.string.cancel), + ), + FAILURE_INCOMPATIBLE( + flag = PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, + title = R.string.installation_incompatible_dialog_title, + contentStringResId = R.string.installation_incompatible_description, + ), + FAILURE_INVALID( + flag = PackageInstaller.STATUS_FAILURE_INVALID, + title = R.string.installation_invalid_dialog_title, + contentStringResId = R.string.installation_invalid_description, + confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> + model.reinstall() + }, + dismissButton = installerStatusDialogButton(R.string.cancel), + ), + FAILURE_STORAGE( + flag = PackageInstaller.STATUS_FAILURE_STORAGE, + title = R.string.installation_storage_issue_dialog_title, + contentStringResId = R.string.installation_storage_issue_description, + ), + + @RequiresApi(34) + FAILURE_TIMEOUT( + flag = PackageInstaller.STATUS_FAILURE_TIMEOUT, + title = R.string.installation_timeout_dialog_title, + contentStringResId = R.string.installation_timeout_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + }, + ); + // Needed due to the @FromValue annotation. + companion object +} 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 163dfbc6..096bbf03 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 @@ -40,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.InstallerStatusDialog import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.Steps import app.revanced.manager.ui.model.State @@ -91,6 +92,9 @@ fun PatcherScreen( onConfirm = vm::install ) + if (vm.installerStatusDialogModel.packageInstallerStatus != null) + InstallerStatusDialog(vm.installerStatusDialogModel) + AppScaffold( topBar = { AppTopBar( @@ -103,7 +107,7 @@ fun PatcherScreen( actions = { IconButton( onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, - enabled = canInstall + enabled = patcherSucceeded == true ) { Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) } @@ -172,4 +176,4 @@ fun PatcherScreen( } } } -} \ No newline at end of file +} 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 e0995911..ae7f95b9 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 @@ -30,6 +30,8 @@ import app.revanced.manager.patcher.logger.LogLevel 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.SelectedApp import app.revanced.manager.ui.model.State @@ -66,6 +68,20 @@ class PatcherViewModel( private val installedAppRepository: InstalledAppRepository by inject() private val rootInstaller: RootInstaller 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: String = input.selectedApp.packageName var installedPackageName by mutableStateOf(null) @@ -144,15 +160,19 @@ class PatcherViewModel( } } - private val installBroadcastReceiver = object : BroadcastReceiver() { + private val installerBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { InstallService.APP_INSTALL_ACTION -> { - val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) - val extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! + val pmStatus = intent.getIntExtra( + InstallService.EXTRA_INSTALL_STATUS, + PackageInstaller.STATUS_FAILURE + ) + + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) if (pmStatus == PackageInstaller.STATUS_SUCCESS) { - app.toast(app.getString(R.string.install_app_success)) installedPackageName = intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME) viewModelScope.launch { @@ -164,8 +184,24 @@ class PatcherViewModel( input.selectedPatches ) } - } else { - app.toast(app.getString(R.string.install_app_fail, extra)) + } + + installerStatusDialogModel.packageInstallerStatus = pmStatus + + isInstalling = false + } + + UninstallService.APP_UNINSTALL_ACTION -> { + val pmStatus = intent.getIntExtra( + UninstallService.EXTRA_UNINSTALL_STATUS, + PackageInstaller.STATUS_FAILURE + ) + + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) + + if (pmStatus != PackageInstaller.STATUS_SUCCESS) { + installerStatusDialogModel.packageInstallerStatus = pmStatus } } } @@ -173,9 +209,15 @@ class PatcherViewModel( } init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it. - ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply { - addAction(InstallService.APP_INSTALL_ACTION) - }, ContextCompat.RECEIVER_NOT_EXPORTED) + ContextCompat.registerReceiver( + app, + installerBroadcastReceiver, + IntentFilter().apply { + addAction(InstallService.APP_INSTALL_ACTION) + addAction(UninstallService.APP_UNINSTALL_ACTION) + }, + ContextCompat.RECEIVER_NOT_EXPORTED + ) viewModelScope.launch { installedApp = installedAppRepository.get(packageName) @@ -185,7 +227,7 @@ class PatcherViewModel( @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() - app.unregisterReceiver(installBroadcastReceiver) + app.unregisterReceiver(installerBroadcastReceiver) workManager.cancelWorkById(patcherWorkerId) if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) { @@ -228,20 +270,56 @@ class PatcherViewModel( fun open() = installedPackageName?.let(pm::launch) fun install(installType: InstallType) = viewModelScope.launch { + var pmInstallStarted = false try { isInstalling = true + + val currentPackageInfo = pm.getPackageInfo(outputFile) + ?: throw Exception("Failed to load application info") + + // If the app is currently installed + val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName) + if (existingPackageInfo != null) { + // 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 + return@launch + } + } + when (installType) { InstallType.DEFAULT -> { + // Check if the app is mounted as root + // If it is, unmount it first, silently + if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) { + rootInstaller.unmount(packageName) + } + + // Install regularly pm.installApp(listOf(outputFile)) + pmInstallStarted = true } InstallType.ROOT -> { try { - val label = with(pm) { - getPackageInfo(outputFile)?.label() - ?: throw Exception("Failed to load application info") + // Check for base APK, first check if the app is already installed + if (existingPackageInfo == null) { + // 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 + return@launch + } } + // Get label + val label = with(pm) { + currentPackageInfo.label() + } + + // Install as root rootInstaller.install( outputFile, inputFile, @@ -273,8 +351,22 @@ class PatcherViewModel( } } } + } catch(e: Exception) { + Log.e(tag, "Failed to install", e) + app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) } finally { - 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") + + pm.installApp(listOf(outputFile)) + isInstalling = true } } 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 21a60b97..0d7a822b 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -10,6 +10,7 @@ import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES import android.content.pm.PackageManager.NameNotFoundException +import androidx.core.content.pm.PackageInfoCompat import android.os.Build import android.os.Parcelable import androidx.compose.runtime.Immutable @@ -115,6 +116,8 @@ class PM( fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() + fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo) + suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { val packageInstaller = app.packageManager.packageInstaller packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> @@ -170,4 +173,4 @@ class PM( Intent(this, UninstallService::class.java), intentFlags ).intentSender -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81522032..5b6c6d93 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -257,6 +257,7 @@ Install App installed Failed to install app: %s + Failed to reinstall app: %s Failed to uninstall app: %s Open Save APK @@ -360,6 +361,24 @@ Import local files from your storage, does not automatically update Import remote files from a URL, can automatically update Recommended + + Installation failed + Installation cancelled + Installation blocked + Installation conflict + Installation incompatible + Installation invalid + Not enough storage + Installation timed out + The installation failed due to an unknown reason. Try again? + The installation was cancelled manually. Try again? + The installation was blocked. Review your device security settings and try again. + The installation was prevented by an existing installation of the app. Uninstall the installed app and try again? + The app is incompatible with this device. Use the correct APK for your device and try again. + The app is invalid. Uninstall the app and try again? + The app could not be installed due to insufficient storage. Free up some space and try again. + The installation took too long. Try again? + Reinstall Show Debugging About device diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05e854ed..1a3f425a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ app-icon-loader-coil = "1.5.0" skrapeit = "1.2.2" libsu = "5.2.2" scrollbars = "1.0.4" +enumutil = "1.1.0" compose-icons = "1.2.4" kotlin-process = "1.4.1" hidden-api-stub = "4.3.3" @@ -121,6 +122,10 @@ libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = # Scrollbars scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", version.ref = "scrollbars" } +# EnumUtil +enumutil = { group = "io.github.materiiapps", name = "enumutil", version.ref = "enumutil" } +enumutil-ksp = { group = "io.github.materiiapps", name = "enumutil-ksp", version.ref = "enumutil" } + # Reorderable lists reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } From f6f72387b92f73b5e3b36f3096928fa83e7614a1 Mon Sep 17 00:00:00 2001 From: alieRN <45766489+aliernfrog@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:17:38 +0300 Subject: [PATCH 3/4] feat(patcher): Improve installation (#2185) --- .../manager/data/room/apps/installed/InstalledApp.kt | 2 +- .../revanced/manager/domain/installer/RootInstaller.kt | 6 +++++- .../revanced/manager/patcher/worker/PatcherWorker.kt | 2 +- .../ui/component/patcher/InstallPickerDialog.kt | 3 ++- .../manager/ui/screen/InstalledAppInfoScreen.kt | 6 +++--- .../app/revanced/manager/ui/screen/PatcherScreen.kt | 4 +++- .../manager/ui/screen/VersionSelectorScreen.kt | 4 ++-- .../manager/ui/viewmodel/InstalledAppInfoViewModel.kt | 2 +- .../manager/ui/viewmodel/InstalledAppsViewModel.kt | 4 ++-- .../revanced/manager/ui/viewmodel/PatcherViewModel.kt | 10 ++++++---- app/src/main/res/values/strings.xml | 2 +- 11 files changed, 27 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt index ad7033dd..290a226d 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt @@ -9,7 +9,7 @@ import kotlinx.parcelize.Parcelize enum class InstallType(val stringResource: Int) { DEFAULT(R.string.default_install), - ROOT(R.string.root_install) + MOUNT(R.string.mount_install) } @Parcelize diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 9ca6cd9b..885f8ad1 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -43,7 +43,7 @@ class RootInstaller( } } - return withTimeoutOrNull(Duration.ofSeconds(120L)) { + return withTimeoutOrNull(Duration.ofSeconds(20L)) { remoteFS.await() } ?: throw RootServiceException() } @@ -58,6 +58,10 @@ class RootInstaller( fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false + fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path -> + File(path, "su").canExecute() + } ?: false + suspend fun isAppInstalled(packageName: String) = awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists() diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 0e779df7..a5c551a4 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -135,7 +135,7 @@ class PatcherWorker( return try { if (args.input is SelectedApp.Installed) { installedAppRepository.get(args.packageName)?.let { - if (it.installType == InstallType.ROOT) { + if (it.installType == InstallType.MOUNT) { rootInstaller.unmount(args.packageName) } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt index ec3cf979..e331db2e 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ListItem import androidx.compose.material3.RadioButton import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,7 +28,7 @@ fun InstallPickerDialog( AlertDialog( onDismissRequest = onDismiss, dismissButton = { - Button(onClick = onDismiss) { + TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }, diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt index 2727e290..239aebbf 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt @@ -81,7 +81,7 @@ fun InstalledAppInfoScreen( AppInfo(viewModel.appInfo) { Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium) - if (viewModel.installedApp.installType == InstallType.ROOT) { + if (viewModel.installedApp.installType == InstallType.MOUNT) { Text( text = if (viewModel.isMounted) { stringResource(R.string.mounted) @@ -112,7 +112,7 @@ fun InstalledAppInfoScreen( onClick = viewModel::uninstall ) - InstallType.ROOT -> { + InstallType.MOUNT -> { SegmentedButton( icon = Icons.Outlined.SettingsBackupRestore, text = stringResource(R.string.unpatch), @@ -138,7 +138,7 @@ fun InstalledAppInfoScreen( onPatchClick(viewModel.installedApp.originalPackageName, it) } }, - enabled = viewModel.installedApp.installType != InstallType.ROOT || viewModel.rootInstaller.hasRootAccess() + enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess() ) } 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 096bbf03..2c7792d0 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 @@ -38,6 +38,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.InstallerStatusDialog @@ -139,7 +140,8 @@ fun PatcherScreen( }, onClick = { if (vm.installedPackageName == null) - showInstallPicker = true + if (vm.isDeviceRooted()) showInstallPicker = true + else vm.install(InstallType.DEFAULT) else vm.open() } ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt index 3a5fc3d7..2ca8baa6 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt @@ -113,8 +113,8 @@ fun VersionSelectorScreen( onClick = { viewModel.select(it) }, patchCount = supportedVersions[it.version], enabled = - !(installedApp?.installType == InstallType.ROOT && !viewModel.rootInstaller.hasRootAccess()), - alreadyPatched = installedApp != null && installedApp.installType != InstallType.ROOT + !(installedApp?.installType == InstallType.MOUNT && !viewModel.rootInstaller.hasRootAccess()), + alreadyPatched = installedApp != null && installedApp.installType != InstallType.MOUNT ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt index 7e610f55..93e2cb74 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -78,7 +78,7 @@ class InstalledAppInfoViewModel( when (installedApp.installType) { InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName) - InstallType.ROOT -> viewModelScope.launch { + InstallType.MOUNT -> viewModelScope.launch { rootInstaller.uninstall(installedApp.currentPackageName) installedAppRepository.delete(installedApp) onBackClick() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt index 27bec4c4..42ad08c7 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt @@ -30,7 +30,7 @@ class InstalledAppsViewModel( packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) { try { if ( - installedApp.installType == InstallType.ROOT && !rootInstaller.isAppInstalled(installedApp.currentPackageName) + installedApp.installType == InstallType.MOUNT && !rootInstaller.isAppInstalled(installedApp.currentPackageName) ) { installedAppsRepository.delete(installedApp) return@withContext null @@ -39,7 +39,7 @@ class InstalledAppsViewModel( val packageInfo = pm.getPackageInfo(installedApp.currentPackageName) - if (packageInfo == null && installedApp.installType != InstallType.ROOT) { + if (packageInfo == null && installedApp.installType != InstallType.MOUNT) { installedAppsRepository.delete(installedApp) return@withContext null } 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 ae7f95b9..a63feed7 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 @@ -77,7 +77,7 @@ class PatcherViewModel( override fun install() { // Since this is a package installer status dialog, - // InstallType.ROOT is never used here. + // InstallType.MOUNT is never used here. install(InstallType.DEFAULT) } } @@ -230,7 +230,7 @@ class PatcherViewModel( app.unregisterReceiver(installerBroadcastReceiver) workManager.cancelWorkById(patcherWorkerId) - if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) { + if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) { GlobalScope.launch(Dispatchers.Main) { uiSafe(app, R.string.failed_to_mount, "Failed to mount") { withTimeout(Duration.ofMinutes(1L)) { @@ -243,6 +243,8 @@ class PatcherViewModel( tempDir.deleteRecursively() } + fun isDeviceRooted() = rootInstaller.isDeviceRooted() + fun export(uri: Uri?) = viewModelScope.launch { uri?.let { withContext(Dispatchers.IO) { @@ -301,7 +303,7 @@ class PatcherViewModel( pmInstallStarted = true } - InstallType.ROOT -> { + InstallType.MOUNT -> { try { // Check for base APK, first check if the app is already installed if (existingPackageInfo == null) { @@ -332,7 +334,7 @@ class PatcherViewModel( packageName, packageName, input.selectedApp.version, - InstallType.ROOT, + InstallType.MOUNT, input.selectedPatches ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b6c6d93..ffcfc9fd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -223,7 +223,7 @@ Applied patches View applied patches Default - Root + Mount Mounted Not mounted Mount From 697386c36c769c07db568aced4b8388e7242924d Mon Sep 17 00:00:00 2001 From: kitadai31 <90122968+kitadai31@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:51:22 +0900 Subject: [PATCH 4/4] fix: Match "Installation incompatible" dialog message with Flutter Manager (#2231) --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ffcfc9fd..0b260bb8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -374,7 +374,7 @@ The installation was cancelled manually. Try again? The installation was blocked. Review your device security settings and try again. The installation was prevented by an existing installation of the app. Uninstall the installed app and try again? - The app is incompatible with this device. Use the correct APK for your device and try again. + The app is incompatible with this device. Use an APK that is supported by this device and try again. The app is invalid. Uninstall the app and try again? The app could not be installed due to insufficient storage. Free up some space and try again. The installation took too long. Try again?