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
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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) =
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,15 +188,13 @@ 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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(visible = messageExpanded && message != null) {
|
AnimatedVisibility(visible = messageExpanded && message != null) {
|
||||||
Text(
|
Text(
|
||||||
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
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
|
@ -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))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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") {
|
||||||
|
fs.uiTempDir.resolve("installer").also {
|
||||||
it.deleteRecursively()
|
it.deleteRecursively()
|
||||||
it.mkdirs()
|
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.
|
||||||
|
@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,
|
app,
|
||||||
input.selectedApp,
|
input.selectedApp
|
||||||
downloadProgress
|
|
||||||
).toMutableStateList()
|
).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,12 +460,17 @@ 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() {
|
||||||
|
// InstallType.MOUNT is never used here since this overload is for the package installer status dialog.
|
||||||
|
install(InstallType.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reinstall() {
|
||||||
|
viewModelScope.launch {
|
||||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||||
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
||||||
?: throw Exception("Failed to load application info")
|
?: throw Exception("Failed to load application info")
|
||||||
@ -433,9 +479,14 @@ class PatcherViewModel(
|
|||||||
isInstalling = true
|
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),
|
||||||
|
@ -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 ->
|
||||||
|
@ -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.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)
|
||||||
|
}
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user