fix: process death resilience and account for android 11 bug (#2355)

This commit is contained in:
Ax333l 2024-12-22 22:28:54 +01:00 committed by GitHub
parent 9916e4da4d
commit 49f75f9edd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 377 additions and 171 deletions

View File

@ -1,10 +1,15 @@
package app.revanced.manager package app.revanced.manager
import android.app.Activity
import android.app.Application 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.di.*
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloaderPluginRepository import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
@ -25,6 +30,7 @@ class ManagerApplication : Application() {
private val prefs: PreferencesManager by inject() private val prefs: PreferencesManager by inject()
private val patchBundleRepository: PatchBundleRepository by inject() private val patchBundleRepository: PatchBundleRepository by inject()
private val downloaderPluginRepository: DownloaderPluginRepository by inject() private val downloaderPluginRepository: DownloaderPluginRepository by inject()
private val fs: Filesystem by inject()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -71,5 +77,34 @@ class ManagerApplication : Application() {
updateCheck() 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()
}
} }
} }

View File

@ -9,6 +9,8 @@ import android.os.Environment
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import app.revanced.manager.util.RequestManageStorageContract import app.revanced.manager.util.RequestManageStorageContract
import java.io.File
import java.nio.file.Path
class Filesystem(private val app: Application) { class Filesystem(private val app: Application) {
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here. 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. * A directory that gets cleared when the app restarts.
* Do not store paths to this directory in a parcel. * 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() deleteRecursively()
mkdirs() 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 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> { 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 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
} }

View File

@ -25,7 +25,7 @@ class Session(
private val androidContext: Context, private val androidContext: Context,
private val logger: Logger, private val logger: Logger,
private val input: File, private val input: File,
private val onPatchCompleted: () -> Unit, private val onPatchCompleted: suspend () -> Unit,
private val onProgress: (name: String?, state: State?, message: String?) -> Unit private val onProgress: (name: String?, state: State?, message: String?) -> Unit
) : Closeable { ) : Closeable {
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) = private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =

View File

@ -20,7 +20,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onPatchCompleted: () -> Unit, onPatchCompleted: suspend () -> Unit,
onProgress: ProgressEventHandler, onProgress: ProgressEventHandler,
) { ) {
val bundles = bundles() val bundles = bundles()

View File

@ -66,7 +66,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onPatchCompleted: () -> Unit, onPatchCompleted: suspend () -> Unit,
onProgress: ProgressEventHandler, onProgress: ProgressEventHandler,
) = coroutineScope { ) = coroutineScope {
// Get the location of our own Apk. // Get the location of our own Apk.
@ -123,7 +123,9 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
val eventHandler = object : IPatcherEvents.Stub() { val eventHandler = object : IPatcherEvents.Stub() {
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg) 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?) = override fun progress(name: String?, state: String?, msg: String?) =
onProgress(name, state?.let { enumValueOf<State>(it) }, msg) onProgress(name, state?.let { enumValueOf<State>(it) }, msg)

View File

@ -34,7 +34,7 @@ sealed class Runtime(context: Context) : KoinComponent {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onPatchCompleted: () -> Unit, onPatchCompleted: suspend () -> Unit,
onProgress: ProgressEventHandler, onProgress: ProgressEventHandler,
) )
} }

View File

@ -42,9 +42,7 @@ import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -73,10 +71,10 @@ class PatcherWorker(
val selectedPatches: PatchSelection, val selectedPatches: PatchSelection,
val options: Options, val options: Options,
val logger: Logger, val logger: Logger,
val downloadProgress: MutableStateFlow<Pair<Long, Long?>?>, val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
val patchesProgress: MutableStateFlow<Pair<Int, Int>>, val onPatchCompleted: suspend () -> Unit,
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult, val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
val setInputFile: (File) -> Unit, val setInputFile: suspend (File) -> Unit,
val onProgress: ProgressEventHandler val onProgress: ProgressEventHandler
) { ) {
val packageName get() = input.packageName val packageName get() = input.packageName
@ -160,7 +158,7 @@ class PatcherWorker(
data, data,
args.packageName, args.packageName,
args.input.version, args.input.version,
onDownload = args.downloadProgress::emit onDownload = args.onDownloadProgress
).also { ).also {
args.setInputFile(it) args.setInputFile(it)
updateProgress(state = State.COMPLETED) // Download APK updateProgress(state = State.COMPLETED) // Download APK
@ -224,11 +222,7 @@ class PatcherWorker(
args.selectedPatches, args.selectedPatches,
args.options, args.options,
args.logger, args.logger,
onPatchCompleted = { args.onPatchCompleted,
args.patchesProgress.update { (completed, total) ->
completed + 1 to total
}
},
args.onProgress args.onProgress
) )

View File

@ -6,7 +6,6 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.model.InstallerModel
import com.github.materiiapps.enumutil.FromValue import com.github.materiiapps.enumutil.FromValue
private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit) private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit)
private typealias InstallerStatusDialogButton = @Composable (model: InstallerStatusDialogModel) -> Unit private typealias InstallerStatusDialogButton = @Composable (model: InstallerModel, dismiss: () -> Unit) -> Unit
interface InstallerModel {
fun reinstall()
fun install()
}
interface InstallerStatusDialogModel : InstallerModel {
var packageInstallerStatus: Int?
}
@Composable @Composable
fun InstallerStatusDialog(model: InstallerStatusDialogModel) { fun InstallerStatusDialog(installerStatus: Int, model: InstallerModel, onDismiss: () -> Unit) {
val dialogKind = remember { val dialogKind = remember {
DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE DialogKind.fromValue(installerStatus) ?: DialogKind.FAILURE
} }
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest = onDismiss,
model.packageInstallerStatus = null
},
confirmButton = { confirmButton = {
dialogKind.confirmButton(model) dialogKind.confirmButton(model, onDismiss)
}, },
dismissButton = { dismissButton = {
dialogKind.dismissButton?.invoke(model) dialogKind.dismissButton?.invoke(model, onDismiss)
}, },
icon = { icon = {
Icon(dialogKind.icon, null) Icon(dialogKind.icon, null)
@ -75,10 +64,10 @@ fun InstallerStatusDialog(model: InstallerStatusDialogModel) {
private fun installerStatusDialogButton( private fun installerStatusDialogButton(
@StringRes buttonStringResId: Int, @StringRes buttonStringResId: Int,
buttonHandler: InstallerStatusDialogButtonHandler = { }, buttonHandler: InstallerStatusDialogButtonHandler = { },
): InstallerStatusDialogButton = { model -> ): InstallerStatusDialogButton = { model, dismiss ->
TextButton( TextButton(
onClick = { onClick = {
model.packageInstallerStatus = null dismiss()
buttonHandler(model) buttonHandler(model)
} }
) { ) {
@ -154,6 +143,7 @@ enum class DialogKind(
model.install() model.install()
}, },
); );
// Needed due to the @FromValue annotation. // Needed due to the @FromValue annotation.
companion object companion object
} }

View File

@ -36,13 +36,14 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.R
import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.ArrowButton
import app.revanced.manager.ui.component.LoadingIndicator 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.State
import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider
import java.util.Locale import java.util.Locale
import kotlin.math.floor import kotlin.math.floor
@ -52,6 +53,7 @@ fun Steps(
category: StepCategory, category: StepCategory,
steps: List<Step>, steps: List<Step>,
stepCount: Pair<Int, Int>? = null, stepCount: Pair<Int, Int>? = null,
stepProgressProvider: StepProgressProvider
) { ) {
var expanded by rememberSaveable { mutableStateOf(true) } var expanded by rememberSaveable { mutableStateOf(true) }
@ -116,13 +118,20 @@ fun Steps(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
steps.forEach { step -> 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( SubStep(
name = step.name, name = step.name,
state = step.state, state = step.state,
message = step.message, message = step.message,
downloadProgress = downloadProgress?.value progress = progress,
progressText = progressText
) )
} }
} }
@ -135,7 +144,8 @@ fun SubStep(
name: String, name: String,
state: State, state: State,
message: String? = null, message: String? = null,
downloadProgress: Pair<Long, Long?>? = null progress: Float? = null,
progressText: String? = null
) { ) {
var messageExpanded by rememberSaveable { mutableStateOf(true) } var messageExpanded by rememberSaveable { mutableStateOf(true) }
@ -156,7 +166,7 @@ fun SubStep(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
StepIcon(state, downloadProgress, size = 20.dp) StepIcon(state, progress, size = 20.dp)
} }
Text( Text(
@ -167,8 +177,8 @@ fun SubStep(
modifier = Modifier.weight(1f, true), modifier = Modifier.weight(1f, true),
) )
if (message != null) { when {
Box( message != null -> Box(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@ -178,13 +188,11 @@ fun SubStep(
onClick = null onClick = null
) )
} }
} else {
downloadProgress?.let { (current, total) -> progressText != null -> Text(
Text( progressText,
if (total != null) "${current.megaBytes}/${total.megaBytes} MB" else "${current.megaBytes} MB", style = MaterialTheme.typography.labelSmall
style = MaterialTheme.typography.labelSmall )
)
}
} }
} }
@ -200,7 +208,7 @@ fun SubStep(
} }
@Composable @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) val strokeWidth = Dp(floor(size.value / 10) + 1)
when (state) { when (state) {
@ -234,12 +242,7 @@ fun StepIcon(state: State, progress: Pair<Long, Long?>? = null, size: Dp) {
contentDescription = description contentDescription = description
} }
}, },
progress = { progress = { progress },
progress?.let { (current, total) ->
if (total == null) return@let null
current / total
}?.toFloat()
},
strokeWidth = strokeWidth strokeWidth = strokeWidth
) )
} }

View File

@ -0,0 +1,6 @@
package app.revanced.manager.ui.model
interface InstallerModel {
fun reinstall()
fun install()
}

View File

@ -1,8 +1,9 @@
package app.revanced.manager.ui.model package app.revanced.manager.ui.model
import android.os.Parcelable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import app.revanced.manager.R import app.revanced.manager.R
import kotlinx.coroutines.flow.StateFlow import kotlinx.parcelize.Parcelize
enum class StepCategory(@StringRes val displayName: Int) { enum class StepCategory(@StringRes val displayName: Int) {
PREPARING(R.string.patcher_step_group_preparing), PREPARING(R.string.patcher_step_group_preparing),
@ -14,10 +15,19 @@ enum class State {
WAITING, RUNNING, FAILED, COMPLETED WAITING, RUNNING, FAILED, COMPLETED
} }
enum class ProgressKey {
DOWNLOAD
}
interface StepProgressProvider {
val downloadProgress: Pair<Long, Long?>?
}
@Parcelize
data class Step( data class Step(
val name: String, val name: String,
val category: StepCategory, val category: StepCategory,
val state: State = State.WAITING, val state: State = State.WAITING,
val message: String? = null, val message: String? = null,
val downloadProgress: StateFlow<Pair<Long, Long?>?>? = null val progressKey: ProgressKey? = null
) ) : Parcelable

View File

@ -5,6 +5,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager 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.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.patcher.aapt.Aapt 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.AppTopBar
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.component.AvailableUpdateDialog 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.haptics.HapticTab
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
import app.revanced.manager.ui.viewmodel.DashboardViewModel import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.util.RequestInstallAppsContract
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel 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 { val availableUpdate by remember {
derivedStateOf { vm.updatedManagerVersion.takeIf { showDialog } } derivedStateOf { vm.updatedManagerVersion.takeIf { showUpdateDialog } }
} }
availableUpdate?.let { version -> availableUpdate?.let { version ->
AvailableUpdateDialog( AvailableUpdateDialog(
onDismiss = { showDialog = false }, onDismiss = { showUpdateDialog = false },
setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch, setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch,
onConfirm = onUpdateClick, onConfirm = onUpdateClick,
newVersion = version 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( Scaffold(
topBar = { topBar = {
if (bundlesSelectable) { if (bundlesSelectable) {
@ -188,6 +207,10 @@ fun DashboardScreen(
} }
return@HapticFloatingActionButton return@HapticFloatingActionButton
} }
if (vm.android11BugActive) {
showAndroid11Dialog = true
return@HapticFloatingActionButton
}
onAppSelectorClick() 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))
}
)
}

View File

@ -29,7 +29,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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.R
import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.AppScaffold 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.haptics.HapticExtendedFloatingActionButton
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.StepCategory import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.viewmodel.PatcherViewModel import app.revanced.manager.ui.viewmodel.PatcherViewModel
import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.APK_MIMETYPE
@ -50,7 +48,11 @@ fun PatcherScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: PatcherViewModel vm: PatcherViewModel
) { ) {
BackHandler(onBack = onBackClick) fun leaveScreen() {
vm.onBack()
onBackClick()
}
BackHandler(onBack = ::leaveScreen)
val context = LocalContext.current val context = LocalContext.current
val exportApkLauncher = 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) { if (patcherSucceeded == null) {
DisposableEffect(Unit) { DisposableEffect(Unit) {
val window = (context as Activity).window val window = (context as Activity).window
@ -98,8 +84,9 @@ fun PatcherScreen(
onConfirm = vm::install onConfirm = vm::install
) )
if (vm.installerStatusDialogModel.packageInstallerStatus != null) vm.packageInstallerStatus?.let {
InstallerStatusDialog(vm.installerStatusDialogModel) InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog)
}
val activityLauncher = rememberLauncherForActivityResult( val activityLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(), contract = ActivityResultContracts.StartActivityForResult(),
@ -137,7 +124,7 @@ fun PatcherScreen(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.patcher), title = stringResource(R.string.patcher),
onBackClick = onBackClick onBackClick = ::leaveScreen
) )
}, },
bottomBar = { bottomBar = {
@ -193,7 +180,7 @@ fun PatcherScreen(
.fillMaxSize() .fillMaxSize()
) { ) {
LinearProgressIndicator( LinearProgressIndicator(
progress = { progress }, progress = { vm.progress },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@ -209,7 +196,8 @@ fun PatcherScreen(
Steps( Steps(
category = category, category = category,
steps = steps, steps = steps,
stepCount = if (category == StepCategory.PATCHING) patchesProgress else null stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null,
stepProgressProvider = vm
) )
} }
} }

View File

@ -6,9 +6,13 @@ import android.net.Uri
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.R
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
@ -22,13 +26,19 @@ import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
@OptIn(SavedStateHandleSaveableApi::class)
class AppSelectorViewModel( class AppSelectorViewModel(
private val app: Application, private val app: Application,
private val pm: PM, private val pm: PM,
private val patchBundleRepository: PatchBundleRepository fs: Filesystem,
private val patchBundleRepository: PatchBundleRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val inputFile = File(app.filesDir, "input.apk").also { private val inputFile = savedStateHandle.saveable(key = "inputFile") {
it.delete() File(
fs.uiTempDir,
"input.apk"
).also(File::delete)
} }
val appList = pm.appList val appList = pm.appList

View File

@ -3,6 +3,7 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.ContentResolver import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf 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.DownloaderPluginRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.PM
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -32,7 +34,8 @@ class DashboardViewModel(
private val downloaderPluginRepository: DownloaderPluginRepository, private val downloaderPluginRepository: DownloaderPluginRepository,
private val reVancedAPI: ReVancedAPI, private val reVancedAPI: ReVancedAPI,
private val networkInfo: NetworkInfo, private val networkInfo: NetworkInfo,
val prefs: PreferencesManager val prefs: PreferencesManager,
private val pm: PM,
) : ViewModel() { ) : ViewModel() {
val availablePatches = val availablePatches =
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } }
@ -43,6 +46,14 @@ class DashboardViewModel(
val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() } 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) var updatedManagerVersion: String? by mutableStateOf(null)
private set private set
var showBatteryOptimizationsWarning by mutableStateOf(false) var showBatteryOptimizationsWarning by mutableStateOf(false)

View File

@ -7,18 +7,22 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.net.Uri import android.net.Uri
import android.os.ParcelUuid
import android.util.Log import android.util.Log
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import app.revanced.manager.R 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.plugin.downloader.UserInteractionException
import app.revanced.manager.service.InstallService import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService 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.InstallerModel
import app.revanced.manager.ui.model.ProgressKey
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
import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory 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.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.simpleMessage
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
@ -51,68 +59,72 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.time.withTimeout
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
import java.time.Duration import java.time.Duration
import java.util.UUID
@Stable @OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
@OptIn(PluginHostApi::class)
class PatcherViewModel( class PatcherViewModel(
private val input: Destination.Patcher private val input: Destination.Patcher
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
private val app: Application by inject() private val app: Application by inject()
private val fs: Filesystem by inject() private val fs: Filesystem by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject() private val workerRepository: WorkerRepository by inject()
private val installedAppRepository: InstalledAppRepository by inject() private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject() private val rootInstaller: RootInstaller by inject()
private val savedStateHandle: SavedStateHandle = get()
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 private var installedApp: InstalledApp? = null
val packageName: String = input.selectedApp.packageName val packageName = input.selectedApp.packageName
var installedPackageName by mutableStateOf<String?>(null)
var installedPackageName by savedStateHandle.saveable(
key = "installedPackageName",
// Force Kotlin to select the correct overload.
stateSaver = autoSaver()
) {
mutableStateOf<String?>(null)
}
private set 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 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 } val activityPromptDialog by derivedStateOf { currentActivityRequest?.second }
private var launchedActivity: CompletableDeferred<ActivityResult>? = null private var launchedActivity: CompletableDeferred<ActivityResult>? = null
private val launchActivityChannel = Channel<Intent>() private val launchActivityChannel = Channel<Intent>()
val launchActivityFlow = launchActivityChannel.receiveAsFlow() val launchActivityFlow = launchActivityChannel.receiveAsFlow()
private val tempDir = fs.tempDir.resolve("installer").also { private val tempDir = savedStateHandle.saveable(key = "tempDir") {
it.deleteRecursively() fs.uiTempDir.resolve("installer").also {
it.mkdirs() 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 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() { private val logger = object : Logger() {
override fun log(level: LogLevel, message: String) { override fun log(level: LogLevel, message: String) {
level.androidLog(message) level.androidLog(message)
@ -124,28 +136,56 @@ class PatcherViewModel(
} }
} }
val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size })) private val patchCount = input.selectedPatches.values.sumOf { it.size }
private val downloadProgress = MutableStateFlow<Pair<Long, Long?>?>(null) private var completedPatchCount by savedStateHandle.saveable {
val steps = generateSteps( // SavedStateHandle.saveable only supports the boxed version.
app, @Suppress("AutoboxingStateCreation") mutableStateOf(
input.selectedApp, 0
downloadProgress )
).toMutableStateList() }
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 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 workManager = WorkManager.getInstance(app)
private val patcherWorkerId: UUID = private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>( ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args( "patching", PatcherWorker.Args(
input.selectedApp, input.selectedApp,
outputFile.path, outputFile.path,
input.selectedPatches, input.selectedPatches,
input.options, input.options,
logger, logger,
downloadProgress, onDownloadProgress = {
patchesProgress, withContext(Dispatchers.Main) {
setInputFile = { inputFile = it }, downloadProgress = it
}
},
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
handleStartActivityRequest = { plugin, intent -> handleStartActivityRequest = { plugin, intent ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (currentActivityRequest != null) throw Exception("Another request is already pending.") if (currentActivityRequest != null) throw Exception("Another request is already pending.")
@ -192,10 +232,11 @@ class PatcherViewModel(
} }
} }
) )
) ))
}
val patcherSucceeded = val patcherSucceeded =
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo? -> workManager.getWorkInfoByIdLiveData(patcherWorkerId.uuid).map { workInfo: WorkInfo? ->
when (workInfo?.state) { when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> true WorkInfo.State.SUCCEEDED -> true
WorkInfo.State.FAILED -> false WorkInfo.State.FAILED -> false
@ -229,9 +270,7 @@ class PatcherViewModel(
input.selectedPatches input.selectedPatches
) )
} }
} } else packageInstallerStatus = pmStatus
installerStatusDialogModel.packageInstallerStatus = pmStatus
isInstalling = false isInstalling = false
} }
@ -245,15 +284,15 @@ class PatcherViewModel(
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
?.let(logger::trace) ?.let(logger::trace)
if (pmStatus != PackageInstaller.STATUS_SUCCESS) { if (pmStatus != PackageInstaller.STATUS_SUCCESS)
installerStatusDialogModel.packageInstallerStatus = pmStatus 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( ContextCompat.registerReceiver(
app, app,
installerBroadcastReceiver, installerBroadcastReceiver,
@ -273,7 +312,7 @@ class PatcherViewModel(
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
app.unregisterReceiver(installerBroadcastReceiver) app.unregisterReceiver(installerBroadcastReceiver)
workManager.cancelWorkById(patcherWorkerId) workManager.cancelWorkById(patcherWorkerId.uuid)
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) { if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
GlobalScope.launch(Dispatchers.Main) { 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() tempDir.deleteRecursively()
} }
@ -342,8 +384,7 @@ class PatcherViewModel(
// Check if the app version is less than the installed version // Check if the app version is less than the installed version
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) { if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
// Exit if the selected app version is less than the installed version // Exit if the selected app version is less than the installed version
installerStatusDialogModel.packageInstallerStatus = packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
PackageInstaller.STATUS_FAILURE_CONFLICT
return@launch return@launch
} }
} }
@ -368,13 +409,13 @@ class PatcherViewModel(
val label = with(pm) { val label = with(pm) {
packageInfo.label() packageInfo.label()
} }
// Check for base APK, first check if the app is already installed // Check for base APK, first check if the app is already installed
if (existingPackageInfo == null) { if (existingPackageInfo == null) {
// If the app is not installed, check if the output file is a base apk // If the app is not installed, check if the output file is a base apk
if (currentPackageInfo.splitNames.isNotEmpty()) { if (currentPackageInfo.splitNames.isNotEmpty()) {
// Exit if there is no base APK package // Exit if there is no base APK package
installerStatusDialogModel.packageInstallerStatus = packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
PackageInstaller.STATUS_FAILURE_INVALID
return@launch return@launch
} }
} }
@ -419,23 +460,33 @@ class PatcherViewModel(
Log.e(tag, "Failed to install", e) Log.e(tag, "Failed to install", e)
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
} finally { } finally {
if (!pmInstallStarted) if (!pmInstallStarted) isInstalling = false
isInstalling = false
} }
} }
fun reinstall() = viewModelScope.launch { override fun install() {
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") { // InstallType.MOUNT is never used here since this overload is for the package installer status dialog.
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) } install(InstallType.DEFAULT)
?: throw Exception("Failed to load application info") }
pm.installApp(listOf(outputFile)) override fun reinstall() {
isInstalling = true 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 { fun dismissPackageInstallerDialog() {
private const val TAG = "ReVanced Patcher" packageInstallerStatus = null
}
private companion object {
const val TAG = "ReVanced Patcher"
fun LogLevel.androidLog(msg: String) = when (this) { fun LogLevel.androidLog(msg: String) = when (this) {
LogLevel.TRACE -> Log.v(TAG, msg) LogLevel.TRACE -> Log.v(TAG, msg)
@ -444,11 +495,7 @@ class PatcherViewModel(
LogLevel.ERROR -> Log.e(TAG, msg) LogLevel.ERROR -> Log.e(TAG, msg)
} }
fun generateSteps( fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
context: Context,
selectedApp: SelectedApp,
downloadProgress: StateFlow<Pair<Long, Long?>?>? = null
): List<Step> {
val needsDownload = val needsDownload =
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
@ -457,7 +504,7 @@ class PatcherViewModel(
context.getString(R.string.download_apk), context.getString(R.string.download_apk),
StepCategory.PREPARING, StepCategory.PREPARING,
state = State.RUNNING, state = State.RUNNING,
downloadProgress = downloadProgress, progressKey = ProgressKey.DOWNLOAD,
).takeIf { needsDownload }, ).takeIf { needsDownload },
Step( Step(
context.getString(R.string.patcher_step_load_patches), context.getString(R.string.patcher_step_load_patches),

View File

@ -163,6 +163,8 @@ class PM(
app.startActivity(it) app.startActivity(it)
} }
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
private fun PackageInstaller.Session.writeApk(apk: File) { private fun PackageInstaller.Session.writeApk(apk: File) {
apk.inputStream().use { inputStream -> apk.inputStream().use { inputStream ->
openWrite(apk.name, 0, apk.length()).use { outputStream -> openWrite(apk.name, 0, apk.length()).use { outputStream ->

View File

@ -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()
}
}

View File

@ -3,8 +3,14 @@ package app.revanced.manager.util
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo 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.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.MainThread
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@ -28,6 +34,7 @@ import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import app.revanced.manager.R import app.revanced.manager.R
@ -48,6 +55,9 @@ import kotlinx.datetime.format.char
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import java.util.Locale import java.util.Locale
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
typealias PatchSelection = Map<Int, Set<String>> typealias PatchSelection = Map<Int, Set<String>>
typealias Options = Map<Int, Map<String, Map<String, Any?>>> 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) 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)
}

View File

@ -35,6 +35,9 @@
<string name="bundle_name_default">Default</string> <string name="bundle_name_default">Default</string>
<string name="bundle_name_fallback">Unnamed</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="selected_app_meta_any_version">Any available version</string>
<string name="app_source_dialog_title">Select source</string> <string name="app_source_dialog_title">Select source</string>
<string name="app_source_dialog_option_auto">Auto</string> <string name="app_source_dialog_option_auto">Auto</string>