Skip to content
This repository has been archived by the owner on Jul 7, 2024. It is now read-only.

Commit

Permalink
(refactor) Separate out installation steps (#71)
Browse files Browse the repository at this point in the history
* Move most download steps

* Add most patching steps

* Add logger to new step runner

* Add last steps

* Add debug information to new runner

* Implement the new installer

* Add documentation

* Document installer components
  • Loading branch information
wingio authored Feb 1, 2024
1 parent dc55ab7 commit 1a53388
Show file tree
Hide file tree
Showing 25 changed files with 1,089 additions and 535 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ dependencies {

implementation(files("libs/lspatch.aar"))
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.collections)
implementation(libs.zip.android) {
artifact {
type = "aar"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class DownloadManager(
}

sealed interface DownloadResult {
object Success : DownloadResult
data object Success : DownloadResult
data class Cancelled(val systemTriggered: Boolean) : DownloadResult
data class Error(val debugReason: String) : DownloadResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package dev.beefers.vendetta.manager.installer.step

import androidx.annotation.StringRes
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import org.koin.core.component.KoinComponent
import kotlin.time.measureTimedValue

/**
* A distinct step to be ran while patching
*/
@Stable
abstract class Step: KoinComponent {

/**
* Group this step belongs to
*/
abstract val group: StepGroup

/**
* Label used in the installer ui
*/
@get:StringRes
abstract val nameRes: Int

/**
* Current status for this step
*/
var status by mutableStateOf(StepStatus.QUEUED)
protected set

/**
* How much progress this step has made, use null if unknown
*/
var progress by mutableStateOf<Float?>(null)
protected set

/**
* How long this step took to run, in milliseconds
*/
var durationMs by mutableIntStateOf(0)
private set

/**
* Runs this step
*
* @param runner The host runner, used to share information between steps
*/
protected abstract suspend fun run(runner: StepRunner)

/**
* Safely runs this step, catching any errors and timing how long it runs.
*
* @param runner The host runner, used to share information between steps
*/
suspend fun runCatching(runner: StepRunner): Throwable? {
if (status != StepStatus.QUEUED)
throw IllegalStateException("Cannot execute a step that has already started")

status = StepStatus.ONGOING

val (error, time) = measureTimedValue {
try {
run(runner)
status = StepStatus.SUCCESSFUL
null
} catch (t: Throwable) {
status = StepStatus.UNSUCCESSFUL
t
}
}

durationMs = time.inWholeMilliseconds.toInt()
return error
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.beefers.vendetta.manager.installer.step

import androidx.annotation.StringRes
import dev.beefers.vendetta.manager.R

/**
* Represents a group of [Step]s
*/
enum class StepGroup(@StringRes val nameRes: Int) {
/**
* All steps deal with downloading files remotely
*/
DL(R.string.group_download),

/**
* Steps that modify the APKs
*/
PATCHING(R.string.group_patch),

/**
* Only contains the [install step][dev.beefers.vendetta.manager.installer.step.installing.InstallStep]
*/
INSTALLING(R.string.group_installing)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package dev.beefers.vendetta.manager.installer.step

import android.content.Context
import android.os.Build
import android.os.Environment
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import dev.beefers.vendetta.manager.BuildConfig
import dev.beefers.vendetta.manager.domain.manager.PreferenceManager
import dev.beefers.vendetta.manager.installer.step.download.DownloadBaseStep
import dev.beefers.vendetta.manager.installer.step.download.DownloadLangStep
import dev.beefers.vendetta.manager.installer.step.download.DownloadLibsStep
import dev.beefers.vendetta.manager.installer.step.download.DownloadResourcesStep
import dev.beefers.vendetta.manager.installer.step.download.DownloadVendettaStep
import dev.beefers.vendetta.manager.installer.step.installing.InstallStep
import dev.beefers.vendetta.manager.installer.step.patching.AddVendettaStep
import dev.beefers.vendetta.manager.installer.step.patching.PatchManifestsStep
import dev.beefers.vendetta.manager.installer.step.patching.PresignApksStep
import dev.beefers.vendetta.manager.installer.step.patching.ReplaceIconStep
import dev.beefers.vendetta.manager.installer.util.Logger
import dev.beefers.vendetta.manager.utils.DiscordVersion
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File

/**
* Runs all installation steps in order
*
* Credit to rushii (github.com/rushiiMachine)
*
* @param discordVersion Version of Discord to inject Vendetta into
*/
@Stable
class StepRunner(
private val discordVersion: DiscordVersion
): KoinComponent {

private val preferenceManager: PreferenceManager by inject()
private val context: Context by inject()
private val debugInfo = """
Vendetta Manager v${BuildConfig.VERSION_NAME}
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} ${if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes Present)" else ""}
Running Android ${Build.VERSION.RELEASE}, API level ${Build.VERSION.SDK_INT}
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
Device: ${Build.MANUFACTURER} - ${Build.MODEL} (${Build.DEVICE})
${if(Build.VERSION.SDK_INT > Build.VERSION_CODES.S) "SOC: ${Build.SOC_MANUFACTURER} ${Build.SOC_MODEL}\n" else "\n\n"}
Adding Vendetta to Discord v$discordVersion
""".trimIndent()

/**
* Logger associated with this runner
*/
val logger = Logger("StepRunner").also {
debugInfo.split("\n").forEach(it.logs::add) // Add debug information to logs but don't print to logcat
}

/**
* Root directory for all downloaded files
*/
private val cacheDir =
context.externalCacheDir
?: File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS)
.resolve("VendettaManager")
.also { it.mkdirs() }

/**
* Where version specific downloads are persisted
*/
private val discordCacheDir = cacheDir.resolve(discordVersion.toVersionCode())

/**
* Working directory where apks are directly modified (i.e. replacing the app icon)
*/
private val patchedDir = discordCacheDir.resolve("patched").also { it.deleteRecursively() }

/**
* Where apks are moved to once signed
*/
private val signedDir = discordCacheDir.resolve("signed").also { it.deleteRecursively() }

/**
* Output directory for LSPatch
*/
private val lspatchedDir = patchedDir.resolve("lspatched").also { it.deleteRecursively() }

var currentStep by mutableStateOf<Step?>(null)
private set

/**
* Whether or not the patching/installation process has completed.
* Note that this does not mean all steps were finished successfully
*/
var completed by mutableStateOf<Boolean>(false)
private set

/**
* List of steps to go through for this install
*
* ORDER MATTERS
*/
val steps: ImmutableList<Step> = buildList {
// Downloading
add(DownloadBaseStep(discordCacheDir, patchedDir, discordVersion.toVersionCode()))
add(DownloadLibsStep(discordCacheDir, patchedDir, discordVersion.toVersionCode()))
add(DownloadLangStep(discordCacheDir, patchedDir, discordVersion.toVersionCode()))
add(DownloadResourcesStep(discordCacheDir, patchedDir, discordVersion.toVersionCode()))
add(DownloadVendettaStep(patchedDir))

// Patching
if (preferenceManager.patchIcon) add(ReplaceIconStep())
add(PatchManifestsStep())
add(PresignApksStep(signedDir))
add(AddVendettaStep(signedDir, lspatchedDir))

// Installing
add(InstallStep(lspatchedDir))
}.toImmutableList()

/**
* Get a step that has already been successfully executed.
* This is used to retrieve previously executed dependency steps from a later step.
*/
inline fun <reified T : Step> getCompletedStep(): T {
val step = steps.asSequence()
.filterIsInstance<T>()
.filter { it.status == StepStatus.SUCCESSFUL }
.firstOrNull()

if (step == null) {
throw IllegalArgumentException("No completed step ${T::class.simpleName} exists in container")
}

return step
}

/**
* Clears all cached files
*/
fun clearCache() {
cacheDir.deleteRecursively()
}

/**
* Run all the [steps] in order
*/
suspend fun runAll(): Throwable? {
for (step in steps) {
if (completed) return null // Failsafe in case runner is incorrectly marked as not completed too early

currentStep = step
val error = step.runCatching(this)
if (error != null) {
logger.e("Failed on ${step::class.simpleName}", error)

completed = true
return error
}

// Add delay for human psychology and
// better group visibility in UI (the active group can change way too fast)
if (!preferenceManager.isDeveloper && step.durationMs < 1000) {
delay(1000L - step.durationMs)
}
}

completed = true
return null
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.beefers.vendetta.manager.installer.step

enum class StepStatus {
/**
* Currently in progress
*/
ONGOING,

/**
* Completed with no errors
*/
SUCCESSFUL,

/**
* Completed with an error
*/
UNSUCCESSFUL,

/**
* Has not yet been ran
*/
QUEUED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.beefers.vendetta.manager.installer.step.download

import androidx.compose.runtime.Stable
import dev.beefers.vendetta.manager.R
import dev.beefers.vendetta.manager.installer.step.download.base.DownloadStep
import java.io.File

/**
* Downloads the base Discord APK
*/
@Stable
class DownloadBaseStep(
dir: File,
workingDir: File,
version: String
): DownloadStep() {

override val nameRes = R.string.step_dl_base

override val url: String = "$baseUrl/tracker/download/$version/base"
override val destination = dir.resolve("base-$version.apk")
override val workingCopy = workingDir.resolve("base-$version.apk")

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.beefers.vendetta.manager.installer.step.download

import androidx.compose.runtime.Stable
import dev.beefers.vendetta.manager.R
import dev.beefers.vendetta.manager.installer.step.download.base.DownloadStep
import java.io.File

/**
* Downloads the languages split, will always be English because Discord doesn't store their strings in this split
*/
@Stable
class DownloadLangStep(
dir: File,
workingDir: File,
version: String
): DownloadStep() {

override val nameRes = R.string.step_dl_lang

override val url: String = "$baseUrl/tracker/download/$version/config.en"
override val destination = dir.resolve("config.en-$version.apk")
override val workingCopy = workingDir.resolve("config.en-$version.apk")

}
Loading

0 comments on commit 1a53388

Please sign in to comment.