Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rewrite install steps & steps UI #63

Merged
merged 12 commits into from
Feb 7, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class ManagerApplication : Application() {
modules(module {
single { providePreferences() }
single { provideDownloadManager() }
single { providePathManager() }
})
}
}
Expand Down
8 changes: 6 additions & 2 deletions app/src/main/kotlin/com/aliucord/manager/di/Managers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package com.aliucord.manager.di

import android.app.Application
import android.content.Context
import com.aliucord.manager.manager.DownloadManager
import com.aliucord.manager.manager.PreferencesManager
import com.aliucord.manager.manager.*
import org.koin.core.scope.Scope

fun Scope.providePreferences(): PreferencesManager {
Expand All @@ -15,3 +14,8 @@ fun Scope.provideDownloadManager(): DownloadManager {
val application: Application = get()
return DownloadManager(application)
}

fun Scope.providePathManager(): PathManager {
val ctx: Context = get()
return PathManager(ctx)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.aliucord.manager.installer.steps

import com.aliucord.manager.installer.steps.base.Step
import com.aliucord.manager.installer.steps.download.*
import com.aliucord.manager.installer.steps.install.*
import com.aliucord.manager.installer.steps.patch.*
import com.aliucord.manager.installer.steps.prepare.DowngradeCheckStep
import com.aliucord.manager.installer.steps.prepare.FetchInfoStep
import kotlinx.collections.immutable.persistentListOf

/**
* Used for installing the old Kotlin Discord app.
*/
class KotlinInstallRunner : StepRunner() {
override val steps = persistentListOf<Step>(
// Prepare
FetchInfoStep(),
DowngradeCheckStep(),

// Download
DownloadDiscordStep(),
DownloadInjectorStep(),
DownloadAliuhookStep(),
DownloadKotlinStep(),

// Patch
CopyDependenciesStep(),
ReplaceIconStep(),
PatchManifestStep(),
AddInjectorStep(),
AddAliuhookStep(),

// Install
AlignmentStep(),
SigningStep(),
InstallStep(),
CleanupStep(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.aliucord.manager.installer.steps

import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import com.aliucord.manager.R

/**
* A group of steps that is shown under one section in the install UI.
* This has no functional impact.
*/
@Immutable
enum class StepGroup(
/**
* The UI name to display this group as
*/
@get:StringRes
val localizedName: Int,
) {
Prepare(R.string.install_group_prepare),
Download(R.string.install_group_download),
Patch(R.string.install_group_patch),
Install(R.string.install_group_install)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.aliucord.manager.installer.steps

import com.aliucord.manager.installer.steps.base.Step
import com.aliucord.manager.manager.PreferencesManager
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

/**
* The minimum time that is required to occur between step switches, to avoid
* quickly switching the step groups in the UI. (very disorienting)
* Larger delay leads to a perception that it's doing more work than it actually is.
*/
const val MINIMUM_STEP_DELAY: Long = 600L

abstract class StepRunner : KoinComponent {
private val preferences: PreferencesManager by inject()

abstract val steps: ImmutableList<Step>

/**
* 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> getStep(): T {
val step = steps.asSequence()
.filterIsInstance<T>()
.filter { it.state.isFinished }
.firstOrNull()

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

return step
}

suspend fun executeAll(): Throwable? {
for (step in steps) {
val error = step.executeCatching(this@StepRunner)
if (error != null) return error

// Skip minimum run time when in dev mode
if (!preferences.devMode && step.durationMs < MINIMUM_STEP_DELAY) {
delay(MINIMUM_STEP_DELAY - step.durationMs)
}
}

return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.aliucord.manager.installer.steps.base

import android.content.Context
import androidx.compose.runtime.Stable
import com.aliucord.manager.R
import com.aliucord.manager.installer.steps.StepGroup
import com.aliucord.manager.installer.steps.StepRunner
import com.aliucord.manager.manager.DownloadManager
import com.aliucord.manager.util.showToast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File

@Stable
abstract class DownloadStep : Step(), KoinComponent {
private val context: Context by inject()
private val downloads: DownloadManager by inject()

/**
* The remote url to download
*/
abstract val targetUrl: String

/**
* Target path to store the download in. If this file already exists,
* then the cached version is used and the step is marked as cancelled/skipped.
*/
abstract val targetFile: File

/**
* Verify that the download completely successfully without errors.
* @throws Throwable If verification fails.
*/
open suspend fun verify() {
if (!targetFile.exists())
throw Error("Downloaded file is missing!")

if (targetFile.length() <= 0)
throw Error("Downloaded file is empty!")
}

override val group = StepGroup.Download

override suspend fun execute(container: StepRunner) {
if (targetFile.exists()) {
if (targetFile.length() > 0) {
state = StepState.Skipped
return
}

targetFile.delete()
}

val result = downloads.download(targetUrl, targetFile) { newProgress ->
progress = newProgress ?: -1f
}

when (result) {
is DownloadManager.Result.Success -> {
try {
verify()
} catch (t: Throwable) {
withContext(Dispatchers.Main) {
context.showToast(R.string.installer_dl_verify_fail)
}

throw t
}
}

is DownloadManager.Result.Error -> {
withContext(Dispatchers.Main) {
context.showToast(result.localizedReason)
}

throw Error("Failed to download: ${result.debugReason}")
}

is DownloadManager.Result.Cancelled ->
state = StepState.Error
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.aliucord.manager.installer.steps.base

import androidx.annotation.StringRes
import androidx.compose.runtime.*
import com.aliucord.manager.installer.steps.StepGroup
import com.aliucord.manager.installer.steps.StepRunner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.time.measureTimedValue
import kotlin.math.roundToInt

/**
* A base install process step. Steps are single-use
*/
@Stable
abstract class Step {
/**
* The group this step belongs to.
*/
abstract val group: StepGroup

/**
* The UI name to display this step as
*/
@get:StringRes
abstract val localizedName: Int

/**
* Run the step's logic.
* It can be assumed that this is executed in the correct order after other steps.
*/
protected abstract suspend fun execute(container: StepRunner)

/**
* The current state of this step in the installation process.
*/
var state by mutableStateOf(StepState.Pending)
protected set

/**
* If the current state is [StepState.Running], then the progress of this step.
* If the progress isn't currently measurable, then this should be set to `-1`.
*/
var progress by mutableFloatStateOf(-1f)
protected set

/**
* The total execution time once this step has finished execution.
*/
// TODO: make this a live value
var durationMs by mutableIntStateOf(0)
private set

/**
* Thin wrapper over [execute] but handling errors.
* @return An exception if the step failed to execute.
*/
suspend fun executeCatching(container: StepRunner): Throwable? {
if (state != StepState.Pending)
throw IllegalStateException("Cannot execute a step that has already started")

state = StepState.Running

// Execute this steps logic while timing it
val (error, executionTimeMs) = measureTimedValue {
try {
withContext(Dispatchers.Default) {
execute(container)
}

if (state != StepState.Skipped)
state = StepState.Success

null
} catch (t: Throwable) {
state = StepState.Error
t
}
}

durationMs = executionTimeMs.roundToInt()
return error
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.aliucord.manager.installer.steps.base

enum class StepState {
Pending,
Running,
Success,
Error,
Skipped;

val isFinished: Boolean
get() = this == Success || this == Error || this == Skipped
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.aliucord.manager.installer.steps.download

import androidx.compose.runtime.Stable
import com.aliucord.manager.R
import com.aliucord.manager.domain.repository.AliucordMavenRepository
import com.aliucord.manager.installer.steps.StepRunner
import com.aliucord.manager.installer.steps.base.DownloadStep
import com.aliucord.manager.manager.PathManager
import com.aliucord.manager.network.utils.getOrThrow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

/**
* Download a packaged AAR of the latest Aliuhook build from the Aliucord maven.
*/
@Stable
class DownloadAliuhookStep : DownloadStep(), KoinComponent {
private val paths: PathManager by inject()
private val maven: AliucordMavenRepository by inject()

/**
* This is populated right before the download starts (ref: [execute])
*/
private lateinit var targetVersion: String

override val localizedName = R.string.install_step_dl_aliuhook
override val targetUrl get() = AliucordMavenRepository.getAliuhookUrl(targetVersion)
override val targetFile get() = paths.cachedAliuhookAAR(targetVersion)

override suspend fun execute(container: StepRunner) {
targetVersion = maven.getAliuhookVersion().getOrThrow()

super.execute(container)
}
}

Loading
Loading