This commit is contained in:
Ax333l 2024-08-17 21:25:13 +02:00
parent e992a99783
commit 0930b9fda7
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
13 changed files with 227 additions and 91 deletions

View File

@ -1,9 +1,14 @@
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.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
@ -23,6 +28,8 @@ class ManagerApplication : Application() {
private val scope = MainScope() private val scope = MainScope()
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 fs: Filesystem by inject()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -65,5 +72,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,35 @@ 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).apply {
mkdirs()
}
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

@ -26,7 +26,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

@ -35,7 +35,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

@ -61,9 +61,9 @@ class PatcherWorker(
val selectedPatches: PatchSelection, val selectedPatches: PatchSelection,
val options: Options, val options: Options,
val logger: Logger, val logger: Logger,
val downloadProgress: MutableStateFlow<Pair<Float, Float>?>, val onDownloadProgress: suspend (Pair<Float, Float>?) -> Unit,
val patchesProgress: MutableStateFlow<Pair<Int, Int>>, val onPatchCompleted: suspend () -> Unit,
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
@ -146,7 +146,7 @@ class PatcherWorker(
downloadedAppRepository.download( downloadedAppRepository.download(
selectedApp.app, selectedApp.app,
prefs.preferSplits.get(), prefs.preferSplits.get(),
onDownload = { args.downloadProgress.emit(it) } onDownload = args.onDownloadProgress
).also { ).also {
args.setInputFile(it) args.setInputFile(it)
updateProgress(state = State.COMPLETED) // Download APK updateProgress(state = State.COMPLETED) // Download APK
@ -170,11 +170,13 @@ class PatcherWorker(
args.selectedPatches, args.selectedPatches,
args.options, args.options,
args.logger, args.logger,
/*
onPatchCompleted = { onPatchCompleted = {
args.patchesProgress.update { (completed, total) -> args.patchesProgress.update { (completed, total) ->
completed + 1 to total completed + 1 to total
} }
}, },*/
args.onPatchCompleted,
args.onProgress args.onProgress
) )

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 kotlin.math.floor import kotlin.math.floor
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt // Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
@ -51,6 +52,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) }
@ -115,13 +117,17 @@ 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) -> downloaded / total to "$downloaded/$total 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
) )
} }
} }
@ -134,7 +140,8 @@ fun SubStep(
name: String, name: String,
state: State, state: State,
message: String? = null, message: String? = null,
downloadProgress: Pair<Float, Float>? = null progress: Float? = null,
progressText: String? = null
) { ) {
var messageExpanded by rememberSaveable { mutableStateOf(true) } var messageExpanded by rememberSaveable { mutableStateOf(true) }
@ -155,7 +162,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(
@ -166,8 +173,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
) { ) {
@ -177,13 +184,11 @@ fun SubStep(
onClick = null onClick = null
) )
} }
} else {
downloadProgress?.let { (current, total) -> progressText != null -> Text(
Text( progressText,
"$current/$total MB", style = MaterialTheme.typography.labelSmall
style = MaterialTheme.typography.labelSmall )
)
}
} }
} }
@ -199,7 +204,7 @@ fun SubStep(
} }
@Composable @Composable
fun StepIcon(state: State, progress: Pair<Float, Float>? = 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) {
@ -233,7 +238,7 @@ fun StepIcon(state: State, progress: Pair<Float, Float>? = null, size: Dp) {
contentDescription = description contentDescription = description
} }
}, },
progress = { progress?.let { (current, total) -> current / total } }, progress = { progress },
strokeWidth = strokeWidth strokeWidth = strokeWidth
) )
} }

View File

@ -1,8 +1,10 @@
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.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 +16,19 @@ enum class State {
WAITING, RUNNING, FAILED, COMPLETED WAITING, RUNNING, FAILED, COMPLETED
} }
enum class ProgressKey {
DOWNLOAD
}
interface StepProgressProvider {
val downloadProgress: Pair<Float, Float>?
}
@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<Float, Float>?>? = null val progressKey: ProgressKey? = null
) ) : Parcelable

View File

@ -36,13 +36,11 @@ 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.ui.component.AppScaffold import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.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
@ -69,22 +67,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 (showInstallPicker) if (showInstallPicker)
InstallPickerDialog( InstallPickerDialog(
onDismiss = { showInstallPicker = false }, onDismiss = { showInstallPicker = false },
@ -150,7 +132,7 @@ fun PatcherScreen(
.fillMaxSize() .fillMaxSize()
) { ) {
LinearProgressIndicator( LinearProgressIndicator(
progress = { progress }, progress = { vm.progress },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@ -166,7 +148,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

@ -9,6 +9,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
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
@ -23,11 +24,10 @@ import java.nio.file.Files
class AppSelectorViewModel( class AppSelectorViewModel(
private val app: Application, private val app: Application,
private val pm: PM, private val pm: PM,
fs: Filesystem,
private val patchBundleRepository: PatchBundleRepository private val patchBundleRepository: PatchBundleRepository
) : ViewModel() { ) : ViewModel() {
private val inputFile = File(app.filesDir, "input.apk").also { private val inputFile = File(fs.uiTempDir, "input.apk").also(File::delete)
it.delete()
}
val appList = pm.appList val appList = pm.appList
var onStorageClick: (SelectedApp.Local) -> Unit = {} var onStorageClick: (SelectedApp.Local) -> Unit = {}

View File

@ -9,14 +9,19 @@ import android.content.pm.PackageInstaller
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
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
@ -31,11 +36,15 @@ import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.service.InstallService import app.revanced.manager.service.InstallService
import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.Destination
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
@ -43,8 +52,6 @@ import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
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
@ -55,32 +62,47 @@ import java.nio.file.Files
import java.time.Duration import java.time.Duration
import java.util.UUID import java.util.UUID
// @SuppressLint("AutoboxingStateCreation")
@Stable @Stable
@OptIn(SavedStateHandleSaveableApi::class)
class PatcherViewModel( class PatcherViewModel(
private val input: Destination.Patcher private val input: Destination.Patcher
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent, StepProgressProvider {
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 by inject()
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 isInstalling by mutableStateOf(ongoingPmSession)
private set private set
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)
@ -92,18 +114,43 @@ 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<Float, Float>?>(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()
) {
viewModelScope
mutableStateOf<Pair<Float, Float>?>(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<UUID> {
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>( workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args( "patching", PatcherWorker.Args(
input.selectedApp, input.selectedApp,
@ -111,9 +158,9 @@ class PatcherViewModel(
input.selectedPatches, input.selectedPatches,
input.options, input.options,
logger, logger,
downloadProgress, onDownloadProgress = { withContext(Dispatchers.Main) { downloadProgress = it } },
patchesProgress, onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
setInputFile = { inputFile = it }, setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
onProgress = { name, state, message -> onProgress = { name, state, message ->
viewModelScope.launch { viewModelScope.launch {
steps[currentStepIndex] = steps[currentStepIndex].run { steps[currentStepIndex] = steps[currentStepIndex].run {
@ -134,6 +181,7 @@ class PatcherViewModel(
} }
) )
) )
}
val patcherSucceeded = val patcherSucceeded =
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo -> workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
@ -172,7 +220,8 @@ class PatcherViewModel(
} }
} }
init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it. init {
// TODO: detect system-initiated process death during the patching process.
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply { ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION) addAction(InstallService.APP_INSTALL_ACTION)
}, ContextCompat.RECEIVER_NOT_EXPORTED) }, ContextCompat.RECEIVER_NOT_EXPORTED)
@ -278,8 +327,8 @@ class PatcherViewModel(
} }
} }
companion object { private companion object {
private const val TAG = "ReVanced Patcher" 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)
@ -288,11 +337,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<Float, Float>?>? = null
): List<Step> {
val needsDownload = selectedApp is SelectedApp.Download val needsDownload = selectedApp is SelectedApp.Download
return listOfNotNull( return listOfNotNull(
@ -300,7 +345,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

@ -10,6 +10,7 @@ import android.icu.text.CompactDecimalFormat
import android.os.Build 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
@ -24,6 +25,7 @@ import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
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
@ -42,6 +44,9 @@ import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException import java.time.format.DateTimeParseException
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?>>>
@ -156,9 +161,21 @@ fun String.relativeTime(context: Context): String {
return when { return when {
duration.toMinutes() < 1 -> context.getString(R.string.just_now) duration.toMinutes() < 1 -> context.getString(R.string.just_now)
duration.toMinutes() < 60 -> context.getString(R.string.minutes_ago, duration.toMinutes().toString()) duration.toMinutes() < 60 -> context.getString(
duration.toHours() < 24 -> context.getString(R.string.hours_ago, duration.toHours().toString()) R.string.minutes_ago,
duration.toDays() < 30 -> context.getString(R.string.days_ago, duration.toDays().toString()) duration.toMinutes().toString()
)
duration.toHours() < 24 -> context.getString(
R.string.hours_ago,
duration.toHours().toString()
)
duration.toDays() < 30 -> context.getString(
R.string.days_ago,
duration.toDays().toString()
)
else -> { else -> {
val formatter = DateTimeFormatter.ofPattern("MMM d") val formatter = DateTimeFormatter.ofPattern("MMM d")
val formattedDate = inputDateTime.format(formatter) val formattedDate = inputDateTime.format(formatter)
@ -219,3 +236,22 @@ fun ScrollState.isScrollingUp(): State<Boolean> {
val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
@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)
}