mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-10 01:01:56 +01:00
feat: improve patcher UI (#1494)
This commit is contained in:
parent
b7cb6b94f5
commit
3232bb10e6
23 changed files with 881 additions and 729 deletions
|
@ -105,6 +105,7 @@ dependencies {
|
|||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.preview)
|
||||
implementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.livedata)
|
||||
implementation(libs.compose.material.icons.extended)
|
||||
implementation(libs.compose.material3)
|
||||
|
|
|
@ -20,7 +20,7 @@ import app.revanced.manager.ui.destination.SettingsDestination
|
|||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||
import app.revanced.manager.ui.screen.DashboardScreen
|
||||
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
|
||||
import app.revanced.manager.ui.screen.InstallerScreen
|
||||
import app.revanced.manager.ui.screen.PatcherScreen
|
||||
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||
import app.revanced.manager.ui.screen.SettingsScreen
|
||||
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
||||
|
@ -157,7 +157,7 @@ class MainActivity : ComponentActivity() {
|
|||
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
|
||||
onPatchClick = { app, patches, options ->
|
||||
navController.navigate(
|
||||
Destination.Installer(
|
||||
Destination.Patcher(
|
||||
app, patches, options
|
||||
)
|
||||
)
|
||||
|
@ -173,7 +173,7 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
)
|
||||
|
||||
is Destination.Installer -> InstallerScreen(
|
||||
is Destination.Patcher -> PatcherScreen(
|
||||
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
||||
vm = getComposeViewModel { parametersOf(destination) }
|
||||
)
|
||||
|
|
|
@ -13,7 +13,7 @@ val viewModelModule = module {
|
|||
viewModelOf(::AdvancedSettingsViewModel)
|
||||
viewModelOf(::AppSelectorViewModel)
|
||||
viewModelOf(::VersionSelectorViewModel)
|
||||
viewModelOf(::InstallerViewModel)
|
||||
viewModelOf(::PatcherViewModel)
|
||||
viewModelOf(::UpdateViewModel)
|
||||
viewModelOf(::ChangelogsViewModel)
|
||||
viewModelOf(::ImportExportViewModel)
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
package app.revanced.manager.patcher
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.library.ApkUtils
|
||||
import app.revanced.manager.ui.viewmodel.ManagerLogger
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.logger.ManagerLogger
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
@ -21,10 +25,15 @@ class Session(
|
|||
frameworkDir: String,
|
||||
aaptPath: String,
|
||||
multithreadingDexFileWriter: Boolean,
|
||||
private val androidContext: Context,
|
||||
private val logger: ManagerLogger,
|
||||
private val input: File,
|
||||
private val onStepSucceeded: suspend () -> Unit
|
||||
private val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||
) : Closeable {
|
||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
onProgress(name, state, message)
|
||||
|
||||
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||
private val patcher = Patcher(
|
||||
PatcherOptions(
|
||||
|
@ -36,22 +45,56 @@ class Session(
|
|||
)
|
||||
)
|
||||
|
||||
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
||||
var nextPatchIndex = 0
|
||||
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.applying_patch, selectedPatches[nextPatchIndex]),
|
||||
state = State.RUNNING
|
||||
)
|
||||
|
||||
private suspend fun Patcher.applyPatchesVerbose() {
|
||||
this.apply(true).collect { (patch, exception) ->
|
||||
if (exception == null) {
|
||||
logger.info("$patch succeeded")
|
||||
onStepSucceeded()
|
||||
return@collect
|
||||
if (patch !in selectedPatches) return@collect
|
||||
|
||||
if (exception != null) {
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.failed_to_apply_patch, patch.name),
|
||||
state = State.FAILED,
|
||||
message = exception.stackTraceToString()
|
||||
)
|
||||
|
||||
logger.error("${patch.name} failed:")
|
||||
logger.error(exception.stackTraceToString())
|
||||
throw exception
|
||||
}
|
||||
logger.error("$patch failed:")
|
||||
logger.error(exception.stackTraceToString())
|
||||
throw exception
|
||||
|
||||
nextPatchIndex++
|
||||
|
||||
patchesProgress.value.let {
|
||||
patchesProgress.emit(it.copy(it.first + 1))
|
||||
}
|
||||
|
||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.applying_patch, nextPatch.name)
|
||||
)
|
||||
}
|
||||
|
||||
logger.info("${patch.name} succeeded")
|
||||
}
|
||||
|
||||
updateProgress(
|
||||
state = State.COMPLETED,
|
||||
name = androidContext.resources.getQuantityString(
|
||||
R.plurals.patches_applied,
|
||||
selectedPatches.size,
|
||||
selectedPatches.size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun run(output: File, selectedPatches: PatchList, integrations: List<File>) {
|
||||
onStepSucceeded() // Unpacking
|
||||
updateProgress(state = State.COMPLETED) // Unpacking
|
||||
Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
|
@ -64,10 +107,10 @@ class Session(
|
|||
logger.info("Merging integrations")
|
||||
acceptIntegrations(integrations)
|
||||
acceptPatches(selectedPatches)
|
||||
onStepSucceeded() // Merging
|
||||
updateProgress(state = State.COMPLETED) // Merging
|
||||
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose()
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
}
|
||||
|
||||
logger.info("Writing patched files...")
|
||||
|
@ -81,7 +124,7 @@ class Session(
|
|||
withContext(Dispatchers.IO) {
|
||||
Files.move(aligned.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
onStepSucceeded() // Saving
|
||||
updateProgress(state = State.COMPLETED) // Saving
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
@ -90,7 +133,7 @@ class Session(
|
|||
}
|
||||
|
||||
companion object {
|
||||
operator fun PatchResult.component1() = patch.name
|
||||
operator fun PatchResult.component1() = patch
|
||||
operator fun PatchResult.component2() = exception
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package app.revanced.manager.patcher.logger
|
||||
|
||||
import android.util.Log
|
||||
import java.util.logging.Handler
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
class ManagerLogger : Handler() {
|
||||
private val logs = mutableListOf<Pair<LogLevel, String>>()
|
||||
private fun log(level: LogLevel, msg: String) {
|
||||
level.androidLog(msg)
|
||||
if (level == LogLevel.TRACE) return
|
||||
logs.add(level to msg)
|
||||
}
|
||||
|
||||
fun export() =
|
||||
logs.asSequence().map { (level, msg) -> "[${level.name}]: $msg" }.joinToString("\n")
|
||||
|
||||
fun trace(msg: String) = log(LogLevel.TRACE, msg)
|
||||
fun info(msg: String) = log(LogLevel.INFO, msg)
|
||||
fun warn(msg: String) = log(LogLevel.WARN, msg)
|
||||
fun error(msg: String) = log(LogLevel.ERROR, msg)
|
||||
override fun publish(record: LogRecord) {
|
||||
val msg = record.message
|
||||
val fn = when (record.level) {
|
||||
Level.INFO -> ::info
|
||||
Level.SEVERE -> ::error
|
||||
Level.WARNING -> ::warn
|
||||
else -> ::trace
|
||||
}
|
||||
|
||||
fn(msg)
|
||||
}
|
||||
|
||||
override fun flush() = Unit
|
||||
|
||||
override fun close() = Unit
|
||||
}
|
||||
|
||||
enum class LogLevel {
|
||||
TRACE {
|
||||
override fun androidLog(msg: String) = Log.v(androidTag, msg)
|
||||
},
|
||||
INFO {
|
||||
override fun androidLog(msg: String) = Log.i(androidTag, msg)
|
||||
},
|
||||
WARN {
|
||||
override fun androidLog(msg: String) = Log.w(androidTag, msg)
|
||||
},
|
||||
ERROR {
|
||||
override fun androidLog(msg: String) = Log.e(androidTag, msg)
|
||||
};
|
||||
|
||||
abstract fun androidLog(msg: String): Int
|
||||
|
||||
private companion object {
|
||||
const val androidTag = "ReVanced Patcher"
|
||||
}
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
package app.revanced.manager.patcher.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
enum class State {
|
||||
WAITING, COMPLETED, FAILED
|
||||
}
|
||||
|
||||
class SubStep(
|
||||
val name: String,
|
||||
val state: State = State.WAITING,
|
||||
val message: String? = null,
|
||||
val progress: StateFlow<Pair<Float, Float>?>? = null
|
||||
)
|
||||
|
||||
class Step(
|
||||
@StringRes val name: Int,
|
||||
val subSteps: ImmutableList<SubStep>,
|
||||
val state: State = State.WAITING
|
||||
)
|
||||
|
||||
class PatcherProgressManager(
|
||||
context: Context,
|
||||
selectedPatches: List<String>,
|
||||
selectedApp: SelectedApp,
|
||||
downloadProgress: StateFlow<Pair<Float, Float>?>
|
||||
) {
|
||||
val steps = generateSteps(context, selectedPatches, selectedApp, downloadProgress)
|
||||
private var currentStep: StepKey? = StepKey(0, 0)
|
||||
|
||||
private fun update(key: StepKey, state: State, message: String? = null) {
|
||||
val isLastSubStep: Boolean
|
||||
steps[key.step] = steps[key.step].let { step ->
|
||||
isLastSubStep = key.substep == step.subSteps.lastIndex
|
||||
|
||||
val newStepState = when {
|
||||
// This step failed because one of its sub-steps failed.
|
||||
state == State.FAILED -> State.FAILED
|
||||
// All sub-steps succeeded.
|
||||
state == State.COMPLETED && isLastSubStep -> State.COMPLETED
|
||||
// Keep the old status.
|
||||
else -> step.state
|
||||
}
|
||||
|
||||
Step(step.name, step.subSteps.mapIndexed { index, subStep ->
|
||||
if (index != key.substep) subStep else SubStep(subStep.name, state, message)
|
||||
}.toImmutableList(), newStepState)
|
||||
}
|
||||
|
||||
val isFinal = isLastSubStep && key.step == steps.lastIndex
|
||||
|
||||
if (state == State.COMPLETED) {
|
||||
// Move the cursor to the next step.
|
||||
currentStep = when {
|
||||
isFinal -> null // Final step has been completed.
|
||||
isLastSubStep -> StepKey(key.step + 1, 0) // Move to the next step.
|
||||
else -> StepKey(
|
||||
key.step,
|
||||
key.substep + 1
|
||||
) // Move to the next sub-step.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun replacePatchesList(newList: List<String>) {
|
||||
steps[1] = generatePatchesStep(newList)
|
||||
}
|
||||
|
||||
private fun updateCurrent(newState: State, message: String? = null) {
|
||||
currentStep?.let { update(it, newState, message) }
|
||||
}
|
||||
|
||||
fun failure(error: Throwable) = updateCurrent(
|
||||
State.FAILED,
|
||||
error.stackTraceToString()
|
||||
)
|
||||
|
||||
fun success() = updateCurrent(State.COMPLETED)
|
||||
|
||||
fun getProgress(): List<Step> = steps
|
||||
|
||||
companion object {
|
||||
private fun generatePatchesStep(selectedPatches: List<String>) = Step(
|
||||
R.string.patcher_step_group_patching,
|
||||
selectedPatches.map { SubStep(it) }.toImmutableList()
|
||||
)
|
||||
|
||||
fun generateSteps(
|
||||
context: Context,
|
||||
selectedPatches: List<String>,
|
||||
selectedApp: SelectedApp,
|
||||
downloadProgress: StateFlow<Pair<Float, Float>?>? = null
|
||||
) = mutableListOf(
|
||||
Step(
|
||||
R.string.patcher_step_group_prepare,
|
||||
listOfNotNull(
|
||||
SubStep(context.getString(R.string.patcher_step_load_patches)),
|
||||
SubStep(
|
||||
"Download apk",
|
||||
progress = downloadProgress
|
||||
).takeIf { selectedApp is SelectedApp.Download },
|
||||
SubStep(context.getString(R.string.patcher_step_unpack)),
|
||||
SubStep(context.getString(R.string.patcher_step_integrations))
|
||||
).toImmutableList()
|
||||
),
|
||||
generatePatchesStep(selectedPatches),
|
||||
Step(
|
||||
R.string.patcher_step_group_saving,
|
||||
persistentListOf(
|
||||
SubStep(context.getString(R.string.patcher_step_write_patched)),
|
||||
SubStep(context.getString(R.string.patcher_step_sign_apk))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private data class StepKey(val step: Int, val substep: Int)
|
||||
}
|
|
@ -28,14 +28,13 @@ import app.revanced.manager.domain.worker.Worker
|
|||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.patcher.logger.ManagerLogger
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.viewmodel.ManagerLogger
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchesSelection
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
@ -47,7 +46,6 @@ class PatcherWorker(
|
|||
context: Context,
|
||||
parameters: WorkerParameters
|
||||
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
|
||||
|
||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||
private val workerRepository: WorkerRepository by inject()
|
||||
private val prefs: PreferencesManager by inject()
|
||||
|
@ -63,18 +61,15 @@ class PatcherWorker(
|
|||
val output: String,
|
||||
val selectedPatches: PatchesSelection,
|
||||
val options: Options,
|
||||
val progress: MutableStateFlow<ImmutableList<Step>>,
|
||||
val logger: ManagerLogger,
|
||||
val setInputFile: (File) -> Unit
|
||||
val downloadProgress: MutableStateFlow<Pair<Float, Float>?>,
|
||||
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
||||
val setInputFile: (File) -> Unit,
|
||||
val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||
) {
|
||||
val packageName get() = input.packageName
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val logPrefix = "[Worker]:"
|
||||
private fun String.logFmt() = "$logPrefix $this"
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo() =
|
||||
ForegroundInfo(
|
||||
1,
|
||||
|
@ -107,8 +102,6 @@ class PatcherWorker(
|
|||
return Result.failure()
|
||||
}
|
||||
|
||||
val args = workerRepository.claimInput(this)
|
||||
|
||||
try {
|
||||
// This does not always show up for some reason.
|
||||
setForeground(getForegroundInfo())
|
||||
|
@ -117,12 +110,13 @@ class PatcherWorker(
|
|||
}
|
||||
|
||||
val wakeLock: PowerManager.WakeLock =
|
||||
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply {
|
||||
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply {
|
||||
acquire(10 * 60 * 1000L)
|
||||
Log.d(tag, "Acquired wakelock.")
|
||||
}
|
||||
}
|
||||
|
||||
val args = workerRepository.claimInput(this)
|
||||
|
||||
return try {
|
||||
runPatcher(args)
|
||||
|
@ -132,38 +126,32 @@ class PatcherWorker(
|
|||
}
|
||||
|
||||
private suspend fun runPatcher(args: Args): Result {
|
||||
val aaptPath =
|
||||
Aapt.binary(applicationContext)?.absolutePath
|
||||
?: throw FileNotFoundException("Could not resolve aapt.")
|
||||
|
||||
val frameworkPath =
|
||||
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||
|
||||
val bundles = patchBundleRepository.bundles.first()
|
||||
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
|
||||
|
||||
val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(null)
|
||||
|
||||
val progressManager =
|
||||
PatcherProgressManager(
|
||||
applicationContext,
|
||||
args.selectedPatches.flatMap { it.value },
|
||||
args.input,
|
||||
downloadProgress
|
||||
)
|
||||
|
||||
val progressFlow = args.progress
|
||||
|
||||
fun updateProgress(advanceCounter: Boolean = true) {
|
||||
if (advanceCounter) {
|
||||
progressManager.success()
|
||||
}
|
||||
progressFlow.value = progressManager.getProgress().toImmutableList()
|
||||
}
|
||||
fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
args.onProgress(name, state, message)
|
||||
|
||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||
|
||||
return try {
|
||||
val bundles = patchBundleRepository.bundles.first()
|
||||
|
||||
// TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
|
||||
val selectedBundles = args.selectedPatches.keys
|
||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
||||
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
||||
|
||||
val selectedPatches = args.selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { selected.contains(it.name) }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
val aaptPath = Aapt.binary(applicationContext)?.absolutePath
|
||||
?: throw FileNotFoundException("Could not resolve aapt.")
|
||||
|
||||
val frameworkPath =
|
||||
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||
|
||||
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
|
||||
|
||||
if (args.input is SelectedApp.Installed) {
|
||||
installedAppRepository.get(args.packageName)?.let {
|
||||
|
@ -173,11 +161,6 @@ class PatcherWorker(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
|
||||
val selectedBundles = args.selectedPatches.keys
|
||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
||||
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
||||
|
||||
// Set all patch options.
|
||||
args.options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
|
@ -189,25 +172,17 @@ class PatcherWorker(
|
|||
}
|
||||
}
|
||||
|
||||
val patches = args.selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { selected.contains(it.name) }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
|
||||
// Ensure they are in the correct order so we can track progress properly.
|
||||
progressManager.replacePatchesList(patches.map { it.name.orEmpty() })
|
||||
updateProgress() // Loading patches
|
||||
updateProgress(state = State.COMPLETED) // Loading patches
|
||||
|
||||
val inputFile = when (val selectedApp = args.input) {
|
||||
is SelectedApp.Download -> {
|
||||
downloadedAppRepository.download(
|
||||
selectedApp.app,
|
||||
prefs.preferSplits.get(),
|
||||
onDownload = { downloadProgress.emit(it) }
|
||||
onDownload = { args.downloadProgress.emit(it) }
|
||||
).also {
|
||||
args.setInputFile(it)
|
||||
updateProgress() // Downloading
|
||||
updateProgress(state = State.COMPLETED) // Download APK
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,26 +195,35 @@ class PatcherWorker(
|
|||
frameworkPath,
|
||||
aaptPath,
|
||||
prefs.multithreadingDexFileWriter.get(),
|
||||
applicationContext,
|
||||
args.logger,
|
||||
inputFile,
|
||||
onStepSucceeded = ::updateProgress
|
||||
args.patchesProgress,
|
||||
args.onProgress
|
||||
).use { session ->
|
||||
session.run(patchedApk, patches, integrations)
|
||||
session.run(
|
||||
patchedApk,
|
||||
selectedPatches,
|
||||
integrations
|
||||
)
|
||||
}
|
||||
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
updateProgress() // Signing
|
||||
updateProgress(state = State.COMPLETED) // Signing
|
||||
|
||||
Log.i(tag, "Patching succeeded".logFmt())
|
||||
progressManager.success()
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Exception while patching".logFmt(), e)
|
||||
progressManager.failure(e)
|
||||
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
||||
Result.failure()
|
||||
} finally {
|
||||
updateProgress(false)
|
||||
patchedApk.delete()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val logPrefix = "[Worker]:"
|
||||
private fun String.logFmt() = "$logPrefix $this"
|
||||
}
|
||||
}
|
|
@ -13,17 +13,28 @@ import androidx.compose.ui.res.stringResource
|
|||
import app.revanced.manager.R
|
||||
|
||||
@Composable
|
||||
fun ArrowButton(modifier: Modifier = Modifier, expanded: Boolean,onClick: () -> Unit) {
|
||||
IconButton(onClick = onClick) {
|
||||
val description = if (expanded) R.string.collapse_content else R.string.expand_content
|
||||
val rotation by animateFloatAsState(targetValue = if (expanded) 0f else 180f, label = "rotation")
|
||||
fun ArrowButton(modifier: Modifier = Modifier, expanded: Boolean, onClick: (() -> Unit)?) {
|
||||
val description = if (expanded) R.string.collapse_content else R.string.expand_content
|
||||
val rotation by animateFloatAsState(
|
||||
targetValue = if (expanded) 0f else 180f,
|
||||
label = "rotation"
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||
contentDescription = stringResource(description),
|
||||
modifier = Modifier
|
||||
.rotate(rotation)
|
||||
.then(modifier)
|
||||
)
|
||||
}
|
||||
onClick?.let {
|
||||
IconButton(onClick = it) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||
contentDescription = stringResource(description),
|
||||
modifier = Modifier
|
||||
.rotate(rotation)
|
||||
.then(modifier)
|
||||
)
|
||||
}
|
||||
} ?: Icon(
|
||||
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||
contentDescription = stringResource(description),
|
||||
modifier = Modifier
|
||||
.rotate(rotation)
|
||||
.then(modifier)
|
||||
)
|
||||
}
|
|
@ -1,37 +1,37 @@
|
|||
package app.revanced.manager.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
@Composable
|
||||
fun LoadingIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
progress: Float? = null,
|
||||
text: String? = null
|
||||
progress: () -> Float? = { null },
|
||||
color: Color = ProgressIndicatorDefaults.circularColor,
|
||||
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
|
||||
trackColor: Color = ProgressIndicatorDefaults.circularTrackColor,
|
||||
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
text?.let { Text(text) }
|
||||
|
||||
progress?.let {
|
||||
CircularProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = Modifier.padding(vertical = 16.dp).then(modifier),
|
||||
)
|
||||
} ?:
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.padding(vertical = 16.dp).then(modifier)
|
||||
)
|
||||
}
|
||||
progress()?.let {
|
||||
CircularProgressIndicator(
|
||||
progress = { it },
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
strokeWidth = strokeWidth,
|
||||
trackColor = trackColor,
|
||||
strokeCap = strokeCap
|
||||
)
|
||||
} ?:
|
||||
CircularProgressIndicator(
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
strokeWidth = strokeWidth,
|
||||
trackColor = trackColor,
|
||||
strokeCap = strokeCap
|
||||
)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package app.revanced.manager.ui.component.patcher
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
|
||||
@Composable
|
||||
fun InstallPickerDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (InstallType) -> Unit
|
||||
) {
|
||||
var selectedInstallType by rememberSaveable { mutableStateOf(InstallType.DEFAULT) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
dismissButton = {
|
||||
Button(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onConfirm(selectedInstallType)
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.install_app))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.select_install_type)) },
|
||||
text = {
|
||||
Column {
|
||||
InstallType.values().forEach {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { selectedInstallType = it },
|
||||
leadingContent = {
|
||||
RadioButton(
|
||||
selected = selectedInstallType == it,
|
||||
onClick = null
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(it.stringResource)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
package app.revanced.manager.ui.component.patcher
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.Circle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
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.State
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import kotlin.math.floor
|
||||
|
||||
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
|
||||
@Composable
|
||||
fun Steps(
|
||||
category: StepCategory,
|
||||
steps: List<Step>,
|
||||
stepCount: Pair<Int, Int>? = null,
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
val categoryColor by animateColorAsState(
|
||||
if (expanded) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent,
|
||||
label = "category"
|
||||
)
|
||||
|
||||
val cardColor by animateColorAsState(
|
||||
if (expanded) MaterialTheme.colorScheme.surfaceContainer else Color.Transparent,
|
||||
label = "card"
|
||||
)
|
||||
|
||||
val state = remember(steps) {
|
||||
when {
|
||||
steps.all { it.state == State.COMPLETED } -> State.COMPLETED
|
||||
steps.any { it.state == State.FAILED } -> State.FAILED
|
||||
steps.any { it.state == State.RUNNING } -> State.RUNNING
|
||||
else -> State.WAITING
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.fillMaxWidth()
|
||||
.background(cardColor)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable { expanded = !expanded }
|
||||
.background(categoryColor)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
StepIcon(state = state, size = 24.dp)
|
||||
|
||||
Text(stringResource(category.displayName))
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
val stepProgress = remember(stepCount, steps) {
|
||||
stepCount?.let { (current, total) -> "$current/$total}" }
|
||||
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stepProgress,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded, onClick = null)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
steps.forEach { step ->
|
||||
val downloadProgress = step.downloadProgress?.collectAsStateWithLifecycle()
|
||||
|
||||
SubStep(
|
||||
name = step.name,
|
||||
state = step.state,
|
||||
message = step.message,
|
||||
downloadProgress = downloadProgress?.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SubStep(
|
||||
name: String,
|
||||
state: State,
|
||||
message: String? = null,
|
||||
downloadProgress: Pair<Float, Float>? = null
|
||||
) {
|
||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.run {
|
||||
if (message != null)
|
||||
clickable { messageExpanded = !messageExpanded }
|
||||
else this
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
StepIcon(state, downloadProgress, size = 20.dp)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, true),
|
||||
)
|
||||
|
||||
if (message != null) {
|
||||
Box(
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ArrowButton(
|
||||
modifier = Modifier.size(20.dp),
|
||||
expanded = messageExpanded,
|
||||
onClick = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
downloadProgress?.let { (current, total) ->
|
||||
Text(
|
||||
"$current/$total MB",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = messageExpanded && message != null) {
|
||||
Text(
|
||||
text = message.orEmpty(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.padding(horizontal = 52.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StepIcon(state: State, progress: Pair<Float, Float>? = null, size: Dp) {
|
||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||
|
||||
when (state) {
|
||||
State.COMPLETED -> Icon(
|
||||
Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(R.string.step_completed),
|
||||
tint = MaterialTheme.colorScheme.surfaceTint,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.FAILED -> Icon(
|
||||
Icons.Filled.Cancel,
|
||||
contentDescription = stringResource(R.string.step_failed),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.WAITING -> Icon(
|
||||
Icons.Outlined.Circle,
|
||||
contentDescription = stringResource(R.string.step_waiting),
|
||||
tint = MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.RUNNING ->
|
||||
LoadingIndicator(
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
progress = { progress?.let { (current, total) -> current / total } },
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
}
|
||||
}
|
|
@ -29,6 +29,6 @@ sealed interface Destination : Parcelable {
|
|||
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
||||
|
||||
@Parcelize
|
||||
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
|
||||
data class Patcher(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package app.revanced.manager.ui.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import app.revanced.manager.R
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
enum class StepCategory(@StringRes val displayName: Int) {
|
||||
PREPARING(R.string.patcher_step_group_preparing),
|
||||
PATCHING(R.string.patcher_step_group_patching),
|
||||
SAVING(R.string.patcher_step_group_saving)
|
||||
}
|
||||
|
||||
enum class State {
|
||||
WAITING, RUNNING, FAILED, COMPLETED
|
||||
}
|
||||
|
||||
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
|
||||
)
|
|
@ -13,7 +13,7 @@ sealed class SelectedApp : Parcelable {
|
|||
data class Download(override val packageName: String, override val version: String, val app: AppDownloader.App) : SelectedApp()
|
||||
|
||||
@Parcelize
|
||||
data class Local(override val packageName: String, override val version: String, val file: File, val shouldDelete: Boolean) : SelectedApp()
|
||||
data class Local(override val packageName: String, override val version: String, val file: File, val temporary: Boolean) : SelectedApp()
|
||||
|
||||
@Parcelize
|
||||
data class Installed(override val packageName: String, override val version: String) : SelectedApp()
|
||||
|
|
|
@ -164,7 +164,8 @@ fun AppSelectorScreen(
|
|||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(paddingValues),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
ListItem(
|
||||
|
|
|
@ -1,305 +0,0 @@
|
|||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.FileDownload
|
||||
import androidx.compose.material.icons.outlined.PostAdd
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
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.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.patcher.worker.State
|
||||
import app.revanced.manager.patcher.worker.Step
|
||||
import app.revanced.manager.ui.component.AppScaffold
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ArrowButton
|
||||
import app.revanced.manager.ui.viewmodel.InstallerViewModel
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
import kotlin.math.floor
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun InstallerScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: InstallerViewModel
|
||||
) {
|
||||
BackHandler(onBack = onBackClick)
|
||||
|
||||
val context = LocalContext.current
|
||||
val exportApkLauncher =
|
||||
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
|
||||
val patcherState by vm.patcherState.observeAsState(null)
|
||||
val steps by vm.progress.collectAsStateWithLifecycle()
|
||||
val canInstall by remember { derivedStateOf { patcherState == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
||||
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showInstallPicker)
|
||||
InstallPicker(
|
||||
onDismiss = { showInstallPicker = false },
|
||||
onConfirm = { vm.install(it) }
|
||||
)
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.installer),
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
AnimatedVisibility(patcherState != null) {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
if (canInstall) {
|
||||
IconButton(onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }) {
|
||||
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { vm.exportLogs(context) }) {
|
||||
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (canInstall) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(vm.appButtonText)) },
|
||||
icon = { Icon(Icons.Outlined.FileDownload, stringResource(id = R.string.install_app)) },
|
||||
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
|
||||
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
|
||||
onClick = {
|
||||
if (vm.installedPackageName == null)
|
||||
showInstallPicker = true
|
||||
else
|
||||
vm.open()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize()
|
||||
) {
|
||||
steps.forEach {
|
||||
InstallStep(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InstallPicker(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (InstallType) -> Unit
|
||||
) {
|
||||
var selectedInstallType by rememberSaveable { mutableStateOf(InstallType.DEFAULT) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
dismissButton = {
|
||||
Button(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onConfirm(selectedInstallType)
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.install_app))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.select_install_type)) },
|
||||
text = {
|
||||
Column {
|
||||
InstallType.values().forEach {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { selectedInstallType = it },
|
||||
leadingContent = {
|
||||
RadioButton(
|
||||
selected = selectedInstallType == it,
|
||||
onClick = null
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(it.stringResource)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
|
||||
|
||||
@Composable
|
||||
fun InstallStep(step: Step) {
|
||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.run {
|
||||
if (expanded) {
|
||||
background(MaterialTheme.colorScheme.secondaryContainer)
|
||||
} else this
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp)
|
||||
.background(if (expanded) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
StepIcon(step.state, size = 24.dp)
|
||||
|
||||
Text(text = stringResource(step.name), style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded) {
|
||||
expanded = !expanded
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background.copy(0.6f))
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
step.subSteps.forEach { subStep ->
|
||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||
val stacktrace = subStep.message
|
||||
val downloadProgress = subStep.progress?.collectAsStateWithLifecycle()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
StepIcon(subStep.state, downloadProgress?.value, size = 24.dp)
|
||||
|
||||
Text(
|
||||
text = subStep.name,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, true),
|
||||
)
|
||||
|
||||
if (stacktrace != null) {
|
||||
ArrowButton(
|
||||
modifier = Modifier.size(24.dp),
|
||||
expanded = messageExpanded
|
||||
) {
|
||||
messageExpanded = !messageExpanded
|
||||
}
|
||||
} else {
|
||||
downloadProgress?.value?.let { (downloaded, total) ->
|
||||
Text(
|
||||
"$downloaded/$total MB",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = messageExpanded && stacktrace != null) {
|
||||
Text(
|
||||
text = stacktrace ?: "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StepIcon(status: State, downloadProgress: Pair<Float, Float>? = null, size: Dp) {
|
||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||
|
||||
when (status) {
|
||||
State.COMPLETED -> Icon(
|
||||
Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(R.string.step_completed),
|
||||
tint = MaterialTheme.colorScheme.surfaceTint,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.FAILED -> Icon(
|
||||
Icons.Filled.Cancel,
|
||||
contentDescription = stringResource(R.string.step_failed),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.WAITING ->
|
||||
downloadProgress?.let { (downloaded, total) ->
|
||||
CircularProgressIndicator(
|
||||
progress = { downloaded / total },
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
} ?: CircularProgressIndicator(
|
||||
strokeWidth = strokeWidth,
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.FileDownload
|
||||
import androidx.compose.material.icons.outlined.PostAdd
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PatcherScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: PatcherViewModel
|
||||
) {
|
||||
BackHandler(onBack = onBackClick)
|
||||
|
||||
val context = LocalContext.current
|
||||
val exportApkLauncher =
|
||||
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
|
||||
|
||||
val patcherSucceeded by vm.patcherSucceeded.observeAsState(null)
|
||||
val canInstall by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
||||
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val steps by remember {
|
||||
derivedStateOf {
|
||||
vm.steps.groupBy { it.category }
|
||||
}
|
||||
}
|
||||
|
||||
val patchesProgress by vm.patchesProgress.collectAsStateWithLifecycle()
|
||||
|
||||
val progress = remember(vm.steps, patchesProgress) {
|
||||
val current = vm.steps.filter {
|
||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
||||
}.size + patchesProgress.first
|
||||
|
||||
val total = vm.steps.size - 1 + patchesProgress.second
|
||||
|
||||
current.toFloat() / total.toFloat()
|
||||
}
|
||||
|
||||
if (showInstallPicker)
|
||||
InstallPickerDialog(
|
||||
onDismiss = { showInstallPicker = false },
|
||||
onConfirm = vm::install
|
||||
)
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.patcher),
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
||||
enabled = canInstall
|
||||
) {
|
||||
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
||||
}
|
||||
IconButton(
|
||||
onClick = { vm.exportLogs(context) },
|
||||
enabled = patcherSucceeded != null
|
||||
) {
|
||||
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(visible = canInstall) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app)
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
vm.installedPackageName?.let {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.OpenInNew,
|
||||
stringResource(R.string.open_app)
|
||||
)
|
||||
} ?: Icon(
|
||||
Icons.Outlined.FileDownload,
|
||||
stringResource(R.string.install_app)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (vm.installedPackageName == null)
|
||||
showInstallPicker = true
|
||||
else vm.open()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
items(
|
||||
items = steps.toList(),
|
||||
key = { it.first }
|
||||
) { (category, steps) ->
|
||||
Steps(
|
||||
category = category,
|
||||
steps = steps,
|
||||
stepCount = if (category == StepCategory.PATCHING) patchesProgress else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package app.revanced.manager.ui.screen
|
|||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
@ -82,7 +83,8 @@ fun VersionSelectorScreen(
|
|||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
viewModel.installedApp?.let { (packageInfo, installedApp) ->
|
||||
SelectedApp.Installed(
|
||||
|
@ -101,7 +103,9 @@ fun VersionSelectorScreen(
|
|||
}
|
||||
}
|
||||
|
||||
GroupHeader(stringResource(R.string.downloadable_versions))
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
GroupHeader(stringResource(R.string.downloadable_versions))
|
||||
}
|
||||
|
||||
list.forEach {
|
||||
SelectedAppItem(
|
||||
|
|
|
@ -31,7 +31,7 @@ class AppSelectorViewModel(
|
|||
packageName = packageInfo.packageName,
|
||||
version = packageInfo.versionName,
|
||||
file = this,
|
||||
shouldDelete = true
|
||||
temporary = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@ 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.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
|
@ -26,22 +26,21 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp
|
|||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.patcher.worker.PatcherProgressManager
|
||||
import app.revanced.manager.patcher.logger.ManagerLogger
|
||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||
import app.revanced.manager.patcher.worker.Step
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
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.util.PM
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
@ -49,12 +48,10 @@ import org.koin.core.component.inject
|
|||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
@Stable
|
||||
class InstallerViewModel(
|
||||
private val input: Destination.Installer
|
||||
class PatcherViewModel(
|
||||
private val input: Destination.Patcher
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val app: Application by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
|
@ -63,62 +60,62 @@ class InstallerViewModel(
|
|||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
val packageName: String = input.selectedApp.packageName
|
||||
var installedPackageName by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var isInstalling by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private val tempDir = fs.tempDir.resolve("installer").also {
|
||||
it.deleteRecursively()
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
private val outputFile = tempDir.resolve("output.apk")
|
||||
private var inputFile: File? = null
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
var isInstalling by mutableStateOf(false)
|
||||
private set
|
||||
var installedPackageName by mutableStateOf<String?>(null)
|
||||
private set
|
||||
val appButtonText by derivedStateOf { if (installedPackageName == null) R.string.install_app else R.string.open_app }
|
||||
private val outputFile = tempDir.resolve("output.apk")
|
||||
|
||||
private val workManager = WorkManager.getInstance(app)
|
||||
|
||||
private val _progress: MutableStateFlow<ImmutableList<Step>>
|
||||
private val patcherWorkerId: UUID
|
||||
private val logger = ManagerLogger()
|
||||
|
||||
init {
|
||||
// TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it.
|
||||
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 var currentStepIndex = 0
|
||||
|
||||
viewModelScope.launch {
|
||||
installedApp = installedAppRepository.get(packageName)
|
||||
}
|
||||
private val patcherWorkerId: UUID =
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.selectedApp,
|
||||
outputFile.path,
|
||||
input.selectedPatches,
|
||||
input.options,
|
||||
logger,
|
||||
downloadProgress,
|
||||
patchesProgress,
|
||||
setInputFile = { inputFile = it },
|
||||
onProgress = { name, state, message ->
|
||||
steps[currentStepIndex] = steps[currentStepIndex].run {
|
||||
copy(
|
||||
name = name ?: this.name,
|
||||
state = state ?: this.state,
|
||||
message = message ?: this.message
|
||||
)
|
||||
}
|
||||
|
||||
val (selectedApp, patches, options) = input
|
||||
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
|
||||
currentStepIndex++
|
||||
|
||||
_progress = MutableStateFlow(
|
||||
PatcherProgressManager.generateSteps(
|
||||
app,
|
||||
patches.flatMap { (_, selected) -> selected },
|
||||
selectedApp
|
||||
).toImmutableList()
|
||||
steps[currentStepIndex] = steps[currentStepIndex].copy(state = State.RUNNING)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
patcherWorkerId =
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
selectedApp,
|
||||
outputFile.path,
|
||||
patches,
|
||||
options,
|
||||
_progress,
|
||||
logger,
|
||||
setInputFile = { inputFile = it }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val progress = _progress.asStateFlow()
|
||||
|
||||
val patcherState =
|
||||
val patcherSucceeded =
|
||||
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
|
||||
when (workInfo.state) {
|
||||
WorkInfo.State.SUCCEEDED -> true
|
||||
|
@ -151,29 +148,18 @@ class InstallerViewModel(
|
|||
app.toast(app.getString(R.string.install_app_fail, extra))
|
||||
}
|
||||
}
|
||||
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it.
|
||||
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
fun exportLogs(context: Context) {
|
||||
val sendIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, logger.export())
|
||||
type = "text/plain"
|
||||
viewModelScope.launch {
|
||||
installedApp = installedAppRepository.get(packageName)
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
context.startActivity(shareIntent)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
@ -183,7 +169,7 @@ class InstallerViewModel(
|
|||
|
||||
when (val selectedApp = input.selectedApp) {
|
||||
is SelectedApp.Local -> {
|
||||
if (selectedApp.shouldDelete) selectedApp.file.delete()
|
||||
if (selectedApp.temporary) selectedApp.file.delete()
|
||||
}
|
||||
|
||||
is SelectedApp.Installed -> {
|
||||
|
@ -199,7 +185,7 @@ class InstallerViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
tempDir.deleteRecursively()
|
||||
|
@ -215,117 +201,103 @@ class InstallerViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun exportLogs(context: Context) {
|
||||
val sendIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, logger.export())
|
||||
type = "text/plain"
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
context.startActivity(shareIntent)
|
||||
}
|
||||
|
||||
fun open() = installedPackageName?.let(pm::launch)
|
||||
|
||||
fun install(installType: InstallType) = viewModelScope.launch {
|
||||
isInstalling = true
|
||||
try {
|
||||
isInstalling = true
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
pm.installApp(listOf(outputFile))
|
||||
}
|
||||
|
||||
InstallType.ROOT -> {
|
||||
installAsRoot()
|
||||
try {
|
||||
val label = with(pm) {
|
||||
getPackageInfo(outputFile)?.label()
|
||||
?: throw Exception("Failed to load application info")
|
||||
}
|
||||
|
||||
rootInstaller.install(
|
||||
outputFile,
|
||||
inputFile,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
label
|
||||
)
|
||||
|
||||
installedAppRepository.addOrUpdate(
|
||||
packageName,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
InstallType.ROOT,
|
||||
input.selectedPatches
|
||||
)
|
||||
|
||||
rootInstaller.mount(packageName)
|
||||
|
||||
installedPackageName = packageName
|
||||
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to install as root", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
try {
|
||||
rootInstaller.uninstall(packageName)
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
isInstalling = false
|
||||
}
|
||||
}
|
||||
|
||||
fun open() = installedPackageName?.let { pm.launch(it) }
|
||||
companion object {
|
||||
fun generateSteps(
|
||||
context: Context,
|
||||
selectedApp: SelectedApp,
|
||||
downloadProgress: StateFlow<Pair<Float, Float>?>? = null
|
||||
): List<Step> {
|
||||
return listOfNotNull(
|
||||
Step(
|
||||
context.getString(R.string.patcher_step_load_patches),
|
||||
StepCategory.PREPARING,
|
||||
state = State.RUNNING
|
||||
),
|
||||
Step(
|
||||
context.getString(R.string.download_apk),
|
||||
StepCategory.PREPARING,
|
||||
downloadProgress = downloadProgress
|
||||
).takeIf { selectedApp is SelectedApp.Download },
|
||||
Step(
|
||||
context.getString(R.string.patcher_step_unpack),
|
||||
StepCategory.PREPARING
|
||||
),
|
||||
Step(
|
||||
context.getString(R.string.patcher_step_integrations),
|
||||
StepCategory.PREPARING
|
||||
),
|
||||
|
||||
private suspend fun installAsRoot() {
|
||||
try {
|
||||
val label = with(pm) {
|
||||
getPackageInfo(outputFile)?.label()
|
||||
?: throw Exception("Failed to load application info")
|
||||
}
|
||||
Step(
|
||||
context.getString(R.string.apply_patches),
|
||||
StepCategory.PATCHING
|
||||
),
|
||||
|
||||
rootInstaller.install(
|
||||
outputFile,
|
||||
inputFile,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
label
|
||||
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
|
||||
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
|
||||
)
|
||||
|
||||
rootInstaller.mount(packageName)
|
||||
|
||||
installedApp?.let { installedAppRepository.delete(it) }
|
||||
|
||||
installedAppRepository.addOrUpdate(
|
||||
packageName,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
InstallType.ROOT,
|
||||
input.selectedPatches
|
||||
)
|
||||
|
||||
installedPackageName = packageName
|
||||
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to install as root", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
try {
|
||||
rootInstaller.uninstall(packageName)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move this to a better place
|
||||
class ManagerLogger : java.util.logging.Handler() {
|
||||
private val logs = mutableListOf<Pair<LogLevel, String>>()
|
||||
private fun log(level: LogLevel, msg: String) {
|
||||
level.androidLog(msg)
|
||||
if (level == LogLevel.TRACE) return
|
||||
logs.add(level to msg)
|
||||
}
|
||||
|
||||
fun export() =
|
||||
logs.asSequence().map { (level, msg) -> "[${level.name}]: $msg" }.joinToString("\n")
|
||||
|
||||
fun trace(msg: String) = log(LogLevel.TRACE, msg)
|
||||
fun info(msg: String) = log(LogLevel.INFO, msg)
|
||||
fun warn(msg: String) = log(LogLevel.WARN, msg)
|
||||
fun error(msg: String) = log(LogLevel.ERROR, msg)
|
||||
override fun publish(record: LogRecord) {
|
||||
val msg = record.message
|
||||
val fn = when (record.level) {
|
||||
Level.INFO -> ::info
|
||||
Level.SEVERE -> ::error
|
||||
Level.WARNING -> ::warn
|
||||
else -> ::trace
|
||||
}
|
||||
|
||||
fn(msg)
|
||||
}
|
||||
|
||||
override fun flush() = Unit
|
||||
|
||||
override fun close() = Unit
|
||||
}
|
||||
|
||||
enum class LogLevel {
|
||||
TRACE {
|
||||
override fun androidLog(msg: String) = Log.v(androidTag, msg)
|
||||
},
|
||||
INFO {
|
||||
override fun androidLog(msg: String) = Log.i(androidTag, msg)
|
||||
},
|
||||
WARN {
|
||||
override fun androidLog(msg: String) = Log.w(androidTag, msg)
|
||||
},
|
||||
ERROR {
|
||||
override fun androidLog(msg: String) = Log.e(androidTag, msg)
|
||||
};
|
||||
|
||||
abstract fun androidLog(msg: String): Int
|
||||
|
||||
private companion object {
|
||||
const val androidTag = "ReVanced Patcher"
|
||||
}
|
||||
}
|
|
@ -8,4 +8,8 @@
|
|||
<item quantity="one">%d applied patch</item>
|
||||
<item quantity="other">%d applied patches</item>
|
||||
</plurals>
|
||||
<plurals name="patches_applied">
|
||||
<item quantity="one">Applied %d patch</item>
|
||||
<item quantity="other">Applied %d patches</item>
|
||||
</plurals>
|
||||
</resources>
|
|
@ -99,10 +99,10 @@
|
|||
<string name="patch_options_clear_bundle_description">Resets patch options for all patches in a bundle</string>
|
||||
<string name="patch_options_clear_all">Clear all patch options</string>
|
||||
<string name="patch_options_clear_all_description">Resets all patch options</string>
|
||||
<string name="prefer_splits">Prefer split apks</string>
|
||||
<string name="prefer_splits_description">Prefer split apks instead of full apks</string>
|
||||
<string name="prefer_universal">Prefer universal apks</string>
|
||||
<string name="prefer_universal_description">Prefer universal instead of arch-specific apks</string>
|
||||
<string name="prefer_splits">Prefer split APK\'s</string>
|
||||
<string name="prefer_splits_description">Prefer split APK\'s instead of full APK\'s</string>
|
||||
<string name="prefer_universal">Prefer universal APK\'s</string>
|
||||
<string name="prefer_universal_description">Prefer universal instead of arch-specific APK\'s</string>
|
||||
|
||||
<string name="search_apps">Search apps…</string>
|
||||
<string name="loading_body">Loading…</string>
|
||||
|
@ -155,6 +155,7 @@
|
|||
<string name="continue_anyways">Continue anyways</string>
|
||||
<string name="download_another_version">Download another version</string>
|
||||
<string name="download_app">Download app</string>
|
||||
<string name="download_apk">Download APK</string>
|
||||
<string name="source_download_fail">Failed to download patch bundle: %s</string>
|
||||
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string>
|
||||
<string name="source_replace_integrations_fail">Failed to update integrations: %s</string>
|
||||
|
@ -179,7 +180,7 @@
|
|||
<string name="version_not_supported">Not all patches support this version (%s). Do you want to continue anyway?</string>
|
||||
<string name="download_application">Download application?</string>
|
||||
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string>
|
||||
<string name="failed_to_load_apk">Failed to load apk</string>
|
||||
<string name="failed_to_load_apk">Failed to load APK</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="not_installed">Not installed</string>
|
||||
<string name="installed">Installed</string>
|
||||
|
@ -233,23 +234,27 @@
|
|||
<string name="open_app">Open</string>
|
||||
<string name="save_apk">Save APK</string>
|
||||
<string name="save_apk_success">APK Saved</string>
|
||||
<string name="sign_fail">Failed to sign Apk: %s</string>
|
||||
<string name="sign_fail">Failed to sign APK: %s</string>
|
||||
<string name="save_logs">Save logs</string>
|
||||
<string name="select_install_type">Select installation type</string>
|
||||
|
||||
<string name="patcher_step_group_prepare">Preparation</string>
|
||||
<string name="patcher_step_group_preparing">Preparing</string>
|
||||
<string name="patcher_step_load_patches">Load patches</string>
|
||||
<string name="patcher_step_unpack">Unpack Apk</string>
|
||||
<string name="patcher_step_unpack">Unpack APK</string>
|
||||
<string name="patcher_step_integrations">Merge Integrations</string>
|
||||
<string name="patcher_step_group_patching">Patching</string>
|
||||
<string name="patcher_step_group_saving">Saving</string>
|
||||
<string name="patcher_step_write_patched">Write patched Apk</string>
|
||||
<string name="patcher_step_sign_apk">Sign Apk</string>
|
||||
<string name="patcher_step_write_patched">Write patched APK</string>
|
||||
<string name="patcher_step_sign_apk">Sign APK</string>
|
||||
<string name="patcher_notification_message">Patching in progress…</string>
|
||||
<string name="apply_patches">Apply patches</string>
|
||||
<string name="applying_patch">Applying %s</string>
|
||||
<string name="failed_to_apply_patch">Failed to apply %s</string>
|
||||
|
||||
<string name="step_completed">completed</string>
|
||||
<string name="step_failed">failed</string>
|
||||
<string name="step_running">running</string>
|
||||
<string name="step_waiting">waiting</string>
|
||||
|
||||
<string name="expand_content">expand</string>
|
||||
<string name="collapse_content">collapse</string>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[versions]
|
||||
ktx = "1.12.0"
|
||||
ui-tooling = "1.6.0-alpha08"
|
||||
viewmodel-lifecycle = "2.6.2"
|
||||
splash-screen = "1.0.1"
|
||||
compose-activity = "1.8.2"
|
||||
|
@ -43,6 +44,7 @@ preferences-datastore = { group = "androidx.datastore", name = "datastore-prefer
|
|||
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "ui-tooling" }
|
||||
compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
|
||||
compose-material3 = { group = "androidx.compose.material3", name = "material3", version = "1.2.0-beta01"}
|
||||
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
|
|
Loading…
Reference in a new issue