mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat: improve patcher UI (#1494)
This commit is contained in:
parent
b7cb6b94f5
commit
3232bb10e6
@ -105,6 +105,7 @@ dependencies {
|
|||||||
implementation(platform(libs.compose.bom))
|
implementation(platform(libs.compose.bom))
|
||||||
implementation(libs.compose.ui)
|
implementation(libs.compose.ui)
|
||||||
implementation(libs.compose.ui.preview)
|
implementation(libs.compose.ui.preview)
|
||||||
|
implementation(libs.compose.ui.tooling)
|
||||||
implementation(libs.compose.livedata)
|
implementation(libs.compose.livedata)
|
||||||
implementation(libs.compose.material.icons.extended)
|
implementation(libs.compose.material.icons.extended)
|
||||||
implementation(libs.compose.material3)
|
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.AppSelectorScreen
|
||||||
import app.revanced.manager.ui.screen.DashboardScreen
|
import app.revanced.manager.ui.screen.DashboardScreen
|
||||||
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
|
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.SelectedAppInfoScreen
|
||||||
import app.revanced.manager.ui.screen.SettingsScreen
|
import app.revanced.manager.ui.screen.SettingsScreen
|
||||||
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
||||||
@ -157,7 +157,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
|
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
|
||||||
onPatchClick = { app, patches, options ->
|
onPatchClick = { app, patches, options ->
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
Destination.Installer(
|
Destination.Patcher(
|
||||||
app, patches, options
|
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 } },
|
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
||||||
vm = getComposeViewModel { parametersOf(destination) }
|
vm = getComposeViewModel { parametersOf(destination) }
|
||||||
)
|
)
|
||||||
|
@ -13,7 +13,7 @@ val viewModelModule = module {
|
|||||||
viewModelOf(::AdvancedSettingsViewModel)
|
viewModelOf(::AdvancedSettingsViewModel)
|
||||||
viewModelOf(::AppSelectorViewModel)
|
viewModelOf(::AppSelectorViewModel)
|
||||||
viewModelOf(::VersionSelectorViewModel)
|
viewModelOf(::VersionSelectorViewModel)
|
||||||
viewModelOf(::InstallerViewModel)
|
viewModelOf(::PatcherViewModel)
|
||||||
viewModelOf(::UpdateViewModel)
|
viewModelOf(::UpdateViewModel)
|
||||||
viewModelOf(::ChangelogsViewModel)
|
viewModelOf(::ChangelogsViewModel)
|
||||||
viewModelOf(::ImportExportViewModel)
|
viewModelOf(::ImportExportViewModel)
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package app.revanced.manager.patcher
|
package app.revanced.manager.patcher
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import app.revanced.library.ApkUtils
|
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.Patcher
|
||||||
import app.revanced.patcher.PatcherOptions
|
import app.revanced.patcher.PatcherOptions
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.patch.PatchResult
|
import app.revanced.patcher.patch.PatchResult
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -21,10 +25,15 @@ class Session(
|
|||||||
frameworkDir: String,
|
frameworkDir: String,
|
||||||
aaptPath: String,
|
aaptPath: String,
|
||||||
multithreadingDexFileWriter: Boolean,
|
multithreadingDexFileWriter: Boolean,
|
||||||
|
private val androidContext: Context,
|
||||||
private val logger: ManagerLogger,
|
private val logger: ManagerLogger,
|
||||||
private val input: File,
|
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 {
|
) : 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 tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||||
private val patcher = Patcher(
|
private val patcher = Patcher(
|
||||||
PatcherOptions(
|
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) ->
|
this.apply(true).collect { (patch, exception) ->
|
||||||
if (exception == null) {
|
if (patch !in selectedPatches) return@collect
|
||||||
logger.info("$patch succeeded")
|
|
||||||
onStepSucceeded()
|
if (exception != null) {
|
||||||
return@collect
|
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())
|
nextPatchIndex++
|
||||||
throw exception
|
|
||||||
|
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>) {
|
suspend fun run(output: File, selectedPatches: PatchList, integrations: List<File>) {
|
||||||
onStepSucceeded() // Unpacking
|
updateProgress(state = State.COMPLETED) // Unpacking
|
||||||
Logger.getLogger("").apply {
|
Logger.getLogger("").apply {
|
||||||
handlers.forEach {
|
handlers.forEach {
|
||||||
it.close()
|
it.close()
|
||||||
@ -64,10 +107,10 @@ class Session(
|
|||||||
logger.info("Merging integrations")
|
logger.info("Merging integrations")
|
||||||
acceptIntegrations(integrations)
|
acceptIntegrations(integrations)
|
||||||
acceptPatches(selectedPatches)
|
acceptPatches(selectedPatches)
|
||||||
onStepSucceeded() // Merging
|
updateProgress(state = State.COMPLETED) // Merging
|
||||||
|
|
||||||
logger.info("Applying patches...")
|
logger.info("Applying patches...")
|
||||||
applyPatchesVerbose()
|
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Writing patched files...")
|
logger.info("Writing patched files...")
|
||||||
@ -81,7 +124,7 @@ class Session(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Files.move(aligned.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
Files.move(aligned.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
}
|
}
|
||||||
onStepSucceeded() // Saving
|
updateProgress(state = State.COMPLETED) // Saving
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
@ -90,7 +133,7 @@ class Session(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
operator fun PatchResult.component1() = patch.name
|
operator fun PatchResult.component1() = patch
|
||||||
operator fun PatchResult.component2() = exception
|
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.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.Session
|
||||||
import app.revanced.manager.patcher.aapt.Aapt
|
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.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.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchesSelection
|
import app.revanced.manager.util.PatchesSelection
|
||||||
import app.revanced.manager.util.tag
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -47,7 +46,6 @@ class PatcherWorker(
|
|||||||
context: Context,
|
context: Context,
|
||||||
parameters: WorkerParameters
|
parameters: WorkerParameters
|
||||||
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
|
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
|
||||||
|
|
||||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
private val workerRepository: WorkerRepository by inject()
|
private val workerRepository: WorkerRepository by inject()
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
@ -63,18 +61,15 @@ class PatcherWorker(
|
|||||||
val output: String,
|
val output: String,
|
||||||
val selectedPatches: PatchesSelection,
|
val selectedPatches: PatchesSelection,
|
||||||
val options: Options,
|
val options: Options,
|
||||||
val progress: MutableStateFlow<ImmutableList<Step>>,
|
|
||||||
val logger: ManagerLogger,
|
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
|
val packageName get() = input.packageName
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val logPrefix = "[Worker]:"
|
|
||||||
private fun String.logFmt() = "$logPrefix $this"
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getForegroundInfo() =
|
override suspend fun getForegroundInfo() =
|
||||||
ForegroundInfo(
|
ForegroundInfo(
|
||||||
1,
|
1,
|
||||||
@ -107,8 +102,6 @@ class PatcherWorker(
|
|||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
val args = workerRepository.claimInput(this)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This does not always show up for some reason.
|
// This does not always show up for some reason.
|
||||||
setForeground(getForegroundInfo())
|
setForeground(getForegroundInfo())
|
||||||
@ -117,12 +110,13 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val wakeLock: PowerManager.WakeLock =
|
val wakeLock: PowerManager.WakeLock =
|
||||||
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||||
newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply {
|
.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply {
|
||||||
acquire(10 * 60 * 1000L)
|
acquire(10 * 60 * 1000L)
|
||||||
Log.d(tag, "Acquired wakelock.")
|
Log.d(tag, "Acquired wakelock.")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
val args = workerRepository.claimInput(this)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
runPatcher(args)
|
runPatcher(args)
|
||||||
@ -132,38 +126,32 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runPatcher(args: Args): Result {
|
private suspend fun runPatcher(args: Args): Result {
|
||||||
val aaptPath =
|
|
||||||
Aapt.binary(applicationContext)?.absolutePath
|
|
||||||
?: throw FileNotFoundException("Could not resolve aapt.")
|
|
||||||
|
|
||||||
val frameworkPath =
|
fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||||
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
args.onProgress(name, state, message)
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||||
|
|
||||||
return try {
|
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) {
|
if (args.input is SelectedApp.Installed) {
|
||||||
installedAppRepository.get(args.packageName)?.let {
|
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.
|
// Set all patch options.
|
||||||
args.options.forEach { (bundle, bundlePatchOptions) ->
|
args.options.forEach { (bundle, bundlePatchOptions) ->
|
||||||
val patches = allPatches[bundle] ?: return@forEach
|
val patches = allPatches[bundle] ?: return@forEach
|
||||||
@ -189,25 +172,17 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val patches = args.selectedPatches.flatMap { (bundle, selected) ->
|
updateProgress(state = State.COMPLETED) // Loading patches
|
||||||
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
|
|
||||||
|
|
||||||
val inputFile = when (val selectedApp = args.input) {
|
val inputFile = when (val selectedApp = args.input) {
|
||||||
is SelectedApp.Download -> {
|
is SelectedApp.Download -> {
|
||||||
downloadedAppRepository.download(
|
downloadedAppRepository.download(
|
||||||
selectedApp.app,
|
selectedApp.app,
|
||||||
prefs.preferSplits.get(),
|
prefs.preferSplits.get(),
|
||||||
onDownload = { downloadProgress.emit(it) }
|
onDownload = { args.downloadProgress.emit(it) }
|
||||||
).also {
|
).also {
|
||||||
args.setInputFile(it)
|
args.setInputFile(it)
|
||||||
updateProgress() // Downloading
|
updateProgress(state = State.COMPLETED) // Download APK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,26 +195,35 @@ class PatcherWorker(
|
|||||||
frameworkPath,
|
frameworkPath,
|
||||||
aaptPath,
|
aaptPath,
|
||||||
prefs.multithreadingDexFileWriter.get(),
|
prefs.multithreadingDexFileWriter.get(),
|
||||||
|
applicationContext,
|
||||||
args.logger,
|
args.logger,
|
||||||
inputFile,
|
inputFile,
|
||||||
onStepSucceeded = ::updateProgress
|
args.patchesProgress,
|
||||||
|
args.onProgress
|
||||||
).use { session ->
|
).use { session ->
|
||||||
session.run(patchedApk, patches, integrations)
|
session.run(
|
||||||
|
patchedApk,
|
||||||
|
selectedPatches,
|
||||||
|
integrations
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
keystoreManager.sign(patchedApk, File(args.output))
|
keystoreManager.sign(patchedApk, File(args.output))
|
||||||
updateProgress() // Signing
|
updateProgress(state = State.COMPLETED) // Signing
|
||||||
|
|
||||||
Log.i(tag, "Patching succeeded".logFmt())
|
Log.i(tag, "Patching succeeded".logFmt())
|
||||||
progressManager.success()
|
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(tag, "Exception while patching".logFmt(), e)
|
Log.e(tag, "Exception while patching".logFmt(), e)
|
||||||
progressManager.failure(e)
|
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
updateProgress(false)
|
|
||||||
patchedApk.delete()
|
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
|
import app.revanced.manager.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArrowButton(modifier: Modifier = Modifier, expanded: Boolean,onClick: () -> Unit) {
|
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 description = if (expanded) R.string.collapse_content else R.string.expand_content
|
val rotation by animateFloatAsState(
|
||||||
val rotation by animateFloatAsState(targetValue = if (expanded) 0f else 180f, label = "rotation")
|
targetValue = if (expanded) 0f else 180f,
|
||||||
|
label = "rotation"
|
||||||
|
)
|
||||||
|
|
||||||
Icon(
|
onClick?.let {
|
||||||
imageVector = Icons.Filled.KeyboardArrowUp,
|
IconButton(onClick = it) {
|
||||||
contentDescription = stringResource(description),
|
Icon(
|
||||||
modifier = Modifier
|
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||||
.rotate(rotation)
|
contentDescription = stringResource(description),
|
||||||
.then(modifier)
|
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
|
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.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
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
|
@Composable
|
||||||
fun LoadingIndicator(
|
fun LoadingIndicator(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
progress: Float? = null,
|
progress: () -> Float? = { null },
|
||||||
text: String? = null
|
color: Color = ProgressIndicatorDefaults.circularColor,
|
||||||
|
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
|
||||||
|
trackColor: Color = ProgressIndicatorDefaults.circularTrackColor,
|
||||||
|
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
|
||||||
) {
|
) {
|
||||||
Column(
|
progress()?.let {
|
||||||
modifier = Modifier.fillMaxSize(),
|
CircularProgressIndicator(
|
||||||
verticalArrangement = Arrangement.Center,
|
progress = { it },
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
modifier = modifier,
|
||||||
) {
|
color = color,
|
||||||
text?.let { Text(text) }
|
strokeWidth = strokeWidth,
|
||||||
|
trackColor = trackColor,
|
||||||
progress?.let {
|
strokeCap = strokeCap
|
||||||
CircularProgressIndicator(
|
)
|
||||||
progress = { progress },
|
} ?:
|
||||||
modifier = Modifier.padding(vertical = 16.dp).then(modifier),
|
CircularProgressIndicator(
|
||||||
)
|
modifier = modifier,
|
||||||
} ?:
|
color = color,
|
||||||
CircularProgressIndicator(
|
strokeWidth = strokeWidth,
|
||||||
modifier = Modifier.padding(vertical = 16.dp).then(modifier)
|
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
|
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@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()
|
data class Download(override val packageName: String, override val version: String, val app: AppDownloader.App) : SelectedApp()
|
||||||
|
|
||||||
@Parcelize
|
@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
|
@Parcelize
|
||||||
data class Installed(override val packageName: String, override val version: String) : SelectedApp()
|
data class Installed(override val packageName: String, override val version: String) : SelectedApp()
|
||||||
|
@ -164,7 +164,8 @@ fun AppSelectorScreen(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
ListItem(
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@ -82,7 +83,8 @@ fun VersionSelectorScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
viewModel.installedApp?.let { (packageInfo, installedApp) ->
|
viewModel.installedApp?.let { (packageInfo, installedApp) ->
|
||||||
SelectedApp.Installed(
|
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 {
|
list.forEach {
|
||||||
SelectedAppItem(
|
SelectedAppItem(
|
||||||
|
@ -31,7 +31,7 @@ class AppSelectorViewModel(
|
|||||||
packageName = packageInfo.packageName,
|
packageName = packageInfo.packageName,
|
||||||
version = packageInfo.versionName,
|
version = packageInfo.versionName,
|
||||||
file = this,
|
file = this,
|
||||||
shouldDelete = true
|
temporary = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@ 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.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.map
|
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.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
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.PatcherWorker
|
||||||
import app.revanced.manager.patcher.worker.Step
|
|
||||||
import app.revanced.manager.service.InstallService
|
import app.revanced.manager.service.InstallService
|
||||||
import app.revanced.manager.service.UninstallService
|
|
||||||
import app.revanced.manager.ui.destination.Destination
|
import app.revanced.manager.ui.destination.Destination
|
||||||
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.Step
|
||||||
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
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
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -49,12 +48,10 @@ import org.koin.core.component.inject
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.logging.Level
|
|
||||||
import java.util.logging.LogRecord
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class InstallerViewModel(
|
class PatcherViewModel(
|
||||||
private val input: Destination.Installer
|
private val input: Destination.Patcher
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
@ -63,62 +60,62 @@ class InstallerViewModel(
|
|||||||
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 var installedApp: InstalledApp? = null
|
||||||
val packageName: String = input.selectedApp.packageName
|
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 {
|
private val tempDir = fs.tempDir.resolve("installer").also {
|
||||||
it.deleteRecursively()
|
it.deleteRecursively()
|
||||||
it.mkdirs()
|
it.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val outputFile = tempDir.resolve("output.apk")
|
|
||||||
private var inputFile: File? = null
|
private var inputFile: File? = null
|
||||||
|
private val outputFile = tempDir.resolve("output.apk")
|
||||||
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 workManager = WorkManager.getInstance(app)
|
private val workManager = WorkManager.getInstance(app)
|
||||||
|
|
||||||
private val _progress: MutableStateFlow<ImmutableList<Step>>
|
|
||||||
private val patcherWorkerId: UUID
|
|
||||||
private val logger = ManagerLogger()
|
private val logger = ManagerLogger()
|
||||||
|
|
||||||
init {
|
val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size }))
|
||||||
// TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it.
|
private val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(null)
|
||||||
|
val steps = generateSteps(
|
||||||
|
app,
|
||||||
|
input.selectedApp,
|
||||||
|
downloadProgress
|
||||||
|
).toMutableStateList()
|
||||||
|
private var currentStepIndex = 0
|
||||||
|
|
||||||
viewModelScope.launch {
|
private val patcherWorkerId: UUID =
|
||||||
installedApp = installedAppRepository.get(packageName)
|
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(
|
steps[currentStepIndex] = steps[currentStepIndex].copy(state = State.RUNNING)
|
||||||
PatcherProgressManager.generateSteps(
|
}
|
||||||
app,
|
}
|
||||||
patches.flatMap { (_, selected) -> selected },
|
)
|
||||||
selectedApp
|
|
||||||
).toImmutableList()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
patcherWorkerId =
|
val patcherSucceeded =
|
||||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
|
||||||
"patching", PatcherWorker.Args(
|
|
||||||
selectedApp,
|
|
||||||
outputFile.path,
|
|
||||||
patches,
|
|
||||||
options,
|
|
||||||
_progress,
|
|
||||||
logger,
|
|
||||||
setInputFile = { inputFile = it }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val progress = _progress.asStateFlow()
|
|
||||||
|
|
||||||
val patcherState =
|
|
||||||
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
|
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
|
||||||
when (workInfo.state) {
|
when (workInfo.state) {
|
||||||
WorkInfo.State.SUCCEEDED -> true
|
WorkInfo.State.SUCCEEDED -> true
|
||||||
@ -151,29 +148,18 @@ class InstallerViewModel(
|
|||||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
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 {
|
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
||||||
addAction(InstallService.APP_INSTALL_ACTION)
|
addAction(InstallService.APP_INSTALL_ACTION)
|
||||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
|
||||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
}
|
|
||||||
|
|
||||||
fun exportLogs(context: Context) {
|
viewModelScope.launch {
|
||||||
val sendIntent: Intent = Intent().apply {
|
installedApp = installedAppRepository.get(packageName)
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
putExtra(Intent.EXTRA_TEXT, logger.export())
|
|
||||||
type = "text/plain"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
|
||||||
context.startActivity(shareIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
@ -183,7 +169,7 @@ class InstallerViewModel(
|
|||||||
|
|
||||||
when (val selectedApp = input.selectedApp) {
|
when (val selectedApp = input.selectedApp) {
|
||||||
is SelectedApp.Local -> {
|
is SelectedApp.Local -> {
|
||||||
if (selectedApp.shouldDelete) selectedApp.file.delete()
|
if (selectedApp.temporary) selectedApp.file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Installed -> {
|
is SelectedApp.Installed -> {
|
||||||
@ -199,7 +185,7 @@ class InstallerViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
tempDir.deleteRecursively()
|
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 {
|
fun install(installType: InstallType) = viewModelScope.launch {
|
||||||
isInstalling = true
|
|
||||||
try {
|
try {
|
||||||
|
isInstalling = true
|
||||||
when (installType) {
|
when (installType) {
|
||||||
InstallType.DEFAULT -> {
|
InstallType.DEFAULT -> {
|
||||||
pm.installApp(listOf(outputFile))
|
pm.installApp(listOf(outputFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
InstallType.ROOT -> {
|
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 {
|
} finally {
|
||||||
isInstalling = false
|
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() {
|
Step(
|
||||||
try {
|
context.getString(R.string.apply_patches),
|
||||||
val label = with(pm) {
|
StepCategory.PATCHING
|
||||||
getPackageInfo(outputFile)?.label()
|
),
|
||||||
?: throw Exception("Failed to load application info")
|
|
||||||
}
|
|
||||||
|
|
||||||
rootInstaller.install(
|
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
|
||||||
outputFile,
|
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
|
||||||
inputFile,
|
|
||||||
packageName,
|
|
||||||
input.selectedApp.version,
|
|
||||||
label
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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="one">%d applied patch</item>
|
||||||
<item quantity="other">%d applied patches</item>
|
<item quantity="other">%d applied patches</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="patches_applied">
|
||||||
|
<item quantity="one">Applied %d patch</item>
|
||||||
|
<item quantity="other">Applied %d patches</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</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_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">Clear all patch options</string>
|
||||||
<string name="patch_options_clear_all_description">Resets 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">Prefer split APK\'s</string>
|
||||||
<string name="prefer_splits_description">Prefer split apks instead of full apks</string>
|
<string name="prefer_splits_description">Prefer split APK\'s instead of full APK\'s</string>
|
||||||
<string name="prefer_universal">Prefer universal apks</string>
|
<string name="prefer_universal">Prefer universal APK\'s</string>
|
||||||
<string name="prefer_universal_description">Prefer universal instead of arch-specific apks</string>
|
<string name="prefer_universal_description">Prefer universal instead of arch-specific APK\'s</string>
|
||||||
|
|
||||||
<string name="search_apps">Search apps…</string>
|
<string name="search_apps">Search apps…</string>
|
||||||
<string name="loading_body">Loading…</string>
|
<string name="loading_body">Loading…</string>
|
||||||
@ -155,6 +155,7 @@
|
|||||||
<string name="continue_anyways">Continue anyways</string>
|
<string name="continue_anyways">Continue anyways</string>
|
||||||
<string name="download_another_version">Download another version</string>
|
<string name="download_another_version">Download another version</string>
|
||||||
<string name="download_app">Download app</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_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_fail">Failed to load updated patch bundle: %s</string>
|
||||||
<string name="source_replace_integrations_fail">Failed to update integrations: %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="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="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="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="loading">Loading…</string>
|
||||||
<string name="not_installed">Not installed</string>
|
<string name="not_installed">Not installed</string>
|
||||||
<string name="installed">Installed</string>
|
<string name="installed">Installed</string>
|
||||||
@ -233,23 +234,27 @@
|
|||||||
<string name="open_app">Open</string>
|
<string name="open_app">Open</string>
|
||||||
<string name="save_apk">Save APK</string>
|
<string name="save_apk">Save APK</string>
|
||||||
<string name="save_apk_success">APK Saved</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="save_logs">Save logs</string>
|
||||||
<string name="select_install_type">Select installation type</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_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_integrations">Merge Integrations</string>
|
||||||
<string name="patcher_step_group_patching">Patching</string>
|
<string name="patcher_step_group_patching">Patching</string>
|
||||||
<string name="patcher_step_group_saving">Saving</string>
|
<string name="patcher_step_group_saving">Saving</string>
|
||||||
<string name="patcher_step_write_patched">Write patched Apk</string>
|
<string name="patcher_step_write_patched">Write patched APK</string>
|
||||||
<string name="patcher_step_sign_apk">Sign Apk</string>
|
<string name="patcher_step_sign_apk">Sign APK</string>
|
||||||
<string name="patcher_notification_message">Patching in progress…</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_completed">completed</string>
|
||||||
<string name="step_failed">failed</string>
|
<string name="step_failed">failed</string>
|
||||||
<string name="step_running">running</string>
|
<string name="step_running">running</string>
|
||||||
|
<string name="step_waiting">waiting</string>
|
||||||
|
|
||||||
<string name="expand_content">expand</string>
|
<string name="expand_content">expand</string>
|
||||||
<string name="collapse_content">collapse</string>
|
<string name="collapse_content">collapse</string>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
ktx = "1.12.0"
|
ktx = "1.12.0"
|
||||||
|
ui-tooling = "1.6.0-alpha08"
|
||||||
viewmodel-lifecycle = "2.6.2"
|
viewmodel-lifecycle = "2.6.2"
|
||||||
splash-screen = "1.0.1"
|
splash-screen = "1.0.1"
|
||||||
compose-activity = "1.8.2"
|
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-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||||
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
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-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
|
||||||
compose-material3 = { group = "androidx.compose.material3", name = "material3", version = "1.2.0-beta01"}
|
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" }
|
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user