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

(refactor) Separate out installation steps #71

Merged
merged 8 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading