mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-30 05:54:26 +02:00
WIP
This commit is contained in:
parent
e992a99783
commit
0930b9fda7
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ActivityResultContract<String, Boolean>, String> {
|
||||
val contract = if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
|
||||
val contract =
|
||||
if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
|
||||
return contract to storagePermissionName
|
||||
}
|
||||
|
||||
fun hasStoragePermission() = if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(storagePermissionName) == PackageManager.PERMISSION_GRANTED
|
||||
fun hasStoragePermission() =
|
||||
if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(
|
||||
storagePermissionName
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
@ -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) =
|
||||
|
@ -20,7 +20,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: () -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) {
|
||||
val bundles = bundles()
|
||||
|
@ -66,7 +66,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: () -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) = coroutineScope {
|
||||
// Get the location of our own Apk.
|
||||
@ -123,7 +123,9 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
val eventHandler = object : IPatcherEvents.Stub() {
|
||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||
|
||||
override fun patchSucceeded() = onPatchCompleted()
|
||||
override fun patchSucceeded() {
|
||||
launch { onPatchCompleted() }
|
||||
}
|
||||
|
||||
override fun progress(name: String?, state: String?, msg: String?) =
|
||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
||||
|
@ -35,7 +35,7 @@ sealed class Runtime(context: Context) : KoinComponent {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: () -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
)
|
||||
}
|
@ -61,9 +61,9 @@ class PatcherWorker(
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: Options,
|
||||
val logger: Logger,
|
||||
val downloadProgress: MutableStateFlow<Pair<Float, Float>?>,
|
||||
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
||||
val setInputFile: (File) -> Unit,
|
||||
val onDownloadProgress: suspend (Pair<Float, Float>?) -> 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
|
||||
)
|
||||
|
||||
|
@ -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<Step>,
|
||||
stepCount: Pair<Int, Int>? = 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<Float, Float>? = 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<Float, Float>? = 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<Float, Float>? = null, size: Dp) {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
progress = { progress?.let { (current, total) -> current / total } },
|
||||
progress = { progress },
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
}
|
||||
|
@ -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<Float, Float>?
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Step(
|
||||
val name: String,
|
||||
val category: StepCategory,
|
||||
val state: State = State.WAITING,
|
||||
val message: String? = null,
|
||||
val downloadProgress: StateFlow<Pair<Float, Float>?>? = null
|
||||
)
|
||||
val progressKey: ProgressKey? = null
|
||||
) : Parcelable
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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 = {}
|
||||
|
@ -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<String?>(null)
|
||||
val packageName = input.selectedApp.packageName
|
||||
|
||||
var installedPackageName by savedStateHandle.saveable(
|
||||
key = "installedPackageName",
|
||||
// Force Kotlin to select the correct overload.
|
||||
stateSaver = autoSaver()
|
||||
) {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
private set
|
||||
var isInstalling by mutableStateOf(false)
|
||||
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
|
||||
|
||||
var 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<Pair<LogLevel, String>>()
|
||||
private val logs by savedStateHandle.saveable<MutableList<Pair<LogLevel, String>>> { mutableListOf() }
|
||||
private val logger = object : Logger() {
|
||||
override fun log(level: LogLevel, message: String) {
|
||||
level.androidLog(message)
|
||||
@ -92,18 +114,43 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size }))
|
||||
private val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(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<Pair<Float, Float>?>(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<UUID> {
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"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<Pair<Float, Float>?>? = null
|
||||
): List<Step> {
|
||||
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
|
||||
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),
|
||||
|
@ -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<Int, Set<String>>
|
||||
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
||||
@ -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)
|
||||
@ -219,3 +236,22 @@ fun ScrollState.isScrollingUp(): State<Boolean> {
|
||||
|
||||
val LazyListState.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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user