mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-30 05:54:26 +02:00
fix: process death resilience and account for android 11 bug (#2355)
This commit is contained in:
parent
9916e4da4d
commit
49f75f9edd
@ -1,10 +1,15 @@
|
||||
package app.revanced.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.di.*
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
@ -25,6 +30,7 @@ class ManagerApplication : Application() {
|
||||
private val prefs: PreferencesManager by inject()
|
||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@ -71,5 +77,34 @@ class ManagerApplication : Application() {
|
||||
updateCheck()
|
||||
}
|
||||
}
|
||||
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||
private var firstActivityCreated = false
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
if (firstActivityCreated) return
|
||||
firstActivityCreated = true
|
||||
|
||||
// We do not want to call onFreshProcessStart() if there is state to restore.
|
||||
// This can happen on system-initiated process death.
|
||||
if (savedInstanceState == null) {
|
||||
Log.d(tag, "Fresh process created")
|
||||
onFreshProcessStart()
|
||||
} else Log.d(tag, "System-initiated process death detected")
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onFreshProcessStart() {
|
||||
fs.uiTempDir.apply {
|
||||
deleteRecursively()
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ import android.os.Environment
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import app.revanced.manager.util.RequestManageStorageContract
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
class Filesystem(private val app: Application) {
|
||||
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
|
||||
@ -17,21 +19,33 @@ class Filesystem(private val app: Application) {
|
||||
* A directory that gets cleared when the app restarts.
|
||||
* Do not store paths to this directory in a parcel.
|
||||
*/
|
||||
val tempDir = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
|
||||
val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
|
||||
deleteRecursively()
|
||||
mkdirs()
|
||||
}
|
||||
|
||||
fun externalFilesDir() = Environment.getExternalStorageDirectory().toPath()
|
||||
/**
|
||||
* A directory for storing temporary files related to UI.
|
||||
* This is the same as [tempDir], but does not get cleared on system-initiated process death.
|
||||
* Paths to this directory can be safely stored in parcels.
|
||||
*/
|
||||
val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE)
|
||||
|
||||
fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath()
|
||||
|
||||
private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
|
||||
private val storagePermissionName = if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
private val storagePermissionName =
|
||||
if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
|
||||
fun permissionContract(): Pair<ActivityResultContract<String, Boolean>, String> {
|
||||
val contract = if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
|
||||
val contract =
|
||||
if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
|
||||
return contract to storagePermissionName
|
||||
}
|
||||
|
||||
fun hasStoragePermission() = if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(storagePermissionName) == PackageManager.PERMISSION_GRANTED
|
||||
fun hasStoragePermission() =
|
||||
if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(
|
||||
storagePermissionName
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
@ -25,7 +25,7 @@ class Session(
|
||||
private val androidContext: Context,
|
||||
private val logger: Logger,
|
||||
private val input: File,
|
||||
private val onPatchCompleted: () -> Unit,
|
||||
private val onPatchCompleted: suspend () -> Unit,
|
||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||
) : Closeable {
|
||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
|
@ -20,7 +20,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: () -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) {
|
||||
val bundles = bundles()
|
||||
|
@ -66,7 +66,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: () -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) = coroutineScope {
|
||||
// Get the location of our own Apk.
|
||||
@ -123,7 +123,9 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
val eventHandler = object : IPatcherEvents.Stub() {
|
||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||
|
||||
override fun patchSucceeded() = onPatchCompleted()
|
||||
override fun patchSucceeded() {
|
||||
launch { onPatchCompleted() }
|
||||
}
|
||||
|
||||
override fun progress(name: String?, state: String?, msg: String?) =
|
||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
||||
|
@ -34,7 +34,7 @@ sealed class Runtime(context: Context) : KoinComponent {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: () -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
)
|
||||
}
|
@ -42,9 +42,7 @@ import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
@ -73,10 +71,10 @@ class PatcherWorker(
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: Options,
|
||||
val logger: Logger,
|
||||
val downloadProgress: MutableStateFlow<Pair<Long, Long?>?>,
|
||||
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
||||
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
||||
val onPatchCompleted: suspend () -> Unit,
|
||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||
val setInputFile: (File) -> Unit,
|
||||
val setInputFile: suspend (File) -> Unit,
|
||||
val onProgress: ProgressEventHandler
|
||||
) {
|
||||
val packageName get() = input.packageName
|
||||
@ -160,7 +158,7 @@ class PatcherWorker(
|
||||
data,
|
||||
args.packageName,
|
||||
args.input.version,
|
||||
onDownload = args.downloadProgress::emit
|
||||
onDownload = args.onDownloadProgress
|
||||
).also {
|
||||
args.setInputFile(it)
|
||||
updateProgress(state = State.COMPLETED) // Download APK
|
||||
@ -224,11 +222,7 @@ class PatcherWorker(
|
||||
args.selectedPatches,
|
||||
args.options,
|
||||
args.logger,
|
||||
onPatchCompleted = {
|
||||
args.patchesProgress.update { (completed, total) ->
|
||||
completed + 1 to total
|
||||
}
|
||||
},
|
||||
args.onPatchCompleted,
|
||||
args.onProgress
|
||||
)
|
||||
|
||||
|
@ -6,7 +6,6 @@ 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
|
||||
@ -21,35 +20,25 @@ 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.model.InstallerModel
|
||||
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?
|
||||
}
|
||||
private typealias InstallerStatusDialogButton = @Composable (model: InstallerModel, dismiss: () -> Unit) -> Unit
|
||||
|
||||
@Composable
|
||||
fun InstallerStatusDialog(model: InstallerStatusDialogModel) {
|
||||
fun InstallerStatusDialog(installerStatus: Int, model: InstallerModel, onDismiss: () -> Unit) {
|
||||
val dialogKind = remember {
|
||||
DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE
|
||||
DialogKind.fromValue(installerStatus) ?: DialogKind.FAILURE
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
model.packageInstallerStatus = null
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
dialogKind.confirmButton(model)
|
||||
dialogKind.confirmButton(model, onDismiss)
|
||||
},
|
||||
dismissButton = {
|
||||
dialogKind.dismissButton?.invoke(model)
|
||||
dialogKind.dismissButton?.invoke(model, onDismiss)
|
||||
},
|
||||
icon = {
|
||||
Icon(dialogKind.icon, null)
|
||||
@ -75,10 +64,10 @@ fun InstallerStatusDialog(model: InstallerStatusDialogModel) {
|
||||
private fun installerStatusDialogButton(
|
||||
@StringRes buttonStringResId: Int,
|
||||
buttonHandler: InstallerStatusDialogButtonHandler = { },
|
||||
): InstallerStatusDialogButton = { model ->
|
||||
): InstallerStatusDialogButton = { model, dismiss ->
|
||||
TextButton(
|
||||
onClick = {
|
||||
model.packageInstallerStatus = null
|
||||
dismiss()
|
||||
buttonHandler(model)
|
||||
}
|
||||
) {
|
||||
@ -154,6 +143,7 @@ enum class DialogKind(
|
||||
model.install()
|
||||
},
|
||||
);
|
||||
|
||||
// Needed due to the @FromValue annotation.
|
||||
companion object
|
||||
}
|
||||
|
@ -36,13 +36,14 @@ import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.ArrowButton
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.model.ProgressKey
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.StepProgressProvider
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
|
||||
@ -52,6 +53,7 @@ fun Steps(
|
||||
category: StepCategory,
|
||||
steps: List<Step>,
|
||||
stepCount: Pair<Int, Int>? = null,
|
||||
stepProgressProvider: StepProgressProvider
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
@ -116,13 +118,20 @@ fun Steps(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
steps.forEach { step ->
|
||||
val downloadProgress = step.downloadProgress?.collectAsStateWithLifecycle()
|
||||
val (progress, progressText) = when (step.progressKey) {
|
||||
null -> null
|
||||
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
|
||||
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
|
||||
else null to "${downloaded.megaBytes} MB"
|
||||
}
|
||||
} ?: (null to null)
|
||||
|
||||
SubStep(
|
||||
name = step.name,
|
||||
state = step.state,
|
||||
message = step.message,
|
||||
downloadProgress = downloadProgress?.value
|
||||
progress = progress,
|
||||
progressText = progressText
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -135,7 +144,8 @@ fun SubStep(
|
||||
name: String,
|
||||
state: State,
|
||||
message: String? = null,
|
||||
downloadProgress: Pair<Long, Long?>? = null
|
||||
progress: Float? = null,
|
||||
progressText: String? = null
|
||||
) {
|
||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
@ -156,7 +166,7 @@ fun SubStep(
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
StepIcon(state, downloadProgress, size = 20.dp)
|
||||
StepIcon(state, progress, size = 20.dp)
|
||||
}
|
||||
|
||||
Text(
|
||||
@ -167,8 +177,8 @@ fun SubStep(
|
||||
modifier = Modifier.weight(1f, true),
|
||||
)
|
||||
|
||||
if (message != null) {
|
||||
Box(
|
||||
when {
|
||||
message != null -> Box(
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@ -178,13 +188,11 @@ fun SubStep(
|
||||
onClick = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
downloadProgress?.let { (current, total) ->
|
||||
Text(
|
||||
if (total != null) "${current.megaBytes}/${total.megaBytes} MB" else "${current.megaBytes} MB",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
|
||||
progressText != null -> Text(
|
||||
progressText,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,7 +208,7 @@ fun SubStep(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StepIcon(state: State, progress: Pair<Long, Long?>? = null, size: Dp) {
|
||||
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
|
||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||
|
||||
when (state) {
|
||||
@ -234,12 +242,7 @@ fun StepIcon(state: State, progress: Pair<Long, Long?>? = null, size: Dp) {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
progress = {
|
||||
progress?.let { (current, total) ->
|
||||
if (total == null) return@let null
|
||||
current / total
|
||||
}?.toFloat()
|
||||
},
|
||||
progress = { progress },
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
interface InstallerModel {
|
||||
fun reinstall()
|
||||
fun install()
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import app.revanced.manager.R
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class StepCategory(@StringRes val displayName: Int) {
|
||||
PREPARING(R.string.patcher_step_group_preparing),
|
||||
@ -14,10 +15,19 @@ enum class State {
|
||||
WAITING, RUNNING, FAILED, COMPLETED
|
||||
}
|
||||
|
||||
enum class ProgressKey {
|
||||
DOWNLOAD
|
||||
}
|
||||
|
||||
interface StepProgressProvider {
|
||||
val downloadProgress: Pair<Long, Long?>?
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Step(
|
||||
val name: String,
|
||||
val category: StepCategory,
|
||||
val state: State = State.WAITING,
|
||||
val message: String? = null,
|
||||
val downloadProgress: StateFlow<Pair<Long, Long?>?>? = null
|
||||
)
|
||||
val progressKey: ProgressKey? = null
|
||||
) : Parcelable
|
@ -5,6 +5,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
@ -27,6 +28,7 @@ import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||
import app.revanced.manager.ui.component.AvailableUpdateDialog
|
||||
@ -36,6 +38,7 @@ import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
||||
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
||||
import app.revanced.manager.util.RequestInstallAppsContract
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
@ -93,20 +96,36 @@ fun DashboardScreen(
|
||||
)
|
||||
}
|
||||
|
||||
var showDialog by rememberSaveable { mutableStateOf(vm.prefs.showManagerUpdateDialogOnLaunch.getBlocking()) }
|
||||
var showUpdateDialog by rememberSaveable { mutableStateOf(vm.prefs.showManagerUpdateDialogOnLaunch.getBlocking()) }
|
||||
val availableUpdate by remember {
|
||||
derivedStateOf { vm.updatedManagerVersion.takeIf { showDialog } }
|
||||
derivedStateOf { vm.updatedManagerVersion.takeIf { showUpdateDialog } }
|
||||
}
|
||||
|
||||
availableUpdate?.let { version ->
|
||||
AvailableUpdateDialog(
|
||||
onDismiss = { showDialog = false },
|
||||
onDismiss = { showUpdateDialog = false },
|
||||
setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch,
|
||||
onConfirm = onUpdateClick,
|
||||
newVersion = version
|
||||
)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
var showAndroid11Dialog by rememberSaveable { mutableStateOf(false) }
|
||||
val installAppsPermissionLauncher =
|
||||
rememberLauncherForActivityResult(RequestInstallAppsContract) { granted ->
|
||||
showAndroid11Dialog = false
|
||||
if (granted) onAppSelectorClick()
|
||||
}
|
||||
if (showAndroid11Dialog) Android11Dialog(
|
||||
onDismissRequest = {
|
||||
showAndroid11Dialog = false
|
||||
},
|
||||
onContinue = {
|
||||
installAppsPermissionLauncher.launch(context.packageName)
|
||||
}
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (bundlesSelectable) {
|
||||
@ -188,6 +207,10 @@ fun DashboardScreen(
|
||||
}
|
||||
return@HapticFloatingActionButton
|
||||
}
|
||||
if (vm.android11BugActive) {
|
||||
showAndroid11Dialog = true
|
||||
return@HapticFloatingActionButton
|
||||
}
|
||||
|
||||
onAppSelectorClick()
|
||||
}
|
||||
@ -317,3 +340,24 @@ fun Notifications(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Android11Dialog(onDismissRequest: () -> Unit, onContinue: () -> Unit) {
|
||||
AlertDialogExtended(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onContinue) {
|
||||
Text(stringResource(R.string.continue_))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.android_11_bug_dialog_title))
|
||||
},
|
||||
icon = {
|
||||
Icon(Icons.Outlined.BugReport, null)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.android_11_bug_dialog_description))
|
||||
}
|
||||
)
|
||||
}
|
@ -29,7 +29,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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
|
||||
@ -38,7 +37,6 @@ import app.revanced.manager.ui.component.InstallerStatusDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
||||
import app.revanced.manager.ui.component.patcher.Steps
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.viewmodel.PatcherViewModel
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
@ -50,7 +48,11 @@ fun PatcherScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: PatcherViewModel
|
||||
) {
|
||||
BackHandler(onBack = onBackClick)
|
||||
fun leaveScreen() {
|
||||
vm.onBack()
|
||||
onBackClick()
|
||||
}
|
||||
BackHandler(onBack = ::leaveScreen)
|
||||
|
||||
val context = LocalContext.current
|
||||
val exportApkLauncher =
|
||||
@ -66,22 +68,6 @@ fun PatcherScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val patchesProgress by vm.patchesProgress.collectAsStateWithLifecycle()
|
||||
|
||||
val progress by remember {
|
||||
derivedStateOf {
|
||||
val (patchesCompleted, patchesTotal) = patchesProgress
|
||||
|
||||
val current = vm.steps.count {
|
||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
||||
} + patchesCompleted
|
||||
|
||||
val total = vm.steps.size - 1 + patchesTotal
|
||||
|
||||
current.toFloat() / total.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
if (patcherSucceeded == null) {
|
||||
DisposableEffect(Unit) {
|
||||
val window = (context as Activity).window
|
||||
@ -98,8 +84,9 @@ fun PatcherScreen(
|
||||
onConfirm = vm::install
|
||||
)
|
||||
|
||||
if (vm.installerStatusDialogModel.packageInstallerStatus != null)
|
||||
InstallerStatusDialog(vm.installerStatusDialogModel)
|
||||
vm.packageInstallerStatus?.let {
|
||||
InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog)
|
||||
}
|
||||
|
||||
val activityLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
@ -137,7 +124,7 @@ fun PatcherScreen(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.patcher),
|
||||
onBackClick = onBackClick
|
||||
onBackClick = ::leaveScreen
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@ -193,7 +180,7 @@ fun PatcherScreen(
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progress },
|
||||
progress = { vm.progress },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
@ -209,7 +196,8 @@ fun PatcherScreen(
|
||||
Steps(
|
||||
category = category,
|
||||
steps = steps,
|
||||
stepCount = if (category == StepCategory.PATCHING) patchesProgress else null
|
||||
stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null,
|
||||
stepProgressProvider = vm
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,13 @@ import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.PM
|
||||
@ -22,13 +26,19 @@ import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class AppSelectorViewModel(
|
||||
private val app: Application,
|
||||
private val pm: PM,
|
||||
private val patchBundleRepository: PatchBundleRepository
|
||||
fs: Filesystem,
|
||||
private val patchBundleRepository: PatchBundleRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
private val inputFile = File(app.filesDir, "input.apk").also {
|
||||
it.delete()
|
||||
private val inputFile = savedStateHandle.saveable(key = "inputFile") {
|
||||
File(
|
||||
fs.uiTempDir,
|
||||
"input.apk"
|
||||
).also(File::delete)
|
||||
}
|
||||
val appList = pm.appList
|
||||
|
||||
|
@ -3,6 +3,7 @@ package app.revanced.manager.ui.viewmodel
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
@ -20,6 +21,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.flow.first
|
||||
@ -32,7 +34,8 @@ class DashboardViewModel(
|
||||
private val downloaderPluginRepository: DownloaderPluginRepository,
|
||||
private val reVancedAPI: ReVancedAPI,
|
||||
private val networkInfo: NetworkInfo,
|
||||
val prefs: PreferencesManager
|
||||
val prefs: PreferencesManager,
|
||||
private val pm: PM,
|
||||
) : ViewModel() {
|
||||
val availablePatches =
|
||||
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } }
|
||||
@ -43,6 +46,14 @@ class DashboardViewModel(
|
||||
|
||||
val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
|
||||
|
||||
/**
|
||||
* Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen.
|
||||
* This value is true when the conditions that trigger the bug are met.
|
||||
*
|
||||
* See: https://github.com/ReVanced/revanced-manager/issues/2138
|
||||
*/
|
||||
val android11BugActive get() = Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !pm.canInstallPackages()
|
||||
|
||||
var updatedManagerVersion: String? by mutableStateOf(null)
|
||||
private set
|
||||
var showBatteryOptimizationsWarning by mutableStateOf(false)
|
||||
|
@ -7,18 +7,22 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.net.Uri
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.autoSaver
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import app.revanced.manager.R
|
||||
@ -35,13 +39,17 @@ import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
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.InstallerModel
|
||||
import app.revanced.manager.ui.model.ProgressKey
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.StepProgressProvider
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.saveableVar
|
||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
@ -51,68 +59,72 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.time.withTimeout
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.time.Duration
|
||||
import java.util.UUID
|
||||
|
||||
@Stable
|
||||
@OptIn(PluginHostApi::class)
|
||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||
class PatcherViewModel(
|
||||
private val input: Destination.Patcher
|
||||
) : ViewModel(), KoinComponent {
|
||||
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
|
||||
private val app: Application by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
private val pm: PM by inject()
|
||||
private val workerRepository: WorkerRepository by inject()
|
||||
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 val savedStateHandle: SavedStateHandle = get()
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
val packageName: String = input.selectedApp.packageName
|
||||
var installedPackageName by mutableStateOf<String?>(null)
|
||||
val packageName = input.selectedApp.packageName
|
||||
|
||||
var installedPackageName by savedStateHandle.saveable(
|
||||
key = "installedPackageName",
|
||||
// Force Kotlin to select the correct overload.
|
||||
stateSaver = autoSaver()
|
||||
) {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
private set
|
||||
var isInstalling by mutableStateOf(false)
|
||||
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
|
||||
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
||||
key = "packageInstallerStatus",
|
||||
stateSaver = autoSaver()
|
||||
) {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
private set
|
||||
|
||||
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(null)
|
||||
var isInstalling by mutableStateOf(ongoingPmSession)
|
||||
private set
|
||||
|
||||
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
||||
null
|
||||
)
|
||||
val activityPromptDialog by derivedStateOf { currentActivityRequest?.second }
|
||||
|
||||
private var launchedActivity: CompletableDeferred<ActivityResult>? = null
|
||||
private val launchActivityChannel = Channel<Intent>()
|
||||
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
|
||||
|
||||
private val tempDir = fs.tempDir.resolve("installer").also {
|
||||
it.deleteRecursively()
|
||||
it.mkdirs()
|
||||
private val tempDir = savedStateHandle.saveable(key = "tempDir") {
|
||||
fs.uiTempDir.resolve("installer").also {
|
||||
it.deleteRecursively()
|
||||
it.mkdirs()
|
||||
}
|
||||
}
|
||||
private var inputFile: File? = null
|
||||
|
||||
private var inputFile: File? by savedStateHandle.saveableVar()
|
||||
private val outputFile = tempDir.resolve("output.apk")
|
||||
|
||||
private val logs = mutableListOf<Pair<LogLevel, String>>()
|
||||
private val logs by savedStateHandle.saveable<MutableList<Pair<LogLevel, String>>> { mutableListOf() }
|
||||
private val logger = object : Logger() {
|
||||
override fun log(level: LogLevel, message: String) {
|
||||
level.androidLog(message)
|
||||
@ -124,28 +136,56 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size }))
|
||||
private val downloadProgress = MutableStateFlow<Pair<Long, Long?>?>(null)
|
||||
val steps = generateSteps(
|
||||
app,
|
||||
input.selectedApp,
|
||||
downloadProgress
|
||||
).toMutableStateList()
|
||||
private val patchCount = input.selectedPatches.values.sumOf { it.size }
|
||||
private var completedPatchCount by savedStateHandle.saveable {
|
||||
// SavedStateHandle.saveable only supports the boxed version.
|
||||
@Suppress("AutoboxingStateCreation") mutableStateOf(
|
||||
0
|
||||
)
|
||||
}
|
||||
val patchesProgress get() = completedPatchCount to patchCount
|
||||
override var downloadProgress by savedStateHandle.saveable(
|
||||
key = "downloadProgress",
|
||||
stateSaver = autoSaver()
|
||||
) {
|
||||
mutableStateOf<Pair<Long, Long?>?>(null)
|
||||
}
|
||||
private set
|
||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||
generateSteps(
|
||||
app,
|
||||
input.selectedApp
|
||||
).toMutableStateList()
|
||||
}
|
||||
private var currentStepIndex = 0
|
||||
|
||||
val progress by derivedStateOf {
|
||||
val current = steps.count {
|
||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
||||
} + completedPatchCount
|
||||
|
||||
val total = steps.size - 1 + patchCount
|
||||
|
||||
current.toFloat() / total.toFloat()
|
||||
}
|
||||
|
||||
private val workManager = WorkManager.getInstance(app)
|
||||
|
||||
private val patcherWorkerId: UUID =
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
||||
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.selectedApp,
|
||||
outputFile.path,
|
||||
input.selectedPatches,
|
||||
input.options,
|
||||
logger,
|
||||
downloadProgress,
|
||||
patchesProgress,
|
||||
setInputFile = { inputFile = it },
|
||||
onDownloadProgress = {
|
||||
withContext(Dispatchers.Main) {
|
||||
downloadProgress = it
|
||||
}
|
||||
},
|
||||
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
|
||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||
handleStartActivityRequest = { plugin, intent ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||
@ -192,10 +232,11 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
val patcherSucceeded =
|
||||
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo? ->
|
||||
workManager.getWorkInfoByIdLiveData(patcherWorkerId.uuid).map { workInfo: WorkInfo? ->
|
||||
when (workInfo?.state) {
|
||||
WorkInfo.State.SUCCEEDED -> true
|
||||
WorkInfo.State.FAILED -> false
|
||||
@ -229,9 +270,7 @@ class PatcherViewModel(
|
||||
input.selectedPatches
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
installerStatusDialogModel.packageInstallerStatus = pmStatus
|
||||
} else packageInstallerStatus = pmStatus
|
||||
|
||||
isInstalling = false
|
||||
}
|
||||
@ -245,15 +284,15 @@ class PatcherViewModel(
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
?.let(logger::trace)
|
||||
|
||||
if (pmStatus != PackageInstaller.STATUS_SUCCESS) {
|
||||
installerStatusDialogModel.packageInstallerStatus = pmStatus
|
||||
}
|
||||
if (pmStatus != PackageInstaller.STATUS_SUCCESS)
|
||||
packageInstallerStatus = pmStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it.
|
||||
init {
|
||||
// TODO: detect system-initiated process death during the patching process.
|
||||
ContextCompat.registerReceiver(
|
||||
app,
|
||||
installerBroadcastReceiver,
|
||||
@ -273,7 +312,7 @@ class PatcherViewModel(
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
app.unregisterReceiver(installerBroadcastReceiver)
|
||||
workManager.cancelWorkById(patcherWorkerId)
|
||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||
|
||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
@ -284,7 +323,10 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
||||
tempDir.deleteRecursively()
|
||||
}
|
||||
|
||||
@ -342,8 +384,7 @@ class PatcherViewModel(
|
||||
// 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
|
||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
@ -368,13 +409,13 @@ class PatcherViewModel(
|
||||
val label = with(pm) {
|
||||
packageInfo.label()
|
||||
}
|
||||
|
||||
// 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.isNotEmpty()) {
|
||||
// Exit if there is no base APK package
|
||||
installerStatusDialogModel.packageInstallerStatus =
|
||||
PackageInstaller.STATUS_FAILURE_INVALID
|
||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
@ -419,23 +460,33 @@ class PatcherViewModel(
|
||||
Log.e(tag, "Failed to install", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
} finally {
|
||||
if (!pmInstallStarted)
|
||||
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")
|
||||
override fun install() {
|
||||
// InstallType.MOUNT is never used here since this overload is for the package installer status dialog.
|
||||
install(InstallType.DEFAULT)
|
||||
}
|
||||
|
||||
pm.installApp(listOf(outputFile))
|
||||
isInstalling = true
|
||||
override 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"
|
||||
fun dismissPackageInstallerDialog() {
|
||||
packageInstallerStatus = null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "ReVanced Patcher"
|
||||
|
||||
fun LogLevel.androidLog(msg: String) = when (this) {
|
||||
LogLevel.TRACE -> Log.v(TAG, msg)
|
||||
@ -444,11 +495,7 @@ class PatcherViewModel(
|
||||
LogLevel.ERROR -> Log.e(TAG, msg)
|
||||
}
|
||||
|
||||
fun generateSteps(
|
||||
context: Context,
|
||||
selectedApp: SelectedApp,
|
||||
downloadProgress: StateFlow<Pair<Long, Long?>?>? = null
|
||||
): List<Step> {
|
||||
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
|
||||
val needsDownload =
|
||||
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
|
||||
|
||||
@ -457,7 +504,7 @@ class PatcherViewModel(
|
||||
context.getString(R.string.download_apk),
|
||||
StepCategory.PREPARING,
|
||||
state = State.RUNNING,
|
||||
downloadProgress = downloadProgress,
|
||||
progressKey = ProgressKey.DOWNLOAD,
|
||||
).takeIf { needsDownload },
|
||||
Step(
|
||||
context.getString(R.string.patcher_step_load_patches),
|
||||
|
@ -163,6 +163,8 @@ class PM(
|
||||
app.startActivity(it)
|
||||
}
|
||||
|
||||
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
|
@ -0,0 +1,18 @@
|
||||
package app.revanced.manager.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
object RequestInstallAppsContract : ActivityResultContract<String, Boolean>(), KoinComponent {
|
||||
private val pm: PM by inject()
|
||||
override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.fromParts("package", input, null))
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
|
||||
return pm.canInstallPackages()
|
||||
}
|
||||
}
|
@ -3,8 +3,14 @@ package app.revanced.manager.util
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.icu.number.Notation
|
||||
import android.icu.number.NumberFormatter
|
||||
import android.icu.number.Precision
|
||||
import android.icu.text.CompactDecimalFormat
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
@ -28,6 +34,7 @@ import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import app.revanced.manager.R
|
||||
@ -48,6 +55,9 @@ import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.util.Locale
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
typealias PatchSelection = Map<Int, Set<String>>
|
||||
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
||||
@ -261,3 +271,22 @@ fun <T, R> ((T) -> R).withHapticFeedback(constant: Int): (T) -> R {
|
||||
}
|
||||
|
||||
fun Modifier.enabled(condition: Boolean) = if (condition) this else alpha(0.5f)
|
||||
|
||||
@MainThread
|
||||
fun <T : Any> SavedStateHandle.saveableVar(init: () -> T): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> =
|
||||
PropertyDelegateProvider { _: Any?, property ->
|
||||
val name = property.name
|
||||
if (name !in this) this[name] = init()
|
||||
object : ReadWriteProperty<Any?, T> {
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): T = get(name)!!
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) =
|
||||
set(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> SavedStateHandle.saveableVar(): ReadWriteProperty<Any?, T?> =
|
||||
object : ReadWriteProperty<Any?, T?> {
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): T? = get(property.name)
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) =
|
||||
set(property.name, value)
|
||||
}
|
@ -35,6 +35,9 @@
|
||||
<string name="bundle_name_default">Default</string>
|
||||
<string name="bundle_name_fallback">Unnamed</string>
|
||||
|
||||
<string name="android_11_bug_dialog_title">Android 11 bug</string>
|
||||
<string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string>
|
||||
|
||||
<string name="selected_app_meta_any_version">Any available version</string>
|
||||
<string name="app_source_dialog_title">Select source</string>
|
||||
<string name="app_source_dialog_option_auto">Auto</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user