From 0930b9fda7d7bd512645d384e43848d93daadc3f Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sat, 17 Aug 2024 21:25:13 +0200 Subject: [PATCH] WIP --- .../revanced/manager/ManagerApplication.kt | 36 ++++++ .../manager/data/platform/Filesystem.kt | 26 ++++- .../app/revanced/manager/patcher/Session.kt | 2 +- .../patcher/runtime/CoroutineRuntime.kt | 2 +- .../manager/patcher/runtime/ProcessRuntime.kt | 6 +- .../manager/patcher/runtime/Runtime.kt | 2 +- .../manager/patcher/worker/PatcherWorker.kt | 12 +- .../manager/ui/component/patcher/Steps.kt | 37 +++--- .../revanced/manager/ui/model/PatcherStep.kt | 15 ++- .../manager/ui/screen/PatcherScreen.kt | 23 +--- .../ui/viewmodel/AppSelectorViewModel.kt | 6 +- .../manager/ui/viewmodel/PatcherViewModel.kt | 107 +++++++++++++----- .../java/app/revanced/manager/util/Util.kt | 44 ++++++- 13 files changed, 227 insertions(+), 91 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index 66ab2483..8a37cbe2 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -1,9 +1,14 @@ package app.revanced.manager +import android.app.Activity import android.app.Application +import android.os.Bundle +import android.util.Log +import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.di.* import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.util.tag import kotlinx.coroutines.Dispatchers import coil.Coil import coil.ImageLoader @@ -23,6 +28,8 @@ class ManagerApplication : Application() { private val scope = MainScope() private val prefs: PreferencesManager by inject() private val patchBundleRepository: PatchBundleRepository by inject() + private val fs: Filesystem by inject() + override fun onCreate() { super.onCreate() @@ -65,5 +72,34 @@ class ManagerApplication : Application() { updateCheck() } } + registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { + private var firstActivityCreated = false + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + if (firstActivityCreated) return + firstActivityCreated = true + + // We do not want to call onFreshProcessStart() if there is state to restore. + // This can happen on system-initiated process death. + if (savedInstanceState == null) { + Log.d(tag, "Fresh process created") + onFreshProcessStart() + } else Log.d(tag, "System-initiated process death detected") + } + + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) {} + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + }) + } + + private fun onFreshProcessStart() { + fs.uiTempDir.apply { + deleteRecursively() + mkdirs() + } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt index 3afbe6e8..97087f3e 100644 --- a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt +++ b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt @@ -9,6 +9,8 @@ import android.os.Environment import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import app.revanced.manager.util.RequestManageStorageContract +import java.io.File +import java.nio.file.Path class Filesystem(private val app: Application) { val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here. @@ -17,21 +19,35 @@ class Filesystem(private val app: Application) { * A directory that gets cleared when the app restarts. * Do not store paths to this directory in a parcel. */ - val tempDir = app.getDir("ephemeral", Context.MODE_PRIVATE).apply { + val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply { deleteRecursively() mkdirs() } - fun externalFilesDir() = Environment.getExternalStorageDirectory().toPath() + /** + * A directory for storing temporary files related to UI. + * This is the same as [tempDir], but does not get cleared on system-initiated process death. + * Paths to this directory can be safely stored in parcels. + */ + val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE).apply { + mkdirs() + } + + fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath() private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - private val storagePermissionName = if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE + private val storagePermissionName = + if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE fun permissionContract(): Pair, String> { - val contract = if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission() + val contract = + if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission() return contract to storagePermissionName } - fun hasStoragePermission() = if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(storagePermissionName) == PackageManager.PERMISSION_GRANTED + fun hasStoragePermission() = + if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission( + storagePermissionName + ) == PackageManager.PERMISSION_GRANTED } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt index 4393794d..a50fd7f9 100644 --- a/app/src/main/java/app/revanced/manager/patcher/Session.kt +++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt @@ -26,7 +26,7 @@ class Session( private val androidContext: Context, private val logger: Logger, private val input: File, - private val onPatchCompleted: () -> Unit, + private val onPatchCompleted: suspend () -> Unit, private val onProgress: (name: String?, state: State?, message: String?) -> Unit ) : Closeable { private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) = diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt index e2aed2ee..59a4fd32 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt @@ -20,7 +20,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) { selectedPatches: PatchSelection, options: Options, logger: Logger, - onPatchCompleted: () -> Unit, + onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, ) { val bundles = bundles() diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt index 389d5201..103dbe07 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt @@ -66,7 +66,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { selectedPatches: PatchSelection, options: Options, logger: Logger, - onPatchCompleted: () -> Unit, + onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, ) = coroutineScope { // Get the location of our own Apk. @@ -123,7 +123,9 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { val eventHandler = object : IPatcherEvents.Stub() { override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg) - override fun patchSucceeded() = onPatchCompleted() + override fun patchSucceeded() { + launch { onPatchCompleted() } + } override fun progress(name: String?, state: String?, msg: String?) = onProgress(name, state?.let { enumValueOf(it) }, msg) diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt index fd39c3f3..e02a6f86 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt @@ -35,7 +35,7 @@ sealed class Runtime(context: Context) : KoinComponent { selectedPatches: PatchSelection, options: Options, logger: Logger, - onPatchCompleted: () -> Unit, + onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 0e779df7..17bca981 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -61,9 +61,9 @@ class PatcherWorker( val selectedPatches: PatchSelection, val options: Options, val logger: Logger, - val downloadProgress: MutableStateFlow?>, - val patchesProgress: MutableStateFlow>, - val setInputFile: (File) -> Unit, + val onDownloadProgress: suspend (Pair?) -> Unit, + val onPatchCompleted: suspend () -> Unit, + val setInputFile: suspend (File) -> Unit, val onProgress: ProgressEventHandler ) { val packageName get() = input.packageName @@ -146,7 +146,7 @@ class PatcherWorker( downloadedAppRepository.download( selectedApp.app, prefs.preferSplits.get(), - onDownload = { args.downloadProgress.emit(it) } + onDownload = args.onDownloadProgress ).also { args.setInputFile(it) updateProgress(state = State.COMPLETED) // Download APK @@ -170,11 +170,13 @@ class PatcherWorker( args.selectedPatches, args.options, args.logger, + /* onPatchCompleted = { args.patchesProgress.update { (completed, total) -> completed + 1 to total } - }, + },*/ + args.onPatchCompleted, args.onProgress ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt index 6840837b..20952168 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt @@ -36,13 +36,14 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.model.ProgressKey import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.StepCategory +import app.revanced.manager.ui.model.StepProgressProvider import kotlin.math.floor // 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, steps: List, stepCount: Pair? = null, + stepProgressProvider: StepProgressProvider ) { var expanded by rememberSaveable { mutableStateOf(true) } @@ -115,13 +117,17 @@ fun Steps( modifier = Modifier.fillMaxWidth() ) { steps.forEach { step -> - val downloadProgress = step.downloadProgress?.collectAsStateWithLifecycle() + val (progress, progressText) = when (step.progressKey) { + null -> null + ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) -> downloaded / total to "$downloaded/$total MB" } + } ?: (null to null) SubStep( name = step.name, state = step.state, message = step.message, - downloadProgress = downloadProgress?.value + progress = progress, + progressText = progressText ) } } @@ -134,7 +140,8 @@ fun SubStep( name: String, state: State, message: String? = null, - downloadProgress: Pair? = null + progress: Float? = null, + progressText: String? = null ) { var messageExpanded by rememberSaveable { mutableStateOf(true) } @@ -155,7 +162,7 @@ fun SubStep( modifier = Modifier.size(24.dp), contentAlignment = Alignment.Center ) { - StepIcon(state, downloadProgress, size = 20.dp) + StepIcon(state, progress, size = 20.dp) } Text( @@ -166,8 +173,8 @@ fun SubStep( modifier = Modifier.weight(1f, true), ) - if (message != null) { - Box( + when { + message != null -> Box( modifier = Modifier.size(24.dp), contentAlignment = Alignment.Center ) { @@ -177,13 +184,11 @@ fun SubStep( onClick = null ) } - } else { - downloadProgress?.let { (current, total) -> - Text( - "$current/$total MB", - style = MaterialTheme.typography.labelSmall - ) - } + + progressText != null -> Text( + progressText, + style = MaterialTheme.typography.labelSmall + ) } } @@ -199,7 +204,7 @@ fun SubStep( } @Composable -fun StepIcon(state: State, progress: Pair? = null, size: Dp) { +fun StepIcon(state: State, progress: Float? = null, size: Dp) { val strokeWidth = Dp(floor(size.value / 10) + 1) when (state) { @@ -233,7 +238,7 @@ fun StepIcon(state: State, progress: Pair? = null, size: Dp) { contentDescription = description } }, - progress = { progress?.let { (current, total) -> current / total } }, + progress = { progress }, strokeWidth = strokeWidth ) } diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt index 4c7fc417..e2f3ea2b 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt @@ -1,8 +1,10 @@ package app.revanced.manager.ui.model +import android.os.Parcelable import androidx.annotation.StringRes import app.revanced.manager.R import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize enum class StepCategory(@StringRes val displayName: Int) { PREPARING(R.string.patcher_step_group_preparing), @@ -14,10 +16,19 @@ enum class State { WAITING, RUNNING, FAILED, COMPLETED } +enum class ProgressKey { + DOWNLOAD +} + +interface StepProgressProvider { + val downloadProgress: Pair? +} + +@Parcelize data class Step( val name: String, val category: StepCategory, val state: State = State.WAITING, val message: String? = null, - val downloadProgress: StateFlow?>? = null -) \ No newline at end of file + val progressKey: ProgressKey? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 163dfbc6..6483b96e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -36,13 +36,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.Steps -import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.viewmodel.PatcherViewModel import app.revanced.manager.util.APK_MIMETYPE @@ -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) InstallPickerDialog( onDismiss = { showInstallPicker = false }, @@ -150,7 +132,7 @@ fun PatcherScreen( .fillMaxSize() ) { LinearProgressIndicator( - progress = { progress }, + progress = { vm.progress }, modifier = Modifier.fillMaxWidth() ) @@ -166,7 +148,8 @@ fun PatcherScreen( Steps( category = category, steps = steps, - stepCount = if (category == StepCategory.PATCHING) patchesProgress else null + stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null, + stepProgressProvider = vm ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt index 85cee8d1..f1941394 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.R +import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.PM @@ -23,11 +24,10 @@ import java.nio.file.Files class AppSelectorViewModel( private val app: Application, private val pm: PM, + fs: Filesystem, private val patchBundleRepository: PatchBundleRepository ) : ViewModel() { - private val inputFile = File(app.filesDir, "input.apk").also { - it.delete() - } + private val inputFile = File(fs.uiTempDir, "input.apk").also(File::delete) val appList = pm.appList var onStorageClick: (SelectedApp.Local) -> Unit = {} diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index e0995911..afd86052 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -9,14 +9,19 @@ import android.content.pm.PackageInstaller import android.net.Uri import android.util.Log import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.autoSaver import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList import androidx.core.content.ContextCompat +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.map import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable import androidx.work.WorkInfo import androidx.work.WorkManager import app.revanced.manager.R @@ -31,11 +36,15 @@ import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.service.InstallService 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.State import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.StepCategory +import app.revanced.manager.ui.model.StepProgressProvider import app.revanced.manager.util.PM +import app.revanced.manager.util.saveableVar +import app.revanced.manager.util.saver.snapshotStateListSaver import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.tag import app.revanced.manager.util.toast @@ -43,8 +52,6 @@ import app.revanced.manager.util.uiSafe import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.withContext @@ -55,32 +62,47 @@ import java.nio.file.Files import java.time.Duration import java.util.UUID + +// @SuppressLint("AutoboxingStateCreation") @Stable +@OptIn(SavedStateHandleSaveableApi::class) class PatcherViewModel( private val input: Destination.Patcher -) : ViewModel(), KoinComponent { +) : ViewModel(), KoinComponent, StepProgressProvider { private val app: Application by inject() private val fs: Filesystem by inject() private val pm: PM by inject() private val workerRepository: WorkerRepository by inject() private val installedAppRepository: InstalledAppRepository by inject() private val rootInstaller: RootInstaller by inject() + private val savedStateHandle: SavedStateHandle by inject() private var installedApp: InstalledApp? = null - val packageName: String = input.selectedApp.packageName - var installedPackageName by mutableStateOf(null) + val packageName = input.selectedApp.packageName + + var installedPackageName by savedStateHandle.saveable( + key = "installedPackageName", + // Force Kotlin to select the correct overload. + stateSaver = autoSaver() + ) { + mutableStateOf(null) + } private set - var isInstalling by mutableStateOf(false) + private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false } + + var isInstalling by mutableStateOf(ongoingPmSession) private set - private val tempDir = fs.tempDir.resolve("installer").also { - it.deleteRecursively() - it.mkdirs() + private val tempDir = savedStateHandle.saveable(key = "tempDir") { + fs.uiTempDir.resolve("installer").also { + it.deleteRecursively() + it.mkdirs() + } } - private var inputFile: File? = null + private var inputFile: File? by savedStateHandle.saveableVar() private val outputFile = tempDir.resolve("output.apk") - private val logs = mutableListOf>() + private val logs by savedStateHandle.saveable>> { mutableListOf() } private val logger = object : Logger() { override fun log(level: LogLevel, message: String) { level.androidLog(message) @@ -92,18 +114,43 @@ class PatcherViewModel( } } - val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size })) - private val downloadProgress = MutableStateFlow?>(null) - val steps = generateSteps( - app, - input.selectedApp, - downloadProgress - ).toMutableStateList() + private val patchCount = input.selectedPatches.values.sumOf { it.size } + private var completedPatchCount by savedStateHandle.saveable { + // SavedStateHandle.saveable only supports the boxed version. + @Suppress("AutoboxingStateCreation") mutableStateOf( + 0 + ) + } + val patchesProgress get() = completedPatchCount to patchCount + override var downloadProgress by savedStateHandle.saveable( + key = "downloadProgress", + stateSaver = autoSaver() + ) { + viewModelScope + mutableStateOf?>(null) + } + private set + val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { + generateSteps( + app, + input.selectedApp + ).toMutableStateList() + } private var currentStepIndex = 0 + val progress by derivedStateOf { + val current = steps.count { + it.state == State.COMPLETED && it.category != StepCategory.PATCHING + } + completedPatchCount + + val total = steps.size - 1 + patchCount + + current.toFloat() / total.toFloat() + } + private val workManager = WorkManager.getInstance(app) - private val patcherWorkerId: UUID = + private val patcherWorkerId by savedStateHandle.saveable { workerRepository.launchExpedited( "patching", PatcherWorker.Args( input.selectedApp, @@ -111,9 +158,9 @@ class PatcherViewModel( input.selectedPatches, input.options, logger, - downloadProgress, - patchesProgress, - setInputFile = { inputFile = it }, + onDownloadProgress = { withContext(Dispatchers.Main) { downloadProgress = it } }, + onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } }, + setInputFile = { withContext(Dispatchers.Main) { inputFile = it } }, onProgress = { name, state, message -> viewModelScope.launch { steps[currentStepIndex] = steps[currentStepIndex].run { @@ -134,6 +181,7 @@ class PatcherViewModel( } ) ) + } val patcherSucceeded = 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 { addAction(InstallService.APP_INSTALL_ACTION) }, ContextCompat.RECEIVER_NOT_EXPORTED) @@ -278,8 +327,8 @@ class PatcherViewModel( } } - companion object { - private const val TAG = "ReVanced Patcher" + private companion object { + const val TAG = "ReVanced Patcher" fun LogLevel.androidLog(msg: String) = when (this) { LogLevel.TRACE -> Log.v(TAG, msg) @@ -288,11 +337,7 @@ class PatcherViewModel( LogLevel.ERROR -> Log.e(TAG, msg) } - fun generateSteps( - context: Context, - selectedApp: SelectedApp, - downloadProgress: StateFlow?>? = null - ): List { + fun generateSteps(context: Context, selectedApp: SelectedApp): List { val needsDownload = selectedApp is SelectedApp.Download return listOfNotNull( @@ -300,7 +345,7 @@ class PatcherViewModel( context.getString(R.string.download_apk), StepCategory.PREPARING, state = State.RUNNING, - downloadProgress = downloadProgress, + progressKey = ProgressKey.DOWNLOAD, ).takeIf { needsDownload }, Step( context.getString(R.string.patcher_step_load_patches), diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index f1de38fd..ad292cfe 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -10,6 +10,7 @@ import android.icu.text.CompactDecimalFormat import android.os.Build import android.util.Log import android.widget.Toast +import androidx.annotation.MainThread import androidx.annotation.StringRes import androidx.compose.foundation.ScrollState import androidx.compose.foundation.lazy.LazyListState @@ -24,6 +25,7 @@ import androidx.compose.ui.graphics.Color import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import app.revanced.manager.R @@ -42,6 +44,9 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException import java.util.Locale +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty typealias PatchSelection = Map> typealias Options = Map>> @@ -156,9 +161,21 @@ fun String.relativeTime(context: Context): String { return when { duration.toMinutes() < 1 -> context.getString(R.string.just_now) - duration.toMinutes() < 60 -> context.getString(R.string.minutes_ago, 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()) + duration.toMinutes() < 60 -> context.getString( + R.string.minutes_ago, + 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 -> { val formatter = DateTimeFormatter.ofPattern("MMM d") val formattedDate = inputDateTime.format(formatter) @@ -218,4 +235,23 @@ fun ScrollState.isScrollingUp(): State { } val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value -val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value \ No newline at end of file +val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value + +@MainThread +fun SavedStateHandle.saveableVar(init: () -> T): PropertyDelegateProvider> = + PropertyDelegateProvider { _: Any?, property -> + val name = property.name + if (name !in this) this[name] = init() + object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T = get(name)!! + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = + set(name, value) + } + } + +fun SavedStateHandle.saveableVar(): ReadWriteProperty = + object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T? = get(property.name) + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) = + set(property.name, value) + } \ No newline at end of file