From b1a821da37ba6b04b9e8080196b03f2ce85fb176 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 23 Jan 2024 01:08:18 +0100 Subject: [PATCH 01/28] feat: Add installer status dialog --- app/build.gradle.kts | 4 + .../ui/component/InstallerStatusDialog.kt | 166 ++++++++++++++++++ .../manager/ui/screen/PatcherScreen.kt | 4 + .../manager/ui/viewmodel/PatcherViewModel.kt | 80 +++++++-- app/src/main/res/values/strings.xml | 22 +++ gradle/libs.versions.toml | 6 + 6 files changed, 270 insertions(+), 12 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 981d57b90c..a6d5d63643 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -167,4 +167,8 @@ dependencies { // Scrollbars implementation(libs.scrollbars) + + // EnumUtil + implementation(libs.enumutil) + ksp(libs.enumutil.ksp) } 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 0000000000..40e6ea6fef --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -0,0 +1,166 @@ +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() + }, + ), + SUCCESS( + flag = PackageInstaller.STATUS_SUCCESS, + title = R.string.installation_success_dialog_title, + contentStringResId = R.string.installation_success_description, + icon = Icons.Outlined.Check, + ); + + // 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 882d8cfedd..012cc062e2 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 @@ -89,6 +90,9 @@ fun PatcherScreen( onConfirm = vm::install ) + if (vm.installerStatusDialogModel.packageInstallerStatus != null) + InstallerStatusDialog(vm.installerStatusDialogModel) + AppScaffold( topBar = { AppTopBar( 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 d1b3d360fc..02ec2167a0 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 @@ -29,6 +29,8 @@ import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.patcher.logger.ManagerLogger 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 @@ -38,6 +40,7 @@ import app.revanced.manager.util.PM import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.tag import app.revanced.manager.util.toast +import app.revanced.manager.util.uiSafe import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -49,6 +52,11 @@ import java.io.File import java.nio.file.Files import java.util.UUID +data class PackageInstallerResult( + val status: Int = PackageInstaller.STATUS_FAILURE, + val extraStatusMessage: String?, +) + @Stable class PatcherViewModel( private val input: Destination.Patcher @@ -60,6 +68,20 @@ class PatcherViewModel( private val installedAppRepository: InstalledAppRepository by inject() private val rootInstaller: RootInstaller by inject() + val installerStatusDialogModel = object : InstallerStatusDialogModel { + override var packageInstallerStatus 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) @@ -109,7 +131,8 @@ class PatcherViewModel( if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) { currentStepIndex++ - steps[currentStepIndex] = steps[currentStepIndex].copy(state = State.RUNNING) + steps[currentStepIndex] = + steps[currentStepIndex].copy(state = State.RUNNING) } } ) @@ -124,15 +147,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 + ) + + // TODO: This is unused and may be completely removed everywhere. + // intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) if (pmStatus == PackageInstaller.STATUS_SUCCESS) { - app.toast(app.getString(R.string.install_app_success)) installedPackageName = intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME) viewModelScope.launch { @@ -144,8 +171,22 @@ class PatcherViewModel( input.selectedPatches ) } - } else { - app.toast(app.getString(R.string.install_app_fail, extra)) + } + + installerStatusDialogModel.packageInstallerStatus = pmStatus + } + + UninstallService.APP_UNINSTALL_ACTION -> { + val pmStatus = intent.getIntExtra( + UninstallService.EXTRA_UNINSTALL_STATUS, + PackageInstaller.STATUS_FAILURE + ) + + // TODO: This is unused and may be completely removed everywhere. + // intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + + if (pmStatus != PackageInstaller.STATUS_SUCCESS) { + installerStatusDialogModel.packageInstallerStatus = pmStatus } } } @@ -153,9 +194,14 @@ 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) @@ -164,7 +210,7 @@ class PatcherViewModel( override fun onCleared() { super.onCleared() - app.unregisterReceiver(installBroadcastReceiver) + app.unregisterReceiver(installerBroadcastReceiver) workManager.cancelWorkById(patcherWorkerId) when (val selectedApp = input.selectedApp) { @@ -255,7 +301,8 @@ class PatcherViewModel( app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) try { rootInstaller.uninstall(packageName) - } catch (_: Exception) { } + } catch (_: Exception) { + } } } } @@ -264,6 +311,15 @@ class PatcherViewModel( } } + 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)) + } + } + companion object { fun generateSteps( context: Context, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bae85e4178..a8b4e59e87 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,6 +229,7 @@ Install App installed Failed to install app: %s + Failed to reinstall app: %s Failed to uninstall app: %s Open Save APK @@ -328,4 +329,25 @@ 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 + Installation storage issue + Installation timeout + Installation succeeded + 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. Contact the developer of the app and ask for support. + 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? + The app has been installed successfully. + + Reinstall \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 318a0a2b2b..73b2688ab5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,8 @@ app-icon-loader-coil = "1.5.0" skrapeit = "1.2.2" libsu = "5.2.0" scrollbars = "1.0.4" +enumutil = "1.1.0" +enumutil-ksp = "1.1.0" [libraries] # AndroidX Core @@ -110,6 +112,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-ksp" } + [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinGradlePlugin" } From d147cd6a1fa6c66f7ff2cac24b6d22dad8d9a030 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 24 Jan 2024 11:14:36 +0100 Subject: [PATCH 02/28] Adjust strings --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8b4e59e87..cc9cc3c80d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -336,8 +336,8 @@ Installation conflict Installation incompatible Installation invalid - Installation storage issue - Installation timeout + Not enough storage + Installation timed out Installation succeeded The installation failed due to an unknown reason. Try again? The installation was cancelled manually. Try again? From 82709990f53de34cb9225459748394e2d3d54eb6 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 24 Jan 2024 11:14:47 +0100 Subject: [PATCH 03/28] Log extra status message --- .../manager/ui/viewmodel/PatcherViewModel.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) 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 02ec2167a0..3d8ff6591e 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 @@ -52,11 +52,6 @@ import java.io.File import java.nio.file.Files import java.util.UUID -data class PackageInstallerResult( - val status: Int = PackageInstaller.STATUS_FAILURE, - val extraStatusMessage: String?, -) - @Stable class PatcherViewModel( private val input: Destination.Patcher @@ -156,8 +151,8 @@ class PatcherViewModel( PackageInstaller.STATUS_FAILURE ) - // TODO: This is unused and may be completely removed everywhere. - // intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) if (pmStatus == PackageInstaller.STATUS_SUCCESS) { installedPackageName = @@ -182,8 +177,8 @@ class PatcherViewModel( PackageInstaller.STATUS_FAILURE ) - // TODO: This is unused and may be completely removed everywhere. - // intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) if (pmStatus != PackageInstaller.STATUS_SUCCESS) { installerStatusDialogModel.packageInstallerStatus = pmStatus From 72fc6d2bd47b28ff10a470927f26cd42e51dd19b Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sat, 6 Jul 2024 09:49:17 -0700 Subject: [PATCH 04/28] Update PatcherViewModel.kt --- .../manager/ui/viewmodel/PatcherViewModel.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 a52333cb9f..512c63730c 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 @@ -270,6 +270,11 @@ class PatcherViewModel( context.startActivity(shareIntent) } + fun versionNameToInt(versionName: String): Int { + val versionParts = versionName.split(".") + return versionParts[0].toInt() * 10000 + versionParts[1].toInt() * 100 + versionParts[2].toInt() + } + fun open() = installedPackageName?.let(pm::launch) fun install(installType: InstallType) = viewModelScope.launch { @@ -277,6 +282,22 @@ class PatcherViewModel( isInstalling = true 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) + } + + // If the app is currently installed + val packageInfo = pm.getPackageInfo(packageName) + if (packageInfo != null) { + // Check if the app version is less than the installed version + if (versionNameToInt(packageInfo.versionName) < versionNameToInt(input.selectedApp.version)) { + reinstall() + return@launch + } + } + pm.installApp(listOf(outputFile)) } From ffe5826f150ef28c79dc750f056d5299d2bb3791 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sat, 6 Jul 2024 17:11:02 -0700 Subject: [PATCH 05/28] Show conflict dialogue when attempting to install a lower version --- .../java/app/revanced/manager/ui/screen/PatcherScreen.kt | 4 +++- .../app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) 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 29025b0d1a..0256d1b3fe 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 @@ -176,4 +176,6 @@ fun PatcherScreen( } } } -} \ No newline at end of file +} + +// Create an uninstall dialogue 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 ab18a4b6f5..ec63ec4ae5 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 @@ -69,8 +69,8 @@ class PatcherViewModel( private val installedAppRepository: InstalledAppRepository by inject() private val rootInstaller: RootInstaller by inject() - val installerStatusDialogModel = object : InstallerStatusDialogModel { - override var packageInstallerStatus by mutableStateOf(null) + val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel { + override var packageInstallerStatus: Int? by mutableStateOf(null) override fun reinstall() { this@PatcherViewModel.reinstall() @@ -300,7 +300,8 @@ class PatcherViewModel( if (packageInfo != null) { // Check if the app version is less than the installed version if (versionNameToInt(packageInfo.versionName) < versionNameToInt(input.selectedApp.version)) { - reinstall() + // Exit if the installed version is less than the selected version + installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT return@launch } } From 6f62bfb386b0f4632ac0ce582a8b9c6af4fea5a2 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 7 Jul 2024 14:14:45 -0700 Subject: [PATCH 06/28] Check for fingerprint differences and base APKs --- .../manager/ui/viewmodel/PatcherViewModel.kt | 98 ++++++++++++++++--- 1 file changed, 84 insertions(+), 14 deletions(-) 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 ec63ec4ae5..5b2331f272 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 @@ -5,7 +5,9 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageInfo import android.content.pm.PackageInstaller +import android.content.pm.PackageManager import android.net.Uri import android.util.Log import androidx.compose.runtime.Stable @@ -53,8 +55,12 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import java.io.ByteArrayInputStream import java.io.File import java.nio.file.Files +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.MessageDigest import java.time.Duration import java.util.UUID @@ -277,45 +283,109 @@ class PatcherViewModel( context.startActivity(shareIntent) } - fun versionNameToInt(versionName: String): Int { + private fun versionNameToInt(versionName: String): Int { val versionParts = versionName.split(".") return versionParts[0].toInt() * 10000 + versionParts[1].toInt() * 100 + versionParts[2].toInt() } + private fun getFingerprint(packageInfo: PackageInfo?): String { + // Get the signature of the app that matches the package name + val signature = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + val signingInfo = packageInfo?.signingInfo + signingInfo?.signingCertificateHistory?.first() + } else { + //TODO: Add support for older versions + return "" + } ?: throw Exception("Failed to get signature") + + // Get the raw certificate data + val rawCert = signature.toByteArray() + + // Generate an X509Certificate from the data + val certFactory = CertificateFactory.getInstance("X509") + val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate + + // Get the SHA256 fingerprint + val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") { + "%02x".format(it) + } + + return fingerprint + } + fun open() = installedPackageName?.let(pm::launch) fun install(installType: InstallType) = viewModelScope.launch { try { isInstalling = true + + // If the app is currently installed + val existingPackageInfo = pm.getPackageInfo(packageName) + if (existingPackageInfo != null) { + // Check if the app version is less than the installed version + if (versionNameToInt(existingPackageInfo.versionName) < versionNameToInt(input.selectedApp.version)) { + // Exit if the selected app version is less than the installed version + installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT + return@launch + } + } + + val currentPackageInfo = pm.getPackageInfo(outputFile) + ?: throw Exception("Failed to load application info") + when (installType) { InstallType.DEFAULT -> { + // Check if existing app has the same signature + if (existingPackageInfo != null && getFingerprint(existingPackageInfo) != getFingerprint(currentPackageInfo)) { + installerStatusDialogModel.packageInstallerStatus = + PackageInstaller.STATUS_FAILURE_CONFLICT + return@launch + } + // Check if the app is mounted as root // If it is, unmount it first, silently if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) { rootInstaller.unmount(packageName) } - // If the app is currently installed - val packageInfo = pm.getPackageInfo(packageName) - if (packageInfo != null) { - // Check if the app version is less than the installed version - if (versionNameToInt(packageInfo.versionName) < versionNameToInt(input.selectedApp.version)) { - // Exit if the installed version is less than the selected version - installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT - return@launch - } - } - + // Install regularly pm.installApp(listOf(outputFile)) } InstallType.ROOT -> { try { + // Get input package info + val inputPackageInfo = if (inputFile != null) + pm.getPackageInfo(inputFile!!) + else + null + + // Check for base APK, first check if the app is already installed + if (existingPackageInfo == null) { + // If the app is not installed, check if the input file is a base apk + if (inputPackageInfo == null || inputPackageInfo.splitNames != null) { + // Exit if there is no base APK package + installerStatusDialogModel.packageInstallerStatus = + PackageInstaller.STATUS_FAILURE_INVALID + return@launch + } + } + + // Check if stock apk has the same fingerprint as the installed apk + if (existingPackageInfo != null && inputPackageInfo != null) { + if (getFingerprint(existingPackageInfo) != getFingerprint(inputPackageInfo)) { + installerStatusDialogModel.packageInstallerStatus = + PackageInstaller.STATUS_FAILURE_CONFLICT + return@launch + } + } + + // Get label val label = with(pm) { - getPackageInfo(outputFile)?.label() - ?: throw Exception("Failed to load application info") + currentPackageInfo.label() } + // Install as root rootInstaller.install( outputFile, inputFile, From cadc3da5377f39e0e4cfa2f451f50a6263913e2c Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 7 Jul 2024 14:27:12 -0700 Subject: [PATCH 07/28] Remove fingerprint detection --- .../manager/ui/viewmodel/PatcherViewModel.kt | 41 ------------------- 1 file changed, 41 deletions(-) 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 5b2331f272..5ad16b4fb4 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 @@ -288,31 +288,6 @@ class PatcherViewModel( return versionParts[0].toInt() * 10000 + versionParts[1].toInt() * 100 + versionParts[2].toInt() } - private fun getFingerprint(packageInfo: PackageInfo?): String { - // Get the signature of the app that matches the package name - val signature = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { - val signingInfo = packageInfo?.signingInfo - signingInfo?.signingCertificateHistory?.first() - } else { - //TODO: Add support for older versions - return "" - } ?: throw Exception("Failed to get signature") - - // Get the raw certificate data - val rawCert = signature.toByteArray() - - // Generate an X509Certificate from the data - val certFactory = CertificateFactory.getInstance("X509") - val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate - - // Get the SHA256 fingerprint - val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") { - "%02x".format(it) - } - - return fingerprint - } - fun open() = installedPackageName?.let(pm::launch) fun install(installType: InstallType) = viewModelScope.launch { @@ -335,13 +310,6 @@ class PatcherViewModel( when (installType) { InstallType.DEFAULT -> { - // Check if existing app has the same signature - if (existingPackageInfo != null && getFingerprint(existingPackageInfo) != getFingerprint(currentPackageInfo)) { - installerStatusDialogModel.packageInstallerStatus = - PackageInstaller.STATUS_FAILURE_CONFLICT - return@launch - } - // Check if the app is mounted as root // If it is, unmount it first, silently if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) { @@ -371,15 +339,6 @@ class PatcherViewModel( } } - // Check if stock apk has the same fingerprint as the installed apk - if (existingPackageInfo != null && inputPackageInfo != null) { - if (getFingerprint(existingPackageInfo) != getFingerprint(inputPackageInfo)) { - installerStatusDialogModel.packageInstallerStatus = - PackageInstaller.STATUS_FAILURE_CONFLICT - return@launch - } - } - // Get label val label = with(pm) { currentPackageInfo.label() From fcb2242e36c439825afee9362e3c8844d7b74c05 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 7 Jul 2024 14:33:42 -0700 Subject: [PATCH 08/28] Clean up code --- .../revanced/manager/ui/viewmodel/PatcherViewModel.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 5ad16b4fb4..dfbe10f5db 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 @@ -322,14 +322,13 @@ class PatcherViewModel( InstallType.ROOT -> { try { - // Get input package info - val inputPackageInfo = if (inputFile != null) - pm.getPackageInfo(inputFile!!) - else - null - // Check for base APK, first check if the app is already installed if (existingPackageInfo == null) { + val inputPackageInfo = if (inputFile != null) + pm.getPackageInfo(inputFile!!) + else + null + // If the app is not installed, check if the input file is a base apk if (inputPackageInfo == null || inputPackageInfo.splitNames != null) { // Exit if there is no base APK package From 874f8baff37ee47194266eb6c4273dd1b5d15f44 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sat, 27 Jul 2024 14:19:45 -0700 Subject: [PATCH 09/28] Updated detection of lower versions --- .../app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 dfbe10f5db..3eb034d628 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 @@ -298,7 +298,7 @@ class PatcherViewModel( val existingPackageInfo = pm.getPackageInfo(packageName) if (existingPackageInfo != null) { // Check if the app version is less than the installed version - if (versionNameToInt(existingPackageInfo.versionName) < versionNameToInt(input.selectedApp.version)) { + if (versionNameToInt(input.selectedApp.version) < versionNameToInt(existingPackageInfo.versionName)) { // Exit if the selected app version is less than the installed version installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT return@launch @@ -384,7 +384,7 @@ class PatcherViewModel( 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)) } } From 0d23d7e2903467275bc10d839910b1a2656f487c Mon Sep 17 00:00:00 2001 From: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:30:23 -0700 Subject: [PATCH 10/28] Update app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt Co-authored-by: Ushie --- .../revanced/manager/ui/component/InstallerStatusDialog.kt | 7 ------- 1 file changed, 7 deletions(-) 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 40e6ea6fef..c7050976b2 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 @@ -154,13 +154,6 @@ enum class DialogKind( model.install() }, ), - SUCCESS( - flag = PackageInstaller.STATUS_SUCCESS, - title = R.string.installation_success_dialog_title, - contentStringResId = R.string.installation_success_description, - icon = Icons.Outlined.Check, - ); - // Needed due to the @FromValue annotation. companion object } From 3c4eaf55164aa708e4de8054917a0117493337c8 Mon Sep 17 00:00:00 2001 From: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:30:34 -0700 Subject: [PATCH 11/28] Update app/src/main/res/values/strings.xml Co-authored-by: Ushie --- app/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e6abfe8a3..d834b8bcae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -369,7 +369,6 @@ Installation invalid Not enough storage Installation timed out - Installation succeeded 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. From 2fa546109f26a8f4691f2411db2b2ccb5e6a7e72 Mon Sep 17 00:00:00 2001 From: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:30:45 -0700 Subject: [PATCH 12/28] Update app/src/main/res/values/strings.xml Co-authored-by: Ushie --- app/src/main/res/values/strings.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d834b8bcae..77526429c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -377,8 +377,6 @@ 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? - The app has been installed successfully. - Reinstall Show Debugging From 453958442f21ce589bb53812ae7a2f3d1a868636 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 28 Jul 2024 13:09:15 -0700 Subject: [PATCH 13/28] Applied fixes --- .../manager/ui/viewmodel/PatcherViewModel.kt | 12 +- .../main/java/app/revanced/manager/util/PM.kt | 349 +++++++++--------- gradle/libs.versions.toml | 3 +- 3 files changed, 180 insertions(+), 184 deletions(-) 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 3eb034d628..67e3d56d7c 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 @@ -283,11 +283,6 @@ class PatcherViewModel( context.startActivity(shareIntent) } - private fun versionNameToInt(versionName: String): Int { - val versionParts = versionName.split(".") - return versionParts[0].toInt() * 10000 + versionParts[1].toInt() * 100 + versionParts[2].toInt() - } - fun open() = installedPackageName?.let(pm::launch) fun install(installType: InstallType) = viewModelScope.launch { @@ -298,7 +293,7 @@ class PatcherViewModel( val existingPackageInfo = pm.getPackageInfo(packageName) if (existingPackageInfo != null) { // Check if the app version is less than the installed version - if (versionNameToInt(input.selectedApp.version) < versionNameToInt(existingPackageInfo.versionName)) { + if (pm.versionNameToInt(input.selectedApp.version) < pm.versionNameToInt(existingPackageInfo.versionName)) { // Exit if the selected app version is less than the installed version installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT return@launch @@ -324,10 +319,7 @@ class PatcherViewModel( try { // Check for base APK, first check if the app is already installed if (existingPackageInfo == null) { - val inputPackageInfo = if (inputFile != null) - pm.getPackageInfo(inputFile!!) - else - null + val inputPackageInfo = inputFile?.let { pm.getPackageInfo(it) } // If the app is not installed, check if the input file is a base apk if (inputPackageInfo == null || inputPackageInfo.splitNames != null) { 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 21a60b97c2..ff742e46f8 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -1,173 +1,178 @@ -package app.revanced.manager.util - -import android.annotation.SuppressLint -import android.app.Application -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInfo -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 android.os.Build -import android.os.Parcelable -import androidx.compose.runtime.Immutable -import app.revanced.manager.domain.repository.PatchBundleRepository -import app.revanced.manager.service.InstallService -import app.revanced.manager.service.UninstallService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize -import java.io.File - -private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable - -@Immutable -@Parcelize -data class AppInfo( - val packageName: String, - val patches: Int?, - val packageInfo: PackageInfo? -) : Parcelable - -@SuppressLint("QueryPermissionsNeeded") -@Suppress("DEPRECATION") -class PM( - private val app: Application, - patchBundleRepository: PatchBundleRepository -) { - private val scope = CoroutineScope(Dispatchers.IO) - - val appList = patchBundleRepository.bundles.map { bundles -> - val compatibleApps = scope.async { - val compatiblePackages = bundles.values - .flatMap { it.patches } - .flatMap { it.compatiblePackages.orEmpty() } - .groupingBy { it.packageName } - .eachCount() - - compatiblePackages.keys.map { pkg -> - getPackageInfo(pkg)?.let { packageInfo -> - AppInfo( - pkg, - compatiblePackages[pkg], - packageInfo - ) - } ?: AppInfo( - pkg, - compatiblePackages[pkg], - null - ) - } - } - - val installedApps = scope.async { - app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo -> - AppInfo( - packageInfo.packageName, - 0, - packageInfo - ) - } - } - - if (compatibleApps.await().isNotEmpty()) { - (compatibleApps.await() + installedApps.await()) - .distinctBy { it.packageName } - .sortedWith( - compareByDescending{ - it.packageInfo != null && (it.patches ?: 0) > 0 - }.thenByDescending { - it.patches - }.thenBy { - it.packageInfo?.label() - }.thenBy { it.packageName } - ) - } else { - emptyList() - } - }.flowOn(Dispatchers.IO) - - fun getPackageInfo(packageName: String): PackageInfo? = - try { - app.packageManager.getPackageInfo(packageName, 0) - } catch (e: NameNotFoundException) { - null - } - - fun getPackageInfo(file: File): PackageInfo? { - val path = file.absolutePath - val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null - - // This is needed in order to load label and icon. - pkgInfo.applicationInfo.apply { - sourceDir = path - publicSourceDir = path - } - - return pkgInfo - } - - fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() - - suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { - val packageInstaller = app.packageManager.packageInstaller - packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> - apks.forEach { apk -> session.writeApk(apk) } - session.commit(app.installIntentSender) - } - } - - fun uninstallPackage(pkg: String) { - val packageInstaller = app.packageManager.packageInstaller - packageInstaller.uninstall(pkg, app.uninstallIntentSender) - } - - fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { - it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - app.startActivity(it) - } - - private fun PackageInstaller.Session.writeApk(apk: File) { - apk.inputStream().use { inputStream -> - openWrite(apk.name, 0, apk.length()).use { outputStream -> - inputStream.copyTo(outputStream, byteArraySize) - fsync(outputStream) - } - } - } - - private val intentFlags - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - PendingIntent.FLAG_MUTABLE - else - 0 - - private val sessionParams - get() = PackageInstaller.SessionParams( - PackageInstaller.SessionParams.MODE_FULL_INSTALL - ).apply { - setInstallReason(PackageManager.INSTALL_REASON_USER) - } - - private val Context.installIntentSender - get() = PendingIntent.getService( - this, - 0, - Intent(this, InstallService::class.java), - intentFlags - ).intentSender - - private val Context.uninstallIntentSender - get() = PendingIntent.getService( - this, - 0, - Intent(this, UninstallService::class.java), - intentFlags - ).intentSender +package app.revanced.manager.util + +import android.annotation.SuppressLint +import android.app.Application +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo +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 android.os.Build +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.service.InstallService +import app.revanced.manager.service.UninstallService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import java.io.File + +private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable + +@Immutable +@Parcelize +data class AppInfo( + val packageName: String, + val patches: Int?, + val packageInfo: PackageInfo? +) : Parcelable + +@SuppressLint("QueryPermissionsNeeded") +@Suppress("DEPRECATION") +class PM( + private val app: Application, + patchBundleRepository: PatchBundleRepository +) { + private val scope = CoroutineScope(Dispatchers.IO) + + val appList = patchBundleRepository.bundles.map { bundles -> + val compatibleApps = scope.async { + val compatiblePackages = bundles.values + .flatMap { it.patches } + .flatMap { it.compatiblePackages.orEmpty() } + .groupingBy { it.packageName } + .eachCount() + + compatiblePackages.keys.map { pkg -> + getPackageInfo(pkg)?.let { packageInfo -> + AppInfo( + pkg, + compatiblePackages[pkg], + packageInfo + ) + } ?: AppInfo( + pkg, + compatiblePackages[pkg], + null + ) + } + } + + val installedApps = scope.async { + app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo -> + AppInfo( + packageInfo.packageName, + 0, + packageInfo + ) + } + } + + if (compatibleApps.await().isNotEmpty()) { + (compatibleApps.await() + installedApps.await()) + .distinctBy { it.packageName } + .sortedWith( + compareByDescending{ + it.packageInfo != null && (it.patches ?: 0) > 0 + }.thenByDescending { + it.patches + }.thenBy { + it.packageInfo?.label() + }.thenBy { it.packageName } + ) + } else { + emptyList() + } + }.flowOn(Dispatchers.IO) + + fun getPackageInfo(packageName: String): PackageInfo? = + try { + app.packageManager.getPackageInfo(packageName, 0) + } catch (e: NameNotFoundException) { + null + } + + fun getPackageInfo(file: File): PackageInfo? { + val path = file.absolutePath + val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null + + // This is needed in order to load label and icon. + pkgInfo.applicationInfo.apply { + sourceDir = path + publicSourceDir = path + } + + return pkgInfo + } + + fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() + + fun versionNameToInt(versionName: String): Int { + val versionParts = versionName.split(".") + return versionParts[0].toInt() * 10000 + versionParts[1].toInt() * 100 + versionParts[2].toInt() + } + + suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { + val packageInstaller = app.packageManager.packageInstaller + packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> + apks.forEach { apk -> session.writeApk(apk) } + session.commit(app.installIntentSender) + } + } + + fun uninstallPackage(pkg: String) { + val packageInstaller = app.packageManager.packageInstaller + packageInstaller.uninstall(pkg, app.uninstallIntentSender) + } + + fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + app.startActivity(it) + } + + private fun PackageInstaller.Session.writeApk(apk: File) { + apk.inputStream().use { inputStream -> + openWrite(apk.name, 0, apk.length()).use { outputStream -> + inputStream.copyTo(outputStream, byteArraySize) + fsync(outputStream) + } + } + } + + private val intentFlags + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + PendingIntent.FLAG_MUTABLE + else + 0 + + private val sessionParams + get() = PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL + ).apply { + setInstallReason(PackageManager.INSTALL_REASON_USER) + } + + private val Context.installIntentSender + get() = PendingIntent.getService( + this, + 0, + Intent(this, InstallService::class.java), + intentFlags + ).intentSender + + private val Context.uninstallIntentSender + get() = PendingIntent.getService( + this, + 0, + Intent(this, UninstallService::class.java), + intentFlags + ).intentSender } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b0a4ae9f5..1a3f425a6d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,6 @@ skrapeit = "1.2.2" libsu = "5.2.2" scrollbars = "1.0.4" enumutil = "1.1.0" -enumutil-ksp = "1.1.0" compose-icons = "1.2.4" kotlin-process = "1.4.1" hidden-api-stub = "4.3.3" @@ -125,7 +124,7 @@ scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", versio # EnumUtil enumutil = { group = "io.github.materiiapps", name = "enumutil", version.ref = "enumutil" } -enumutil-ksp = { group = "io.github.materiiapps", name = "enumutil-ksp", version.ref = "enumutil-ksp" } +enumutil-ksp = { group = "io.github.materiiapps", name = "enumutil-ksp", version.ref = "enumutil" } # Reorderable lists reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } From dc05099ace4a7d45162cecc0d7aee6824003d0d0 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 28 Jul 2024 13:11:10 -0700 Subject: [PATCH 14/28] Added missing semicolon --- .../app/revanced/manager/ui/component/InstallerStatusDialog.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c7050976b2..a31a813eab 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 @@ -153,7 +153,7 @@ enum class DialogKind( confirmButton = installerStatusDialogButton(R.string.install_app) { model -> model.install() }, - ), + ); // Needed due to the @FromValue annotation. companion object } From e48013a674b2d92d910b6c84ab5126e153d895ec Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 28 Jul 2024 13:51:42 -0700 Subject: [PATCH 15/28] Hide install button when installing --- .../main/java/app/revanced/manager/ui/screen/PatcherScreen.kt | 3 ++- .../app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 0256d1b3fe..2494b640bb 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 @@ -62,6 +62,7 @@ fun PatcherScreen( val patcherSucceeded by vm.patcherSucceeded.observeAsState(null) val canInstall by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null || !vm.isInstalling) } } + val canSaveApk by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null) } } var showInstallPicker by rememberSaveable { mutableStateOf(false) } val steps by remember { @@ -107,7 +108,7 @@ fun PatcherScreen( actions = { IconButton( onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, - enabled = canInstall + enabled = canSaveApk ) { Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) } 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 67e3d56d7c..90a4ad53fe 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 @@ -194,6 +194,8 @@ class PatcherViewModel( } installerStatusDialogModel.packageInstallerStatus = pmStatus + + isInstalling = false } UninstallService.APP_UNINSTALL_ACTION -> { @@ -367,7 +369,7 @@ class PatcherViewModel( } } } - } finally { + } catch(_: Exception) { isInstalling = false } } From 3f92371bd8e3621799a2ddfbe10242c9a30204c2 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 28 Jul 2024 13:56:35 -0700 Subject: [PATCH 16/28] Get version code the correct way --- .../revanced/manager/ui/viewmodel/PatcherViewModel.kt | 8 ++++---- app/src/main/java/app/revanced/manager/util/PM.kt | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) 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 90a4ad53fe..856eff8e8d 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 @@ -291,20 +291,20 @@ class PatcherViewModel( 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(packageName) if (existingPackageInfo != null) { // Check if the app version is less than the installed version - if (pm.versionNameToInt(input.selectedApp.version) < pm.versionNameToInt(existingPackageInfo.versionName)) { + 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 } } - val currentPackageInfo = pm.getPackageInfo(outputFile) - ?: throw Exception("Failed to load application info") - when (installType) { InstallType.DEFAULT -> { // Check if the app is mounted as root 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 ff742e46f8..2f722d3d58 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -115,9 +115,12 @@ class PM( fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() - fun versionNameToInt(versionName: String): Int { - val versionParts = versionName.split(".") - return versionParts[0].toInt() * 10000 + versionParts[1].toInt() * 100 + versionParts[2].toInt() + fun getVersionCode(packageInfo: PackageInfo): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode.toInt() + } else { + packageInfo.versionCode + } } suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { From 32d46817b56a74bd31dd585379197a76c4f84449 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 4 Aug 2024 16:43:44 -0700 Subject: [PATCH 17/28] Fixed some issues --- .../main/java/app/revanced/manager/ui/screen/PatcherScreen.kt | 2 -- app/src/main/java/app/revanced/manager/util/PM.kt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) 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 2494b640bb..34d83bbeae 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 @@ -178,5 +178,3 @@ fun PatcherScreen( } } } - -// Create an uninstall dialogue 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 2f722d3d58..4d5cbe3d31 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -178,4 +178,4 @@ class PM( Intent(this, UninstallService::class.java), intentFlags ).intentSender -} \ No newline at end of file +} From 982d3e644571deb39fa0119ead1f55031cfe7f6b Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 4 Aug 2024 16:47:44 -0700 Subject: [PATCH 18/28] Set `isInstalling` to false when a root error occurs --- .../app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 856eff8e8d..8fa33cf8f0 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 @@ -366,10 +366,13 @@ class PatcherViewModel( rootInstaller.uninstall(packageName) } catch (_: Exception) { } + isInstalling = false } } } - } catch(_: Exception) { + } catch(e: Exception) { + Log.e(tag, "Failed to install", e) + app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) isInstalling = false } } From 970c3fcd38fbdd595d73e6dd2093820d75b32fa0 Mon Sep 17 00:00:00 2001 From: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Date: Tue, 6 Aug 2024 06:38:03 -0700 Subject: [PATCH 19/28] Update app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt Co-authored-by: Ax333l --- .../java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 8fa33cf8f0..034403ff89 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 @@ -217,7 +217,8 @@ 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, installerBroadcastReceiver, + app, + installerBroadcastReceiver, IntentFilter().apply { addAction(InstallService.APP_INSTALL_ACTION) addAction(UninstallService.APP_UNINSTALL_ACTION) From 23c41ac1ac054e64f7765e3f2d254faa10942edb Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Sun, 11 Aug 2024 09:10:41 -0700 Subject: [PATCH 20/28] Add finally block --- .../manager/ui/viewmodel/PatcherViewModel.kt | 10 +- .../main/java/app/revanced/manager/util/PM.kt | 362 +++++++++--------- 2 files changed, 188 insertions(+), 184 deletions(-) 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 034403ff89..cddfa9fa31 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 @@ -289,6 +289,7 @@ class PatcherViewModel( fun open() = installedPackageName?.let(pm::launch) fun install(installType: InstallType) = viewModelScope.launch { + var isInstallingRegularly = false try { isInstalling = true @@ -315,6 +316,7 @@ class PatcherViewModel( } // Install regularly + isInstallingRegularly = true pm.installApp(listOf(outputFile)) } @@ -367,14 +369,15 @@ class PatcherViewModel( rootInstaller.uninstall(packageName) } catch (_: Exception) { } - isInstalling = false } } } } catch(e: Exception) { Log.e(tag, "Failed to install", e) app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) - isInstalling = false + } finally { + if (!isInstallingRegularly) + isInstalling = false } } @@ -382,8 +385,9 @@ class PatcherViewModel( 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 4d5cbe3d31..e42b0459bb 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -1,181 +1,181 @@ -package app.revanced.manager.util - -import android.annotation.SuppressLint -import android.app.Application -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInfo -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 android.os.Build -import android.os.Parcelable -import androidx.compose.runtime.Immutable -import app.revanced.manager.domain.repository.PatchBundleRepository -import app.revanced.manager.service.InstallService -import app.revanced.manager.service.UninstallService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize -import java.io.File - -private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable - -@Immutable -@Parcelize -data class AppInfo( - val packageName: String, - val patches: Int?, - val packageInfo: PackageInfo? -) : Parcelable - -@SuppressLint("QueryPermissionsNeeded") -@Suppress("DEPRECATION") -class PM( - private val app: Application, - patchBundleRepository: PatchBundleRepository -) { - private val scope = CoroutineScope(Dispatchers.IO) - - val appList = patchBundleRepository.bundles.map { bundles -> - val compatibleApps = scope.async { - val compatiblePackages = bundles.values - .flatMap { it.patches } - .flatMap { it.compatiblePackages.orEmpty() } - .groupingBy { it.packageName } - .eachCount() - - compatiblePackages.keys.map { pkg -> - getPackageInfo(pkg)?.let { packageInfo -> - AppInfo( - pkg, - compatiblePackages[pkg], - packageInfo - ) - } ?: AppInfo( - pkg, - compatiblePackages[pkg], - null - ) - } - } - - val installedApps = scope.async { - app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo -> - AppInfo( - packageInfo.packageName, - 0, - packageInfo - ) - } - } - - if (compatibleApps.await().isNotEmpty()) { - (compatibleApps.await() + installedApps.await()) - .distinctBy { it.packageName } - .sortedWith( - compareByDescending{ - it.packageInfo != null && (it.patches ?: 0) > 0 - }.thenByDescending { - it.patches - }.thenBy { - it.packageInfo?.label() - }.thenBy { it.packageName } - ) - } else { - emptyList() - } - }.flowOn(Dispatchers.IO) - - fun getPackageInfo(packageName: String): PackageInfo? = - try { - app.packageManager.getPackageInfo(packageName, 0) - } catch (e: NameNotFoundException) { - null - } - - fun getPackageInfo(file: File): PackageInfo? { - val path = file.absolutePath - val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null - - // This is needed in order to load label and icon. - pkgInfo.applicationInfo.apply { - sourceDir = path - publicSourceDir = path - } - - return pkgInfo - } - - fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() - - fun getVersionCode(packageInfo: PackageInfo): Int { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - packageInfo.longVersionCode.toInt() - } else { - packageInfo.versionCode - } - } - - suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { - val packageInstaller = app.packageManager.packageInstaller - packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> - apks.forEach { apk -> session.writeApk(apk) } - session.commit(app.installIntentSender) - } - } - - fun uninstallPackage(pkg: String) { - val packageInstaller = app.packageManager.packageInstaller - packageInstaller.uninstall(pkg, app.uninstallIntentSender) - } - - fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { - it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - app.startActivity(it) - } - - private fun PackageInstaller.Session.writeApk(apk: File) { - apk.inputStream().use { inputStream -> - openWrite(apk.name, 0, apk.length()).use { outputStream -> - inputStream.copyTo(outputStream, byteArraySize) - fsync(outputStream) - } - } - } - - private val intentFlags - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - PendingIntent.FLAG_MUTABLE - else - 0 - - private val sessionParams - get() = PackageInstaller.SessionParams( - PackageInstaller.SessionParams.MODE_FULL_INSTALL - ).apply { - setInstallReason(PackageManager.INSTALL_REASON_USER) - } - - private val Context.installIntentSender - get() = PendingIntent.getService( - this, - 0, - Intent(this, InstallService::class.java), - intentFlags - ).intentSender - - private val Context.uninstallIntentSender - get() = PendingIntent.getService( - this, - 0, - Intent(this, UninstallService::class.java), - intentFlags - ).intentSender -} +package app.revanced.manager.util + +import android.annotation.SuppressLint +import android.app.Application +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo +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 android.os.Build +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.service.InstallService +import app.revanced.manager.service.UninstallService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import java.io.File + +private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable + +@Immutable +@Parcelize +data class AppInfo( + val packageName: String, + val patches: Int?, + val packageInfo: PackageInfo? +) : Parcelable + +@SuppressLint("QueryPermissionsNeeded") +@Suppress("DEPRECATION") +class PM( + private val app: Application, + patchBundleRepository: PatchBundleRepository +) { + private val scope = CoroutineScope(Dispatchers.IO) + + val appList = patchBundleRepository.bundles.map { bundles -> + val compatibleApps = scope.async { + val compatiblePackages = bundles.values + .flatMap { it.patches } + .flatMap { it.compatiblePackages.orEmpty() } + .groupingBy { it.packageName } + .eachCount() + + compatiblePackages.keys.map { pkg -> + getPackageInfo(pkg)?.let { packageInfo -> + AppInfo( + pkg, + compatiblePackages[pkg], + packageInfo + ) + } ?: AppInfo( + pkg, + compatiblePackages[pkg], + null + ) + } + } + + val installedApps = scope.async { + app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo -> + AppInfo( + packageInfo.packageName, + 0, + packageInfo + ) + } + } + + if (compatibleApps.await().isNotEmpty()) { + (compatibleApps.await() + installedApps.await()) + .distinctBy { it.packageName } + .sortedWith( + compareByDescending{ + it.packageInfo != null && (it.patches ?: 0) > 0 + }.thenByDescending { + it.patches + }.thenBy { + it.packageInfo?.label() + }.thenBy { it.packageName } + ) + } else { + emptyList() + } + }.flowOn(Dispatchers.IO) + + fun getPackageInfo(packageName: String): PackageInfo? = + try { + app.packageManager.getPackageInfo(packageName, 0) + } catch (e: NameNotFoundException) { + null + } + + fun getPackageInfo(file: File): PackageInfo? { + val path = file.absolutePath + val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null + + // This is needed in order to load label and icon. + pkgInfo.applicationInfo.apply { + sourceDir = path + publicSourceDir = path + } + + return pkgInfo + } + + fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() + + fun getVersionCode(packageInfo: PackageInfo): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode.toInt() + } else { + packageInfo.versionCode + } + } + + suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { + val packageInstaller = app.packageManager.packageInstaller + packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> + apks.forEach { apk -> session.writeApk(apk) } + session.commit(app.installIntentSender) + } + } + + fun uninstallPackage(pkg: String) { + val packageInstaller = app.packageManager.packageInstaller + packageInstaller.uninstall(pkg, app.uninstallIntentSender) + } + + fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + app.startActivity(it) + } + + private fun PackageInstaller.Session.writeApk(apk: File) { + apk.inputStream().use { inputStream -> + openWrite(apk.name, 0, apk.length()).use { outputStream -> + inputStream.copyTo(outputStream, byteArraySize) + fsync(outputStream) + } + } + } + + private val intentFlags + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + PendingIntent.FLAG_MUTABLE + else + 0 + + private val sessionParams + get() = PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL + ).apply { + setInstallReason(PackageManager.INSTALL_REASON_USER) + } + + private val Context.installIntentSender + get() = PendingIntent.getService( + this, + 0, + Intent(this, InstallService::class.java), + intentFlags + ).intentSender + + private val Context.uninstallIntentSender + get() = PendingIntent.getService( + this, + 0, + Intent(this, UninstallService::class.java), + intentFlags + ).intentSender +} From 85546924f3fa92e41ae48692ca2575f78d4c7449 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Mon, 12 Aug 2024 13:10:03 -0700 Subject: [PATCH 21/28] Update PatcherViewModel.kt --- .../app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 cddfa9fa31..2c79df31ed 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 @@ -289,7 +289,7 @@ class PatcherViewModel( fun open() = installedPackageName?.let(pm::launch) fun install(installType: InstallType) = viewModelScope.launch { - var isInstallingRegularly = false + var pmInstallStarted = false try { isInstalling = true @@ -316,8 +316,8 @@ class PatcherViewModel( } // Install regularly - isInstallingRegularly = true pm.installApp(listOf(outputFile)) + pmInstallStarted = true } InstallType.ROOT -> { @@ -376,7 +376,7 @@ class PatcherViewModel( Log.e(tag, "Failed to install", e) app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) } finally { - if (!isInstallingRegularly) + if (!pmInstallStarted) isInstalling = false } } From dc7b419f9e87ae11bbf1051f0e9f23f7910a28c5 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Mon, 12 Aug 2024 13:18:08 -0700 Subject: [PATCH 22/28] Update getting package info --- .../revanced/manager/ui/viewmodel/PatcherViewModel.kt | 2 -- app/src/main/java/app/revanced/manager/util/PM.kt | 9 +++------ 2 files changed, 3 insertions(+), 8 deletions(-) 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 2c79df31ed..fe886e9441 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 @@ -5,9 +5,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.pm.PackageInfo import android.content.pm.PackageInstaller -import android.content.pm.PackageManager import android.net.Uri import android.util.Log import androidx.compose.runtime.Stable 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 e42b0459bb..5736c71524 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,12 +116,8 @@ class PM( fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() - fun getVersionCode(packageInfo: PackageInfo): Int { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - packageInfo.longVersionCode.toInt() - } else { - packageInfo.versionCode - } + fun getVersionCode(packageInfo: PackageInfo): Long { + return PackageInfoCompat.getLongVersionCode(packageInfo) } suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { From 59f6903205ea90d6a9bfa6fa132f978812ff7039 Mon Sep 17 00:00:00 2001 From: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Date: Thu, 15 Aug 2024 08:51:52 -0700 Subject: [PATCH 23/28] Update app/src/main/java/app/revanced/manager/util/PM.kt Co-authored-by: Ax333l --- app/src/main/java/app/revanced/manager/util/PM.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 5736c71524..0d7a822b96 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -116,9 +116,7 @@ class PM( fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() - fun getVersionCode(packageInfo: PackageInfo): Long { - return PackageInfoCompat.getLongVersionCode(packageInfo) - } + fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo) suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { val packageInstaller = app.packageManager.packageInstaller From 87261b70b038bd6de88d4ac42ae05e1b29fde6c2 Mon Sep 17 00:00:00 2001 From: Benjamin Halko Date: Thu, 15 Aug 2024 09:00:45 -0700 Subject: [PATCH 24/28] Update PatcherViewModel.kt --- .../app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | 5 ----- 1 file changed, 5 deletions(-) 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 fe886e9441..6a10d49937 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 @@ -50,15 +50,10 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import java.io.ByteArrayInputStream import java.io.File import java.nio.file.Files -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.security.MessageDigest import java.time.Duration import java.util.UUID From 800b138044e0cae514cfe0be77c15e1fe1f7b49a Mon Sep 17 00:00:00 2001 From: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Date: Sat, 17 Aug 2024 23:13:49 -0700 Subject: [PATCH 25/28] Update app/src/main/res/values/strings.xml Co-authored-by: Ax333l --- 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 2fddcef31d..a22fe2b421 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -375,7 +375,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. Contact the developer of the app and ask for support. + The APK 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? From 679434914e1e24f7b2329ed949fbe3657ed2cf11 Mon Sep 17 00:00:00 2001 From: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Date: Wed, 21 Aug 2024 08:29:52 -0700 Subject: [PATCH 26/28] Update strings.xml --- 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 a22fe2b421..b885343cf2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -375,7 +375,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 APK is incompatible with this device. Use the correct APK for your device 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? From cea0c66e4c88b2b5360bbce1b4d99de2488e4921 Mon Sep 17 00:00:00 2001 From: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:49:57 -0700 Subject: [PATCH 27/28] Update app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt Co-authored-by: Ax333l --- .../java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 50457de691..3c89892a4e 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 @@ -278,7 +278,7 @@ class PatcherViewModel( ?: throw Exception("Failed to load application info") // If the app is currently installed - val existingPackageInfo = pm.getPackageInfo(packageName) + 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)) { From 5c4899f88a21e8179c911608956edfcd4890d89c Mon Sep 17 00:00:00 2001 From: Benjamin Date: Wed, 28 Aug 2024 09:34:15 -0700 Subject: [PATCH 28/28] Use output APK --- .../java/app/revanced/manager/ui/screen/PatcherScreen.kt | 3 +-- .../app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) 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 34d83bbeae..096bbf03de 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 @@ -62,7 +62,6 @@ fun PatcherScreen( val patcherSucceeded by vm.patcherSucceeded.observeAsState(null) val canInstall by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null || !vm.isInstalling) } } - val canSaveApk by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null) } } var showInstallPicker by rememberSaveable { mutableStateOf(false) } val steps by remember { @@ -108,7 +107,7 @@ fun PatcherScreen( actions = { IconButton( onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, - enabled = canSaveApk + enabled = patcherSucceeded == true ) { Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) } 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 3c89892a4e..ae7f95b961 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 @@ -305,10 +305,8 @@ class PatcherViewModel( try { // Check for base APK, first check if the app is already installed if (existingPackageInfo == null) { - val inputPackageInfo = inputFile?.let { pm.getPackageInfo(it) } - - // If the app is not installed, check if the input file is a base apk - if (inputPackageInfo == null || inputPackageInfo.splitNames != 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