mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-03 15:24:25 +02:00
🔮 Merge repository updated to latest snapshot!
Script Execution UTC Time: null Signed-off-by: validcube <pun.butrach@gmail.com>
This commit is contained in:
commit
8442bf2e14
@ -191,6 +191,10 @@ dependencies {
|
||||
// Scrollbars
|
||||
implementation(libs.scrollbars)
|
||||
|
||||
// EnumUtil
|
||||
implementation(libs.enumutil)
|
||||
ksp(libs.enumutil.ksp)
|
||||
|
||||
// Reorderable lists
|
||||
implementation(libs.reorderable)
|
||||
|
||||
|
@ -9,7 +9,7 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class InstallType(val stringResource: Int) {
|
||||
DEFAULT(R.string.default_install),
|
||||
ROOT(R.string.root_install)
|
||||
MOUNT(R.string.mount_install)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
@ -43,7 +43,7 @@ class RootInstaller(
|
||||
}
|
||||
}
|
||||
|
||||
return withTimeoutOrNull(Duration.ofSeconds(120L)) {
|
||||
return withTimeoutOrNull(Duration.ofSeconds(20L)) {
|
||||
remoteFS.await()
|
||||
} ?: throw RootServiceException()
|
||||
}
|
||||
@ -58,6 +58,10 @@ class RootInstaller(
|
||||
|
||||
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
|
||||
|
||||
fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path ->
|
||||
File(path, "su").canExecute()
|
||||
} ?: false
|
||||
|
||||
suspend fun isAppInstalled(packageName: String) =
|
||||
awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists()
|
||||
|
||||
|
@ -135,7 +135,7 @@ class PatcherWorker(
|
||||
return try {
|
||||
if (args.input is SelectedApp.Installed) {
|
||||
installedAppRepository.get(args.packageName)?.let {
|
||||
if (it.installType == InstallType.ROOT) {
|
||||
if (it.installType == InstallType.MOUNT) {
|
||||
rootInstaller.unmount(args.packageName)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -1,33 +1,35 @@
|
||||
package app.revanced.manager.ui.component.bundle
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Lightbulb
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.component.ArrowButton
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -35,7 +37,8 @@ fun BundlePatchesDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
bundle: PatchBundleSource,
|
||||
) {
|
||||
var informationCardVisible by remember { mutableStateOf(true) }
|
||||
var showAllVersions by rememberSaveable { mutableStateOf(false) }
|
||||
var showOptions by rememberSaveable { mutableStateOf(false) }
|
||||
val state by bundle.state.collectAsStateWithLifecycle()
|
||||
|
||||
Dialog(
|
||||
@ -62,44 +65,212 @@ fun BundlePatchesDialog(
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.padding(paddingValues),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
item {
|
||||
AnimatedVisibility(visible = informationCardVisible) {
|
||||
NotificationCard(
|
||||
icon = Icons.Outlined.Lightbulb,
|
||||
text = stringResource(R.string.tap_on_patches),
|
||||
onDismiss = { informationCardVisible = false }
|
||||
state.patchBundleOrNull()?.let { bundle ->
|
||||
items(bundle.patches) { patch ->
|
||||
PatchItem(
|
||||
patch,
|
||||
showAllVersions,
|
||||
onExpandVersions = { showAllVersions = !showAllVersions },
|
||||
showOptions,
|
||||
onExpandOptions = { showOptions = !showOptions }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.patchBundleOrNull()?.let { bundle ->
|
||||
items(bundle.patches.size) { bundleIndex ->
|
||||
val patch = bundle.patches[bundleIndex]
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun PatchItem(
|
||||
patch: PatchInfo,
|
||||
expandVersions: Boolean,
|
||||
onExpandVersions: () -> Unit,
|
||||
expandOptions: Boolean,
|
||||
onExpandOptions: () -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (patch.options.isNullOrEmpty()) Modifier else Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(onClick = onExpandOptions),
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Absolute.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = patch.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
|
||||
if (!patch.options.isNullOrEmpty()) {
|
||||
ArrowButton(expanded = expandOptions, onClick = null)
|
||||
}
|
||||
}
|
||||
patch.description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (patch.compatiblePackages.isNullOrEmpty()) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PatchInfoChip(
|
||||
text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}"
|
||||
)
|
||||
PatchInfoChip(
|
||||
text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
patch.compatiblePackages.forEach { compatiblePackage ->
|
||||
val packageName = compatiblePackage.packageName
|
||||
val versions = compatiblePackage.versions.orEmpty().reversed()
|
||||
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
PatchInfoChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "$PACKAGE_ICON $packageName"
|
||||
)
|
||||
|
||||
if (versions.isNotEmpty()) {
|
||||
if (expandVersions) {
|
||||
versions.forEach { version ->
|
||||
PatchInfoChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "$VERSION_ICON $version"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
PatchInfoChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "$VERSION_ICON ${versions.first()}"
|
||||
)
|
||||
}
|
||||
if (versions.size > 1) {
|
||||
PatchInfoChip(
|
||||
onClick = onExpandVersions,
|
||||
text = if (expandVersions) stringResource(R.string.less) else "+${versions.size - 1}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!patch.options.isNullOrEmpty()) {
|
||||
AnimatedVisibility(visible = expandOptions) {
|
||||
val options = patch.options
|
||||
|
||||
Column {
|
||||
options.forEachIndexed { i, option ->
|
||||
OutlinedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurface
|
||||
), shape = when {
|
||||
options.size == 1 -> RoundedCornerShape(8.dp)
|
||||
i == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)
|
||||
i == options.lastIndex -> RoundedCornerShape(
|
||||
bottomStart = 8.dp,
|
||||
bottomEnd = 8.dp
|
||||
)
|
||||
|
||||
else -> RoundedCornerShape(0.dp)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = option.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = option.description,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PatchInfoChip(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
text: String
|
||||
) {
|
||||
val shape = RoundedCornerShape(8.0.dp)
|
||||
val cardModifier = if (onClick != null) {
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.clickable(onClick = onClick)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
modifier = modifier.then(cardModifier),
|
||||
colors = CardColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
shape = shape,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.20f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val PACKAGE_ICON = "\uD83D\uDCE6"
|
||||
const val VERSION_ICON = "\uD83C\uDFAF"
|
@ -7,6 +7,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -27,7 +28,7 @@ fun InstallPickerDialog(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
dismissButton = {
|
||||
Button(onClick = onDismiss) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
|
@ -81,7 +81,7 @@ fun InstalledAppInfoScreen(
|
||||
AppInfo(viewModel.appInfo) {
|
||||
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||
|
||||
if (viewModel.installedApp.installType == InstallType.ROOT) {
|
||||
if (viewModel.installedApp.installType == InstallType.MOUNT) {
|
||||
Text(
|
||||
text = if (viewModel.isMounted) {
|
||||
stringResource(R.string.mounted)
|
||||
@ -112,7 +112,7 @@ fun InstalledAppInfoScreen(
|
||||
onClick = viewModel::uninstall
|
||||
)
|
||||
|
||||
InstallType.ROOT -> {
|
||||
InstallType.MOUNT -> {
|
||||
SegmentedButton(
|
||||
icon = Icons.Outlined.SettingsBackupRestore,
|
||||
text = stringResource(R.string.unpatch),
|
||||
@ -138,7 +138,7 @@ fun InstalledAppInfoScreen(
|
||||
onPatchClick(viewModel.installedApp.originalPackageName, it)
|
||||
}
|
||||
},
|
||||
enabled = viewModel.installedApp.installType != InstallType.ROOT || viewModel.rootInstaller.hasRootAccess()
|
||||
enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -38,8 +38,10 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.ui.component.AppScaffold
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.InstallerStatusDialog
|
||||
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
||||
import app.revanced.manager.ui.component.patcher.Steps
|
||||
import app.revanced.manager.ui.model.State
|
||||
@ -91,6 +93,9 @@ fun PatcherScreen(
|
||||
onConfirm = vm::install
|
||||
)
|
||||
|
||||
if (vm.installerStatusDialogModel.packageInstallerStatus != null)
|
||||
InstallerStatusDialog(vm.installerStatusDialogModel)
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
@ -103,7 +108,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))
|
||||
}
|
||||
@ -135,7 +140,8 @@ fun PatcherScreen(
|
||||
},
|
||||
onClick = {
|
||||
if (vm.installedPackageName == null)
|
||||
showInstallPicker = true
|
||||
if (vm.isDeviceRooted()) showInstallPicker = true
|
||||
else vm.install(InstallType.DEFAULT)
|
||||
else vm.open()
|
||||
}
|
||||
)
|
||||
|
@ -113,8 +113,8 @@ fun VersionSelectorScreen(
|
||||
onClick = { viewModel.select(it) },
|
||||
patchCount = supportedVersions[it.version],
|
||||
enabled =
|
||||
!(installedApp?.installType == InstallType.ROOT && !viewModel.rootInstaller.hasRootAccess()),
|
||||
alreadyPatched = installedApp != null && installedApp.installType != InstallType.ROOT
|
||||
!(installedApp?.installType == InstallType.MOUNT && !viewModel.rootInstaller.hasRootAccess()),
|
||||
alreadyPatched = installedApp != null && installedApp.installType != InstallType.MOUNT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ class InstalledAppInfoViewModel(
|
||||
when (installedApp.installType) {
|
||||
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
|
||||
|
||||
InstallType.ROOT -> viewModelScope.launch {
|
||||
InstallType.MOUNT -> viewModelScope.launch {
|
||||
rootInstaller.uninstall(installedApp.currentPackageName)
|
||||
installedAppRepository.delete(installedApp)
|
||||
onBackClick()
|
||||
|
@ -30,7 +30,7 @@ class InstalledAppsViewModel(
|
||||
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (
|
||||
installedApp.installType == InstallType.ROOT && !rootInstaller.isAppInstalled(installedApp.currentPackageName)
|
||||
installedApp.installType == InstallType.MOUNT && !rootInstaller.isAppInstalled(installedApp.currentPackageName)
|
||||
) {
|
||||
installedAppsRepository.delete(installedApp)
|
||||
return@withContext null
|
||||
@ -39,7 +39,7 @@ class InstalledAppsViewModel(
|
||||
|
||||
val packageInfo = pm.getPackageInfo(installedApp.currentPackageName)
|
||||
|
||||
if (packageInfo == null && installedApp.installType != InstallType.ROOT) {
|
||||
if (packageInfo == null && installedApp.installType != InstallType.MOUNT) {
|
||||
installedAppsRepository.delete(installedApp)
|
||||
return@withContext null
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ import app.revanced.manager.patcher.logger.LogLevel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.ui.component.InstallerStatusDialogModel
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
@ -66,6 +68,20 @@ class PatcherViewModel(
|
||||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
|
||||
val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel {
|
||||
override var packageInstallerStatus: Int? by mutableStateOf(null)
|
||||
|
||||
override fun reinstall() {
|
||||
this@PatcherViewModel.reinstall()
|
||||
}
|
||||
|
||||
override fun install() {
|
||||
// Since this is a package installer status dialog,
|
||||
// InstallType.MOUNT is never used here.
|
||||
install(InstallType.DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
val packageName: String = input.selectedApp.packageName
|
||||
var installedPackageName by mutableStateOf<String?>(null)
|
||||
@ -144,15 +160,19 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
||||
private val installerBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
InstallService.APP_INSTALL_ACTION -> {
|
||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
||||
val extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
||||
val pmStatus = intent.getIntExtra(
|
||||
InstallService.EXTRA_INSTALL_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE
|
||||
)
|
||||
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
?.let(logger::trace)
|
||||
|
||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
installedPackageName =
|
||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||
viewModelScope.launch {
|
||||
@ -164,8 +184,24 @@ class PatcherViewModel(
|
||||
input.selectedPatches
|
||||
)
|
||||
}
|
||||
} else {
|
||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
||||
}
|
||||
|
||||
installerStatusDialogModel.packageInstallerStatus = pmStatus
|
||||
|
||||
isInstalling = false
|
||||
}
|
||||
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
val pmStatus = intent.getIntExtra(
|
||||
UninstallService.EXTRA_UNINSTALL_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE
|
||||
)
|
||||
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
?.let(logger::trace)
|
||||
|
||||
if (pmStatus != PackageInstaller.STATUS_SUCCESS) {
|
||||
installerStatusDialogModel.packageInstallerStatus = pmStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -173,9 +209,15 @@ class PatcherViewModel(
|
||||
}
|
||||
|
||||
init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it.
|
||||
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
||||
ContextCompat.registerReceiver(
|
||||
app,
|
||||
installerBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||
},
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
installedApp = installedAppRepository.get(packageName)
|
||||
@ -185,10 +227,10 @@ 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) {
|
||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
|
||||
withTimeout(Duration.ofMinutes(1L)) {
|
||||
@ -201,6 +243,8 @@ class PatcherViewModel(
|
||||
tempDir.deleteRecursively()
|
||||
}
|
||||
|
||||
fun isDeviceRooted() = rootInstaller.isDeviceRooted()
|
||||
|
||||
fun export(uri: Uri?) = viewModelScope.launch {
|
||||
uri?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
@ -228,20 +272,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 -> {
|
||||
InstallType.MOUNT -> {
|
||||
try {
|
||||
val label = with(pm) {
|
||||
getPackageInfo(outputFile)?.label()
|
||||
?: throw Exception("Failed to load application info")
|
||||
// Check for base APK, first check if the app is already installed
|
||||
if (existingPackageInfo == null) {
|
||||
// If the app is not installed, check if the output file is a base apk
|
||||
if (currentPackageInfo.splitNames != null) {
|
||||
// Exit if there is no base APK package
|
||||
installerStatusDialogModel.packageInstallerStatus =
|
||||
PackageInstaller.STATUS_FAILURE_INVALID
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
// Get label
|
||||
val label = with(pm) {
|
||||
currentPackageInfo.label()
|
||||
}
|
||||
|
||||
// Install as root
|
||||
rootInstaller.install(
|
||||
outputFile,
|
||||
inputFile,
|
||||
@ -254,7 +334,7 @@ class PatcherViewModel(
|
||||
packageName,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
InstallType.ROOT,
|
||||
InstallType.MOUNT,
|
||||
input.selectedPatches
|
||||
)
|
||||
|
||||
@ -273,11 +353,25 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Log.e(tag, "Failed to install", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
} finally {
|
||||
if (!pmInstallStarted)
|
||||
isInstalling = false
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ReVanced Patcher"
|
||||
|
||||
|
@ -10,6 +10,7 @@ import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
@ -115,6 +116,8 @@ class PM(
|
||||
|
||||
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
|
||||
|
||||
fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo)
|
||||
|
||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
||||
val packageInstaller = app.packageManager.packageInstaller
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||
|
@ -223,7 +223,7 @@
|
||||
<string name="applied_patches">Applied patches</string>
|
||||
<string name="view_applied_patches">View applied patches</string>
|
||||
<string name="default_install">Default</string>
|
||||
<string name="root_install">Root</string>
|
||||
<string name="mount_install">Mount</string>
|
||||
<string name="mounted">Mounted</string>
|
||||
<string name="not_mounted">Not mounted</string>
|
||||
<string name="mount">Mount</string>
|
||||
@ -257,6 +257,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>
|
||||
@ -287,6 +288,7 @@
|
||||
<string name="drag_handle">reorder</string>
|
||||
|
||||
<string name="more">More</string>
|
||||
<string name="less">Less</string>
|
||||
<string name="continue_">Continue</string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="permanent_dismiss">Do not show this again</string>
|
||||
@ -305,6 +307,9 @@
|
||||
<string name="bundle_auto_update">Auto update</string>
|
||||
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string>
|
||||
<string name="bundle_view_patches">View patches</string>
|
||||
<string name="bundle_view_patches_any_version">Any version</string>
|
||||
<string name="bundle_view_patches_any_package">Any package</string>
|
||||
|
||||
<string name="about_revanced_manager">About ReVanced Manager</string>
|
||||
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
|
||||
<string name="update_available">An update is available</string>
|
||||
@ -356,6 +361,24 @@
|
||||
<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_dialog_title">Installation failed</string>
|
||||
<string name="installation_cancelled_dialog_title">Installation cancelled</string>
|
||||
<string name="installation_blocked_dialog_title">Installation blocked</string>
|
||||
<string name="installation_conflict_dialog_title">Installation conflict</string>
|
||||
<string name="installation_incompatible_dialog_title">Installation incompatible</string>
|
||||
<string name="installation_invalid_dialog_title">Installation invalid</string>
|
||||
<string name="installation_storage_issue_dialog_title">Not enough storage</string>
|
||||
<string name="installation_timeout_dialog_title">Installation timed out</string>
|
||||
<string name="installation_failed_description">The installation failed due to an unknown reason. Try again?</string>
|
||||
<string name="installation_aborted_description">The installation was cancelled manually. Try again?</string>
|
||||
<string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string>
|
||||
<string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string>
|
||||
<string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is supported by this device and try again.</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. Try again?</string>
|
||||
<string name="reinstall">Reinstall</string>
|
||||
<string name="show">Show</string>
|
||||
<string name="debugging">Debugging</string>
|
||||
<string name="about_device">About device</string>
|
||||
|
@ -32,6 +32,7 @@ app-icon-loader-coil = "1.5.0"
|
||||
skrapeit = "1.2.2"
|
||||
libsu = "5.2.2"
|
||||
scrollbars = "1.0.4"
|
||||
enumutil = "1.1.0"
|
||||
compose-icons = "1.2.4"
|
||||
kotlin-process = "1.4.1"
|
||||
hidden-api-stub = "4.3.3"
|
||||
@ -121,6 +122,10 @@ libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref =
|
||||
# Scrollbars
|
||||
scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", version.ref = "scrollbars" }
|
||||
|
||||
# EnumUtil
|
||||
enumutil = { group = "io.github.materiiapps", name = "enumutil", version.ref = "enumutil" }
|
||||
enumutil-ksp = { group = "io.github.materiiapps", name = "enumutil-ksp", version.ref = "enumutil" }
|
||||
|
||||
# Reorderable lists
|
||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user