mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-10 01:01:56 +01:00
feat: Add installer status dialog (#1473)
Co-authored-by: Benjamin Halko <benjaminhalko@hotmail.com> Co-authored-by: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Co-authored-by: Ushie <ushiekane@gmail.com> Co-authored-by: Ax333l <main@axelen.xyz>
This commit is contained in:
parent
2055400565
commit
d201bdc422
7 changed files with 303 additions and 17 deletions
|
@ -191,6 +191,10 @@ dependencies {
|
||||||
// Scrollbars
|
// Scrollbars
|
||||||
implementation(libs.scrollbars)
|
implementation(libs.scrollbars)
|
||||||
|
|
||||||
|
// EnumUtil
|
||||||
|
implementation(libs.enumutil)
|
||||||
|
ksp(libs.enumutil.ksp)
|
||||||
|
|
||||||
// Reorderable lists
|
// Reorderable lists
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppScaffold
|
import app.revanced.manager.ui.component.AppScaffold
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
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.InstallPickerDialog
|
||||||
import app.revanced.manager.ui.component.patcher.Steps
|
import app.revanced.manager.ui.component.patcher.Steps
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
|
@ -91,6 +92,9 @@ fun PatcherScreen(
|
||||||
onConfirm = vm::install
|
onConfirm = vm::install
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (vm.installerStatusDialogModel.packageInstallerStatus != null)
|
||||||
|
InstallerStatusDialog(vm.installerStatusDialogModel)
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
|
@ -103,7 +107,7 @@ fun PatcherScreen(
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
||||||
enabled = canInstall
|
enabled = patcherSucceeded == true
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
||||||
}
|
}
|
||||||
|
@ -172,4 +176,4 @@ fun PatcherScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ import app.revanced.manager.patcher.logger.LogLevel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||||
import app.revanced.manager.service.InstallService
|
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.destination.Destination
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
|
@ -66,6 +68,20 @@ class PatcherViewModel(
|
||||||
private val installedAppRepository: InstalledAppRepository by inject()
|
private val installedAppRepository: InstalledAppRepository by inject()
|
||||||
private val rootInstaller: RootInstaller by inject()
|
private val rootInstaller: RootInstaller by inject()
|
||||||
|
|
||||||
|
val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel {
|
||||||
|
override var packageInstallerStatus: Int? by mutableStateOf(null)
|
||||||
|
|
||||||
|
override fun reinstall() {
|
||||||
|
this@PatcherViewModel.reinstall()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun install() {
|
||||||
|
// Since this is a package installer status dialog,
|
||||||
|
// InstallType.ROOT is never used here.
|
||||||
|
install(InstallType.DEFAULT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var installedApp: InstalledApp? = null
|
private var installedApp: InstalledApp? = null
|
||||||
val packageName: String = input.selectedApp.packageName
|
val packageName: String = input.selectedApp.packageName
|
||||||
var installedPackageName by mutableStateOf<String?>(null)
|
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?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
InstallService.APP_INSTALL_ACTION -> {
|
InstallService.APP_INSTALL_ACTION -> {
|
||||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
val pmStatus = intent.getIntExtra(
|
||||||
val extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
InstallService.EXTRA_INSTALL_STATUS,
|
||||||
|
PackageInstaller.STATUS_FAILURE
|
||||||
|
)
|
||||||
|
|
||||||
|
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||||
|
?.let(logger::trace)
|
||||||
|
|
||||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
|
||||||
installedPackageName =
|
installedPackageName =
|
||||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -164,8 +184,24 @@ class PatcherViewModel(
|
||||||
input.selectedPatches
|
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.
|
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(
|
||||||
addAction(InstallService.APP_INSTALL_ACTION)
|
app,
|
||||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
installerBroadcastReceiver,
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(InstallService.APP_INSTALL_ACTION)
|
||||||
|
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||||
|
},
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
|
)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
installedApp = installedAppRepository.get(packageName)
|
installedApp = installedAppRepository.get(packageName)
|
||||||
|
@ -185,7 +227,7 @@ class PatcherViewModel(
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installBroadcastReceiver)
|
app.unregisterReceiver(installerBroadcastReceiver)
|
||||||
workManager.cancelWorkById(patcherWorkerId)
|
workManager.cancelWorkById(patcherWorkerId)
|
||||||
|
|
||||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) {
|
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) {
|
||||||
|
@ -228,20 +270,56 @@ class PatcherViewModel(
|
||||||
fun open() = installedPackageName?.let(pm::launch)
|
fun open() = installedPackageName?.let(pm::launch)
|
||||||
|
|
||||||
fun install(installType: InstallType) = viewModelScope.launch {
|
fun install(installType: InstallType) = viewModelScope.launch {
|
||||||
|
var pmInstallStarted = false
|
||||||
try {
|
try {
|
||||||
isInstalling = true
|
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) {
|
when (installType) {
|
||||||
InstallType.DEFAULT -> {
|
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))
|
pm.installApp(listOf(outputFile))
|
||||||
|
pmInstallStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
InstallType.ROOT -> {
|
InstallType.ROOT -> {
|
||||||
try {
|
try {
|
||||||
val label = with(pm) {
|
// Check for base APK, first check if the app is already installed
|
||||||
getPackageInfo(outputFile)?.label()
|
if (existingPackageInfo == null) {
|
||||||
?: throw Exception("Failed to load application info")
|
// 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(
|
rootInstaller.install(
|
||||||
outputFile,
|
outputFile,
|
||||||
inputFile,
|
inputFile,
|
||||||
|
@ -273,8 +351,22 @@ class PatcherViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch(e: Exception) {
|
||||||
|
Log.e(tag, "Failed to install", e)
|
||||||
|
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||||
} finally {
|
} finally {
|
||||||
isInstalling = false
|
if (!pmInstallStarted)
|
||||||
|
isInstalling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reinstall() = viewModelScope.launch {
|
||||||
|
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||||
|
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
|
||||||
|
pm.installApp(listOf(outputFile))
|
||||||
|
isInstalling = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.content.pm.PackageInstaller
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
@ -115,6 +116,8 @@ class PM(
|
||||||
|
|
||||||
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
|
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) {
|
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
val packageInstaller = app.packageManager.packageInstaller
|
||||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||||
|
@ -170,4 +173,4 @@ class PM(
|
||||||
Intent(this, UninstallService::class.java),
|
Intent(this, UninstallService::class.java),
|
||||||
intentFlags
|
intentFlags
|
||||||
).intentSender
|
).intentSender
|
||||||
}
|
}
|
||||||
|
|
|
@ -257,6 +257,7 @@
|
||||||
<string name="install_app">Install</string>
|
<string name="install_app">Install</string>
|
||||||
<string name="install_app_success">App installed</string>
|
<string name="install_app_success">App installed</string>
|
||||||
<string name="install_app_fail">Failed to install app: %s</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="uninstall_app_fail">Failed to uninstall app: %s</string>
|
||||||
<string name="open_app">Open</string>
|
<string name="open_app">Open</string>
|
||||||
<string name="save_apk">Save APK</string>
|
<string name="save_apk">Save APK</string>
|
||||||
|
@ -360,6 +361,24 @@
|
||||||
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string>
|
<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="remote_bundle_description">Import remote files from a URL, can automatically update</string>
|
||||||
<string name="recommended">Recommended</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 the correct APK for your 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="show">Show</string>
|
||||||
<string name="debugging">Debugging</string>
|
<string name="debugging">Debugging</string>
|
||||||
<string name="about_device">About device</string>
|
<string name="about_device">About device</string>
|
||||||
|
|
|
@ -32,6 +32,7 @@ app-icon-loader-coil = "1.5.0"
|
||||||
skrapeit = "1.2.2"
|
skrapeit = "1.2.2"
|
||||||
libsu = "5.2.2"
|
libsu = "5.2.2"
|
||||||
scrollbars = "1.0.4"
|
scrollbars = "1.0.4"
|
||||||
|
enumutil = "1.1.0"
|
||||||
compose-icons = "1.2.4"
|
compose-icons = "1.2.4"
|
||||||
kotlin-process = "1.4.1"
|
kotlin-process = "1.4.1"
|
||||||
hidden-api-stub = "4.3.3"
|
hidden-api-stub = "4.3.3"
|
||||||
|
@ -121,6 +122,10 @@ libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref =
|
||||||
# Scrollbars
|
# Scrollbars
|
||||||
scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", 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 lists
|
||||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue