mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-30 14:04:26 +02:00
WIP
This commit is contained in:
parent
e992a99783
commit
0930b9fda7
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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) =
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 = {}
|
||||||
|
@ -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),
|
||||||
|
@ -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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user