Skip to content

Commit

Permalink
feat: Add installer status dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
oSumAtrIX committed Jan 23, 2024
1 parent 607d8b6 commit 962e68d
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 12 deletions.
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,8 @@ dependencies {

// Scrollbars
implementation(libs.scrollbars)

// EnumUtil
implementation(libs.enumutil)
ksp(libs.enumutil.ksp)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
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 app.revanced.manager.ui.viewmodel.PackageInstallerResult
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,
contentStringResId = R.string.installation_failed_description,
),
FAILURE_ABORTED(
flag = PackageInstaller.STATUS_FAILURE_ABORTED,
title = R.string.installation_cancelled,
contentStringResId = R.string.installation_aborted_description,
),
FAILURE_BLOCKED(
flag = PackageInstaller.STATUS_FAILURE_BLOCKED,
title = R.string.installation_blocked,
contentStringResId = R.string.installation_blocked_description,
),
FAILURE_CONFLICT(
flag = PackageInstaller.STATUS_FAILURE_CONFLICT,
title = R.string.installation_conflict,
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,
contentStringResId = R.string.installation_incompatible_description,
),
FAILURE_INVALID(
flag = PackageInstaller.STATUS_FAILURE_INVALID,
title = R.string.installation_invalid,
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,
contentStringResId = R.string.installation_storage_issue_description,
),

@RequiresApi(34)
FAILURE_TIMEOUT(
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
title = R.string.installation_timeout,
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,
contentStringResId = R.string.installation_success_description,
icon = Icons.Outlined.Check,
);

// 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 @@ -89,6 +90,9 @@ fun PatcherScreen(
onConfirm = vm::install
)

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

AppScaffold(
topBar = {
AppTopBar(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<Int?>(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<String?>(null)
Expand Down Expand Up @@ -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)
}
}
)
Expand All @@ -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 {
Expand All @@ -144,18 +171,37 @@ 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
}
}
}
}
}

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 @@ -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) {
Expand Down Expand Up @@ -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) {
}
}
}
}
Expand All @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@
<string name="install_app">Install</string>
<string name="install_app_success">App installed</string>
<string name="install_app_fail">Failed to install app: %s</string>
<string name="reinstall_app_fail">Failed to reinstall app: %s</string>
<string name="uninstall_app_fail">Failed to uninstall app: %s</string>
<string name="open_app">Open</string>
<string name="save_apk">Save APK</string>
Expand Down Expand Up @@ -328,4 +329,25 @@
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string>
<string name="remote_bundle_description">Import remote files from a URL, can automatically update</string>
<string name="recommended">Recommended</string>

<string name="installation_failed">Installation failed</string>
<string name="installation_cancelled">Installation cancelled</string>
<string name="installation_blocked">Installation blocked</string>
<string name="installation_conflict">Installation conflict</string>
<string name="installation_incompatible">Installation incompatible</string>
<string name="installation_invalid">Installation invalid</string>
<string name="installation_storage_issue">Installation storage issue</string>
<string name="installation_timeout">Installation timeout</string>
<string name="installation_success">Installation succeeded</string>
<string name="installation_failed_description">The installation failed due to an unknown reason. Please try again.</string>
<string name="installation_aborted_description">The installation was cancelled manually.</string>
<string name="installation_blocked_description">The installation was blocked. Adjust your security settings and try again.</string>
<string name="installation_conflict_description">An existing installation of the app prevents the installation. Uninstall the app and try again?</string>
<string name="installation_incompatible_description">The app is incompatible with this device. Contact the developer of the app and ask for support.</string>
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string>
<string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string>
<string name="installation_timeout_description">The installation took too long.</string>
<string name="installation_success_description">The app has been installed successfully.</string>

<string name="reinstall">Reinstall</string>
</resources>
Loading

0 comments on commit 962e68d

Please sign in to comment.