Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add installer status dialog #1473

Merged
merged 36 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b1a821d
feat: Add installer status dialog
oSumAtrIX Jan 23, 2024
d147cd6
Adjust strings
oSumAtrIX Jan 24, 2024
8270999
Log extra status message
oSumAtrIX Jan 24, 2024
887794c
Merge branch 'compose-dev' into feat/installer-dialog
BenjaminHalko Jun 24, 2024
7462f5f
Merge branch 'compose-dev' into feat/installer-dialog
BenjaminHalko Jun 30, 2024
72fc6d2
Update PatcherViewModel.kt
BenjaminHalko Jul 6, 2024
63b5e5d
Merge remote-tracking branch 'origin/compose-dev' into feat/installer…
BenjaminHalko Jul 6, 2024
ffe5826
Show conflict dialogue when attempting to install a lower version
BenjaminHalko Jul 7, 2024
6f62bfb
Check for fingerprint differences and base APKs
BenjaminHalko Jul 7, 2024
cadc3da
Remove fingerprint detection
BenjaminHalko Jul 7, 2024
fcb2242
Clean up code
BenjaminHalko Jul 7, 2024
9527c9a
Merge remote-tracking branch 'origin/compose-dev' into feat/installer…
BenjaminHalko Jul 27, 2024
874f8ba
Updated detection of lower versions
BenjaminHalko Jul 27, 2024
0d23d7e
Update app/src/main/java/app/revanced/manager/ui/component/InstallerS…
BenjaminHalko Jul 28, 2024
3c4eaf5
Update app/src/main/res/values/strings.xml
BenjaminHalko Jul 28, 2024
2fa5461
Update app/src/main/res/values/strings.xml
BenjaminHalko Jul 28, 2024
4539584
Applied fixes
BenjaminHalko Jul 28, 2024
dc05099
Added missing semicolon
BenjaminHalko Jul 28, 2024
e48013a
Hide install button when installing
BenjaminHalko Jul 28, 2024
3f92371
Get version code the correct way
BenjaminHalko Jul 28, 2024
3c268b7
Merge remote-tracking branch 'origin/compose-dev' into feat/installer…
BenjaminHalko Aug 4, 2024
32d4681
Fixed some issues
BenjaminHalko Aug 4, 2024
982d3e6
Set `isInstalling` to false when a root error occurs
BenjaminHalko Aug 4, 2024
970c3fc
Update app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherVie…
BenjaminHalko Aug 6, 2024
46e6cc8
Merge branch 'compose-dev' into feat/installer-dialog
BenjaminHalko Aug 11, 2024
23c41ac
Add finally block
BenjaminHalko Aug 11, 2024
8554692
Update PatcherViewModel.kt
BenjaminHalko Aug 12, 2024
dc7b419
Update getting package info
BenjaminHalko Aug 12, 2024
59f6903
Update app/src/main/java/app/revanced/manager/util/PM.kt
BenjaminHalko Aug 15, 2024
87261b7
Update PatcherViewModel.kt
BenjaminHalko Aug 15, 2024
d709990
Merge branch 'compose-dev' into feat/installer-dialog
BenjaminHalko Aug 15, 2024
800b138
Update app/src/main/res/values/strings.xml
BenjaminHalko Aug 18, 2024
6794349
Update strings.xml
BenjaminHalko Aug 21, 2024
cea0c66
Update app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherVie…
BenjaminHalko Aug 28, 2024
c636949
Merge branch 'compose-dev' into feat/installer-dialog
BenjaminHalko Aug 28, 2024
5c4899f
Use output APK
BenjaminHalko Aug 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ dependencies {
// Scrollbars
implementation(libs.scrollbars)

// EnumUtil
implementation(libs.enumutil)
ksp(libs.enumutil.ksp)

// Reorderable lists
implementation(libs.reorderable)

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -91,6 +92,9 @@ fun PatcherScreen(
onConfirm = vm::install
)

if (vm.installerStatusDialogModel.packageInstallerStatus != null)
InstallerStatusDialog(vm.installerStatusDialogModel)

AppScaffold(
topBar = {
AppTopBar(
Expand All @@ -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))
}
Expand Down Expand Up @@ -172,4 +176,4 @@ fun PatcherScreen(
}
}
}
}
}
118 changes: 105 additions & 13 deletions app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
[email protected]()
}

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<String?>(null)
Expand Down Expand Up @@ -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 {
Expand All @@ -164,18 +184,40 @@ class PatcherViewModel(
input.selectedPatches
)
}
} else {
app.toast(app.getString(R.string.install_app_fail, extra))
}

installerStatusDialogModel.packageInstallerStatus = pmStatus
Axelen123 marked this conversation as resolved.
Show resolved Hide resolved

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
}
}
}
}
}

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)
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
// 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) {
getPackageInfo(outputFile)?.label()
?: throw Exception("Failed to load application info")
currentPackageInfo.label()
}

// Install as root
rootInstaller.install(
outputFile,
inputFile,
Expand Down Expand Up @@ -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 {
BenjaminHalko marked this conversation as resolved.
Show resolved Hide resolved
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
}
}

Expand Down
Loading