From 8ed6e41c0b96dbdd695640685c76717166a8b30f Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Fri, 19 Jan 2024 18:02:51 -0800 Subject: [PATCH 1/9] wip: rewrite patching process --- .../aliucord/manager/ManagerApplication.kt | 1 + .../com/aliucord/manager/di/Managers.kt | 8 +- .../installer/steps/KtStepContainer.kt | 19 ++ .../manager/installer/steps/StepContainer.kt | 48 ++++ .../manager/installer/steps/StepGroup.kt | 23 ++ .../installer/steps/base/DownloadStep.kt | 85 +++++++ .../manager/installer/steps/base/Step.kt | 77 +++++++ .../manager/installer/steps/base/StepState.kt | 10 + .../steps/download/DownloadAliuhookStep.kt | 36 +++ .../steps/download/DownloadDiscordStep.kt | 39 ++++ .../steps/download/DownloadInjectorStep.kt | 45 ++++ .../steps/download/DownloadKotlinStep.kt | 27 +++ .../installer/steps/patch/ReplaceIconStep.kt | 22 ++ .../installer/steps/prepare/FetchInfoStep.kt | 29 +++ .../manager/manager/DownloadManager.kt | 50 ++-- .../aliucord/manager/manager/PathManager.kt | 56 +++++ .../network/service/AliucordGithubService.kt | 11 +- .../components/dialogs/InstallAbortDialog.kt | 52 +++++ .../ui/components/dialogs/InstallerDialog.kt | 16 +- .../manager/ui/screens/about/AboutScreen.kt | 2 +- .../manager/ui/screens/home/HomeScreen.kt | 22 +- .../ui/screens/install/InstallModel.kt | 139 +---------- .../ui/screens/install/InstallScreen.kt | 48 ++-- .../ui/screens/plugins/PluginsScreen.kt | 135 ++++++----- .../ui/screens/settings/SettingsScreen.kt | 215 +++++++++--------- app/src/main/res/values/strings.xml | 12 +- 26 files changed, 854 insertions(+), 373 deletions(-) create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/KtStepContainer.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/StepGroup.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadAliuhookStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadDiscordStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadInjectorStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadKotlinStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallAbortDialog.kt diff --git a/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt b/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt index db70858b..17f47532 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt @@ -59,6 +59,7 @@ class ManagerApplication : Application() { modules(module { single { providePreferences() } single { provideDownloadManager() } + single { providePathManager() } }) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/di/Managers.kt b/app/src/main/kotlin/com/aliucord/manager/di/Managers.kt index 534b1a4b..52b2e6a9 100644 --- a/app/src/main/kotlin/com/aliucord/manager/di/Managers.kt +++ b/app/src/main/kotlin/com/aliucord/manager/di/Managers.kt @@ -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 { @@ -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) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/KtStepContainer.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/KtStepContainer.kt new file mode 100644 index 00000000..35d5d35f --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/KtStepContainer.kt @@ -0,0 +1,19 @@ +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.prepare.FetchInfoStep +import kotlinx.collections.immutable.persistentListOf + +/** + * Used for installing the old Kotlin Discord app. + */ +class KtStepContainer : StepContainer() { + override val steps = persistentListOf( + FetchInfoStep(), + DownloadDiscordStep(), + DownloadInjectorStep(), + DownloadAliuhookStep(), + DownloadKotlinStep(), + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt new file mode 100644 index 00000000..431979ab --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt @@ -0,0 +1,48 @@ +package com.aliucord.manager.installer.steps + +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.base.StepState +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 + +abstract class StepContainer : KoinComponent { + private val preferences: PreferencesManager by inject() + + abstract val steps: ImmutableList + + /** + * Get a step that has already been successfully executed. + * This is used to retrieve previously executed dependency steps from a later step. + */ + inline fun getCompletedStep(): T { + val step = steps.asSequence() + .filterIsInstance() + .filter { it.state == StepState.Success } + .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@StepContainer) + if (error != null) return error + + // Add delay for human psychology and + // better group visibility in UI (the active group can change way too fast) + if (!preferences.devMode && step.durationMs < 1000) { + delay(1000L - step.durationMs) + } + + } + + return null + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepGroup.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepGroup.kt new file mode 100644 index 00000000..46fc6d3c --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepGroup.kt @@ -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) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt new file mode 100644 index 00000000..72693934 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt @@ -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.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +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: StepContainer) { + 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.Cancelled + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt new file mode 100644 index 00000000..456cca7a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt @@ -0,0 +1,77 @@ +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.StepContainer +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: StepContainer) + + /** + * 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: StepContainer): 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 { + execute(container) + state = StepState.Success + null + } catch (t: Throwable) { + state = StepState.Error + t + } + } + + durationMs = executionTimeMs.roundToInt() + return error + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt new file mode 100644 index 00000000..1024f3f8 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt @@ -0,0 +1,10 @@ +package com.aliucord.manager.installer.steps.base + +enum class StepState { + Pending, + Running, + Success, + Error, + Skipped, + Cancelled, // TODO: something like the discord dnd sign except its not red, but gray maybe +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadAliuhookStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadAliuhookStep.kt new file mode 100644 index 00000000..11ed3dd1 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadAliuhookStep.kt @@ -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.StepContainer +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: StepContainer) { + targetVersion = maven.getAliuhookVersion().getOrThrow() + + super.execute(container) + } +} + diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadDiscordStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadDiscordStep.kt new file mode 100644 index 00000000..2fced638 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadDiscordStep.kt @@ -0,0 +1,39 @@ +package com.aliucord.manager.installer.steps.download + +import androidx.compose.runtime.Stable +import com.aliucord.manager.BuildConfig +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.base.DownloadStep +import com.aliucord.manager.manager.PathManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * If not already cached, then download the raw unmodified v126.21 (Kotlin) Discord APK + * from a redirect to an APK mirror site provided by the Aliucord backend. + */ +@Stable +class DownloadDiscordStep : DownloadStep(), KoinComponent { + private val paths: PathManager by inject() + + override val localizedName = R.string.install_step_dl_kt_apk + override val targetUrl = getDiscordApkUrl(DISCORD_KT_VERSION) + override val targetFile = paths.discordApkVersionCache(DISCORD_KT_VERSION) + .resolve("discord.apk") + + override suspend fun verify() { + super.verify() + + // TODO: verify signature + } + + private companion object { + /** + * Last version of Discord before the RN transition. + */ + const val DISCORD_KT_VERSION = 126021 + + fun getDiscordApkUrl(version: Int) = + "${BuildConfig.BACKEND_URL}/download/discord?v=$version" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadInjectorStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadInjectorStep.kt new file mode 100644 index 00000000..be792237 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadInjectorStep.kt @@ -0,0 +1,45 @@ +package com.aliucord.manager.installer.steps.download + +import androidx.compose.runtime.Stable +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.base.DownloadStep +import com.aliucord.manager.installer.steps.prepare.FetchInfoStep +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.network.dto.Version +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Download a compiled dex file to be injected into the APK as the first `classes.dex` to override an entry point class. + */ +@Stable +class DownloadInjectorStep : DownloadStep(), KoinComponent { + private val paths: PathManager by inject() + + /** + * Populated from a dependency step ([FetchInfoStep]). + * This is used as cache invalidation (ref: [Version.aliucordHash]) + */ + private lateinit var aliucordHash: String + + override val localizedName = R.string.install_step_dl_injector + override val targetUrl = URL + override val targetFile + get() = paths.cachedInjectorDex(aliucordHash).resolve("discord.apk") + + override suspend fun execute(container: StepContainer) { + aliucordHash = container + .getCompletedStep() + .data.aliucordHash + + super.execute(container) + } + + private companion object { + const val ORG = "Aliucord" + const val MAIN_REPO = "Aliucord" + + const val URL = "https://raw.githubusercontent.com/$ORG/$MAIN_REPO/builds/Injector.dex" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadKotlinStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadKotlinStep.kt new file mode 100644 index 00000000..cf5990e4 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadKotlinStep.kt @@ -0,0 +1,27 @@ +package com.aliucord.manager.installer.steps.download + +import androidx.compose.runtime.Stable +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.base.DownloadStep +import com.aliucord.manager.manager.PathManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Download the most recent available Kotlin stdlib build that is supported. + */ +@Stable +class DownloadKotlinStep : DownloadStep(), KoinComponent { + private val paths: PathManager by inject() + + override val localizedName = R.string.install_step_dl_kotlin + override val targetUrl = URL + override val targetFile = paths.cachedKotlinDex() + + private companion object { + const val ORG = "Aliucord" + const val MAIN_REPO = "Aliucord" + + const val URL = "https://raw.githubusercontent.com/$ORG/$MAIN_REPO/main/installer/android/app/src/main/assets/kotlin/classes.dex" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt new file mode 100644 index 00000000..0c0f253d --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt @@ -0,0 +1,22 @@ +package com.aliucord.manager.installer.steps.patch + +import android.content.Context +import androidx.compose.runtime.Stable +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +@Stable +class ReplaceIconStep : Step(), KoinComponent { + val context: Context by inject() + + override val group = StepGroup.Patch + override val localizedName = R.string.setting_replace_icon + + override suspend fun execute(container: StepContainer) { + TODO("Not yet implemented") + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt new file mode 100644 index 00000000..79555f1a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt @@ -0,0 +1,29 @@ +package com.aliucord.manager.installer.steps.prepare + +import androidx.compose.runtime.Stable +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.network.dto.Version +import com.aliucord.manager.network.service.AliucordGithubService +import com.aliucord.manager.network.utils.getOrThrow +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +@Stable +class FetchInfoStep : Step(), KoinComponent { + private val github: AliucordGithubService by inject() + + override val group = StepGroup.Prepare + override val localizedName = R.string.install_step_fetch_kt_version + + /** + * Fetched data about the latest Aliucord commit and supported Discord version. + */ + lateinit var data: Version + + override suspend fun execute(container: StepContainer) { + data = github.getDataJson().getOrThrow() + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt index 990d4e02..8b58c948 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt @@ -8,47 +8,31 @@ import androidx.annotation.StringRes import androidx.core.content.getSystemService import com.aliucord.manager.BuildConfig import com.aliucord.manager.R -import com.aliucord.manager.domain.repository.AliucordMavenRepository -import com.aliucord.manager.network.service.AliucordGithubService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import java.io.File import kotlin.coroutines.cancellation.CancellationException /** - * Handle downloading remote urls to a file through the system [DownloadManager]. + * Handle downloading remote urls to a path through the system [DownloadManager]. */ class DownloadManager(application: Application) { private val downloadManager = application.getSystemService() ?: throw IllegalStateException("DownloadManager service is not available") - // Discord APK downloading - suspend fun downloadDiscordApk(version: String, out: File): Result = - download("${BuildConfig.BACKEND_URL}/download/discord?v=$version", out) - - // Aliucord Kotlin downloads - suspend fun downloadKtInjector(out: File): Result = - download(AliucordGithubService.KT_INJECTOR_URL, out) - - suspend fun downloadAliuhook(version: String, out: File): Result = - download(AliucordMavenRepository.getAliuhookUrl(version), out) - - suspend fun downloadKotlinDex(out: File): Result = - download(AliucordGithubService.KOTLIN_DEX_URL, out) - /** * Start a cancellable download with the system [DownloadManager]. * If the current [CoroutineScope] is cancelled, then the system download will be cancelled within 100ms. * @param url Remote src url - * @param out Target path to download to - * @param onProgressUpdate Download progress update in a `[0,1]` range, and if null then the download is currently in a pending state. - * This is called every 100ms, and should not perform long-running tasks. + * @param out Target path to download to. It is assumed that the application has write permissions to this path. + * @param onProgressUpdate An optional [ProgressListener] */ suspend fun download( url: String, out: File, - onProgressUpdate: ((Float?) -> Unit)? = null, + onProgressUpdate: ProgressListener? = null, ): Result { + onProgressUpdate?.onUpdate(null) out.parentFile?.mkdirs() // Create and start a download in the system DownloadManager @@ -90,13 +74,13 @@ class DownloadManager(application: Application) { when (status) { DownloadManager.STATUS_PENDING, DownloadManager.STATUS_PAUSED -> - onProgressUpdate?.invoke(null) + onProgressUpdate?.onUpdate(null) DownloadManager.STATUS_RUNNING -> - onProgressUpdate?.invoke(getDownloadProgress(cursor)) + onProgressUpdate?.onUpdate(getDownloadProgress(cursor)) DownloadManager.STATUS_SUCCESSFUL -> - return Result.Success + return Result.Success(out) DownloadManager.STATUS_FAILED -> { val reasonColumn = cursor.getColumnIndex(DownloadManager.COLUMN_REASON) @@ -126,11 +110,27 @@ class DownloadManager(application: Application) { return bytes.toFloat() / totalBytes } + /** + * A callback executed from a coroutine called every 100ms in order to provide + * info about the current download. This should not perform long-running tasks as the delay will be offset. + */ + fun interface ProgressListener { + /** + * @param progress The current download progress in a `[0,1]` range. If null, then the download is either + * paused, pending, or waiting to retry. + */ + fun onUpdate(progress: Float?) + } + /** * The state of a download after execution has been completed and the system-level [DownloadManager] has been cleaned up. */ sealed interface Result { - data object Success : Result + /** + * The download succeeded successfully. + * @param file The path that the download was downloaded to. + */ + data class Success(val file: File) : Result /** * This download was interrupted and the in-progress file has been deleted. diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt new file mode 100644 index 00000000..20ab4562 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt @@ -0,0 +1,56 @@ +package com.aliucord.manager.manager + +import android.content.Context +import java.io.File + +/** + * A central place to provide all system paths that are used. + */ +class PathManager(context: Context) { + private val externalCacheDir = context.externalCacheDir + ?: throw Error("External cache directory isn't supported") + + /** + * Standard path: `~/Android/data/com.aliucord.manager/cache` + */ + private val discordApkCache = externalCacheDir + .resolve("discord") + + /** + * Delete the entire cache dir and recreate it. + */ + fun clearCache() { + if (!externalCacheDir.deleteRecursively()) + throw IllegalStateException("Failed to delete cache") + + externalCacheDir.mkdirs() + } + + /** + * Create a new subfolder in the Discord APK cache for a specific version. + */ + fun discordApkVersionCache(version: Int): File = discordApkCache + .resolve(version.toString()) + .apply { mkdirs() } + + /** + * Resolve a specific path for a cached injector. + */ + fun cachedInjectorDex(aliucordHash: String) = externalCacheDir + .resolve("injector").apply { mkdirs() } + .resolve("$aliucordHash.dex") + + /** + * Resolve a specific path for a versioned cached Aliuhook build + */ + fun cachedAliuhookAAR(version: String) = externalCacheDir + .resolve("aliuhook").apply { mkdirs() } + .resolve("$version.aar") + + /** + * Singular Kotlin file of the most up-to-date version + * since the stdlib is backwards compatible. + */ + fun cachedKotlinDex() = externalCacheDir + .resolve("kotlin.dex") +} diff --git a/app/src/main/kotlin/com/aliucord/manager/network/service/AliucordGithubService.kt b/app/src/main/kotlin/com/aliucord/manager/network/service/AliucordGithubService.kt index eafcdd7c..bb14129e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/service/AliucordGithubService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/service/AliucordGithubService.kt @@ -21,12 +21,9 @@ class AliucordGithubService( suspend fun getManagerReleases() = github.getReleases(ORG, MANAGER_REPO) suspend fun getContributors() = github.getContributors(ORG, MAIN_REPO) - companion object { - private const val ORG = "Aliucord" - private const val MAIN_REPO = "Aliucord" - private const val MANAGER_REPO = "Manager" - - const val KT_INJECTOR_URL = "https://raw.githubusercontent.com/$ORG/$MAIN_REPO/builds/Injector.dex" - const val KOTLIN_DEX_URL = "https://raw.githubusercontent.com/$ORG/$MAIN_REPO/main/installer/android/app/src/main/assets/kotlin/classes.dex" + private companion object { + const val ORG = "Aliucord" + const val MAIN_REPO = "Aliucord" + const val MANAGER_REPO = "Manager" } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallAbortDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallAbortDialog.kt new file mode 100644 index 00000000..271e608a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallAbortDialog.kt @@ -0,0 +1,52 @@ +package com.aliucord.manager.ui.components.dialogs + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.aliucord.manager.R + +@Composable +fun InstallAbortDialog( + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + FilledTonalButton( + onClick = onConfirm, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Text(stringResource(R.string.action_cancel)) + } + }, + dismissButton = { + TextButton( + onClick = onDismiss, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onErrorContainer + ), + ) { + Text(stringResource(R.string.action_exit_anyways)) + } + }, + title = { + Text(stringResource(R.string.installer_abort_title)) + }, + text = { + Text(stringResource(R.string.installer_abort_body)) + }, + icon = { + Icon(Icons.Filled.Warning, contentDescription = null) + }, + containerColor = MaterialTheme.colorScheme.errorContainer, + iconContentColor = MaterialTheme.colorScheme.onErrorContainer, + titleContentColor = MaterialTheme.colorScheme.onErrorContainer, + textContentColor = MaterialTheme.colorScheme.onErrorContainer + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallerDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallerDialog.kt index a4fa0254..0cb15353 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallerDialog.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallerDialog.kt @@ -9,25 +9,15 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import com.aliucord.manager.ui.screens.install.InstallData -enum class DownloadMethod { DOWNLOAD } - @Composable fun InstallerDialog( onDismiss: () -> Unit, - onConfirm: (InstallData) -> Unit, + onConfirm: () -> Unit, ) { - val downloadMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) } - - fun triggerConfirm() { - onDismiss() - onConfirm( - InstallData(downloadMethod) - ) - } - - SideEffect(::triggerConfirm) + SideEffect(onConfirm) // TODO: local install option + // TODO: mobile data warning // Dialog( // onDismissRequest = onDismiss, diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/about/AboutScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/about/AboutScreen.kt index 2d2b1db8..f7339a12 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/about/AboutScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/about/AboutScreen.kt @@ -161,7 +161,7 @@ private fun MainContributors(modifier: Modifier = Modifier) { ) { UserEntry("Vendicated", "the ven") UserEntry("Juby210", "Fox") - UserEntry("rushii", "explod", "DiamondMiner88") + UserEntry("rushii", "explod", "rushiiMachine") } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt index b0413ed7..eca80bc9 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt @@ -26,6 +26,8 @@ import com.aliucord.manager.ui.components.dialogs.InstallerDialog import com.aliucord.manager.ui.components.home.InfoCard import com.aliucord.manager.ui.screens.about.AboutScreen import com.aliucord.manager.ui.screens.install.InstallScreen +import com.aliucord.manager.ui.screens.plugins.PluginsScreen +import com.aliucord.manager.ui.screens.settings.SettingsScreen class HomeScreen : Screen { override val key = "Home" @@ -44,15 +46,13 @@ class HomeScreen : Screen { if (showInstallerDialog) { InstallerDialog( onDismiss = { showInstallerDialog = false }, - onConfirm = { data -> + onConfirm = { showInstallerDialog = false - navigator.push(InstallScreen(data)) + navigator.push(InstallScreen()) } ) } - // TODO: add a way to open plugins and settings - Scaffold( topBar = { HomeAppBar() }, ) { paddingValues -> @@ -102,6 +102,20 @@ private fun HomeAppBar() { contentDescription = stringResource(R.string.navigation_about) ) } + + IconButton(onClick = { navigator.push(PluginsScreen()) }) { + Icon( + painter = painterResource(R.drawable.ic_extension), + contentDescription = stringResource(R.string.navigation_about) + ) + } + + IconButton(onClick = { navigator.push(SettingsScreen()) }) { + Icon( + painter = painterResource(R.drawable.ic_settings), + contentDescription = stringResource(R.string.navigation_settings) + ) + } } ) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt index 1416a5a1..5b1474fa 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt @@ -10,11 +10,10 @@ import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import com.aliucord.manager.BuildConfig import com.aliucord.manager.R -import com.aliucord.manager.manager.DownloadManager -import com.aliucord.manager.manager.PreferencesManager import com.aliucord.manager.domain.repository.AliucordMavenRepository import com.aliucord.manager.domain.repository.GithubRepository import com.aliucord.manager.installer.util.* +import com.aliucord.manager.manager.* import com.aliucord.manager.network.utils.getOrThrow import com.aliucord.manager.ui.components.installer.InstallStatus import com.aliucord.manager.ui.components.installer.InstallStepData @@ -28,13 +27,12 @@ import kotlin.time.measureTimedValue class InstallModel( private val application: Application, + private val paths: PathManager, private val downloadManager: DownloadManager, private val preferences: PreferencesManager, private val githubRepository: GithubRepository, private val aliucordMaven: AliucordMavenRepository, - private val installData: InstallData, ) : ScreenModel { - private val externalCacheDir = application.externalCacheDir!! private val installationRunning = AtomicBoolean(false) var returnToHome by mutableStateOf(false) @@ -53,8 +51,6 @@ class InstallModel( Running Android ${Build.VERSION.RELEASE}, API level ${Build.VERSION.SDK_INT} Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()} - Installing Aliucord kt with the ${installData.downloadMethod} apk method - Failed on: ${currentStep?.name} """.trimIndent() @@ -81,7 +77,7 @@ class InstallModel( } fun clearCache() { - externalCacheDir.deleteRecursively() + paths.clearCache() application.showToast(R.string.action_cleared_cache) } @@ -110,14 +106,6 @@ class InstallModel( } } - private fun clearOldCache(targetVersion: Int) { - externalCacheDir.listFiles { f -> f.isDirectory } - ?.map { it.name.toIntOrNull() to it } - ?.filter { it.first != null } - ?.filter { it.first!! in (126021 + 1) until targetVersion } - ?.forEach { it.second.deleteRecursively() } - } - private suspend fun uninstallNewAliucord(targetVersion: Int) { val (_, versionCode) = try { application.getPackageVersion(preferences.packageName) @@ -136,19 +124,8 @@ class InstallModel( } } - override fun onDispose() { - if (installationRunning.getAndSet(false)) { - installJob.cancel("ViewModel cleared") - } - } - private suspend fun installKotlin() { steps += listOfNotNull( - InstallStep.FETCH_KT_VERSION, - InstallStep.DL_KT_APK, - InstallStep.DL_KOTLIN, - InstallStep.DL_INJECTOR, - InstallStep.DL_ALIUHOOK, if (preferences.replaceIcon) InstallStep.PATCH_APP_ICON else null, InstallStep.PATCH_MANIFEST, InstallStep.PATCH_DEX, @@ -173,58 +150,6 @@ class InstallModel( uninstallNewAliucord(it) } - // Download base.apk - val baseApkFile = step(InstallStep.DL_KT_APK) { - discordCacheDir.resolve("base.apk").let { file -> - if (file.exists()) { - cached = true - } else { - downloadManager.downloadDiscordApk(dataJson.versionCode, file) - } - - file.copyTo( - patchedDir.resolve(file.name), - true - ) - } - } - - val kotlinFile = step(InstallStep.DL_KOTLIN) { - cacheDir.resolve("kotlin.dex").also { file -> - if (file.exists()) { - cached = true - } else { - downloadManager.downloadKotlinDex(file) - } - } - } - - // Download the injector dex - val injectorFile = step(InstallStep.DL_INJECTOR) { - cacheDir.resolve("injector-${dataJson.aliucordHash}.dex").also { file -> - if (file.exists()) { - cached = true - } else { - downloadManager.downloadKtInjector(file) - } - } - } - - // Download Aliuhook aar - val aliuhookAarFile = step(InstallStep.DL_ALIUHOOK) { - // Fetch aliuhook version - val aliuhookVersion = aliucordMaven.getAliuhookVersion().getOrThrow() - - // Download aliuhook aar - cacheDir.resolve("aliuhook-${aliuhookVersion}.aar").also { file -> - if (file.exists()) { - cached = true - } else { - downloadManager.downloadAliuhook(aliuhookVersion, file) - } - } - } - // Replace app icons if (preferences.replaceIcon) { step(InstallStep.PATCH_APP_ICON) { @@ -344,58 +269,11 @@ class InstallModel( } } - private inline fun step(step: InstallStep, block: InstallStepData.() -> T): T { - steps[step]!!.status = InstallStatus.ONGOING - currentStep = step - - try { - val value = measureTimedValue { block.invoke(steps[step]!!) } - val millis = value.duration.inWholeMilliseconds - - // Add delay for human psychology + groups are switched too fast - if (!preferences.devMode && millis < 1000) { - Thread.sleep(1000 - millis) - } - - steps[step]!!.apply { - duration = millis.div(1000f) - status = InstallStatus.SUCCESSFUL - } - - currentStep = step - return value.value - } catch (t: Throwable) { - steps[step]!!.status = InstallStatus.UNSUCCESSFUL - - currentStep = step - throw t - } - } - - enum class InstallStepGroup( - @StringRes - val nameResId: Int, - ) { - APK_DL(R.string.install_group_apk_dl), - LIB_DL(R.string.install_group_lib_dl), - PATCHING(R.string.install_group_patch), - INSTALLING(R.string.install_group_install) - } - // Order matters, define it in the same order as it is patched enum class InstallStep( - val group: InstallStepGroup, - @StringRes val nameResId: Int, ) { - // Kotlin - FETCH_KT_VERSION(InstallStepGroup.APK_DL, R.string.install_step_fetch_kt_version), - DL_KT_APK(InstallStepGroup.APK_DL, R.string.install_step_dl_kt_apk), - DL_KOTLIN(InstallStepGroup.LIB_DL, R.string.install_step_dl_kotlin), - DL_INJECTOR(InstallStepGroup.LIB_DL, R.string.install_step_dl_injector), - DL_ALIUHOOK(InstallStepGroup.LIB_DL, R.string.install_step_dl_aliuhook), - // Common PATCH_APP_ICON(InstallStepGroup.PATCHING, R.string.install_step_patch_icons), PATCH_MANIFEST(InstallStepGroup.PATCHING, R.string.install_step_patch_manifests), @@ -404,15 +282,4 @@ class InstallModel( SIGN_APK(InstallStepGroup.INSTALLING, R.string.install_step_signing), INSTALL_APK(InstallStepGroup.INSTALLING, R.string.install_step_installing); } - - var currentStep: InstallStep? by mutableStateOf(null) - val steps = mutableStateMapOf() - - // TODO: cache this instead - fun getSteps(group: InstallStepGroup): List { - return steps - .filterKeys { it.group == group }.entries - .sortedBy { it.key.ordinal } - .map { it.value } - } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt index 9362c51e..173cb42b 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt @@ -5,7 +5,7 @@ package com.aliucord.manager.ui.screens.install -import android.os.Parcelable +import androidx.activity.compose.BackHandler import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -15,6 +15,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp @@ -23,31 +24,20 @@ import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.aliucord.manager.R -import com.aliucord.manager.ui.components.BackButton import com.aliucord.manager.ui.components.back -import com.aliucord.manager.ui.components.dialogs.DownloadMethod +import com.aliucord.manager.ui.components.dialogs.InstallAbortDialog import com.aliucord.manager.ui.components.installer.InstallGroup import com.aliucord.manager.ui.components.installer.InstallStatus import com.aliucord.manager.ui.screens.install.InstallModel.InstallStepGroup import kotlinx.collections.immutable.toImmutableList -import kotlinx.parcelize.Parcelize -import org.koin.core.parameter.parametersOf - -@Immutable // this isn't *really* stable, but this never gets modified after being passed to a composable, so... -@Parcelize -data class InstallData( - val downloadMethod: DownloadMethod, - var baseApk: String? = null, - var splits: List? = null, -) : Parcelable - -class InstallScreen(val data: InstallData) : Screen { + +class InstallScreen : Screen { override val key = "Install" @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val model = getScreenModel(parameters = { parametersOf(data) }) + val model = getScreenModel() var expandedGroup by remember { mutableStateOf(null) } @@ -58,13 +48,35 @@ class InstallScreen(val data: InstallData) : Screen { expandedGroup = model.currentStep?.group } + // Exit warning dialog + var showAbortWarning by remember { mutableStateOf(false) } + if (showAbortWarning) { + InstallAbortDialog( + onDismiss = { showAbortWarning = false }, + onConfirm = { + navigator.back(currentActivity = null) + model.clearCache() + }, + ) + } else { + BackHandler { + showAbortWarning = true + } + } + Scaffold( topBar = { TopAppBar( title = { Text(stringResource(R.string.installer)) }, navigationIcon = { - // TODO: add confirm to exit dialog to button as well as BackHandler - BackButton() + IconButton( + onClick = { showAbortWarning = true }, + ) { + Icon( + painter = painterResource(R.drawable.ic_back), + contentDescription = stringResource(R.string.navigation_back), + ) + } } ) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/plugins/PluginsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/plugins/PluginsScreen.kt index 191b810b..fa0f0037 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/plugins/PluginsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/plugins/PluginsScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.getScreenModel import com.aliucord.manager.R +import com.aliucord.manager.ui.components.BackButton import com.aliucord.manager.ui.components.plugins.Changelog import com.aliucord.manager.ui.components.plugins.PluginCard @@ -52,75 +53,85 @@ class PluginsScreen : Screen { ) } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(14.dp) - ) { - PluginSearch( - currentFilter = model.search, - onFilterChange = model::search, - modifier = Modifier.fillMaxWidth(), - ) + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.plugins_title)) }, + navigationIcon = { BackButton() }, + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + PluginSearch( + currentFilter = model.search, + onFilterChange = model::search, + modifier = Modifier.fillMaxWidth(), + ) - if (model.error) { - Box( - modifier = Modifier.fillMaxSize() - ) { - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), + if (model.error) { + Box( + modifier = Modifier.fillMaxSize() ) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - ) - Text( - text = stringResource(R.string.plugins_error), - color = MaterialTheme.colorScheme.error, - ) + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = stringResource(R.string.plugins_error), + color = MaterialTheme.colorScheme.error, + ) + } } - } - } else if (model.plugins.isNotEmpty()) { - LazyColumn( - contentPadding = PaddingValues(bottom = 15.dp, top = 6.dp), - verticalArrangement = Arrangement.spacedBy(14.dp) - ) { - items( - // TODO: remember {} this - model.plugins.filter { plugin -> - plugin.manifest.run { - name.contains(model.search, true) - || description.contains(model.search, true) - || authors.any { (name) -> name.contains(model.search, true) } + } else if (model.plugins.isNotEmpty()) { + LazyColumn( + contentPadding = PaddingValues(bottom = 15.dp, top = 6.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + items( + // TODO: remember {} this + model.plugins.filter { plugin -> + plugin.manifest.run { + name.contains(model.search, true) + || description.contains(model.search, true) + || authors.any { (name) -> name.contains(model.search, true) } + } } + ) { plugin -> + PluginCard( + plugin = plugin, + enabled = model.enabled[plugin.manifest.name] ?: true, + onClickDelete = { model.showUninstallDialog(plugin) }, + onClickShowChangelog = { model.showChangelogDialog(plugin) }, + onSetEnabled = { model.setPluginEnabled(plugin.manifest.name, it) } + ) } - ) { plugin -> - PluginCard( - plugin = plugin, - enabled = model.enabled[plugin.manifest.name] ?: true, - onClickDelete = { model.showUninstallDialog(plugin) }, - onClickShowChangelog = { model.showChangelogDialog(plugin) }, - onSetEnabled = { model.setPluginEnabled(plugin.manifest.name, it) } - ) } - } - } else { - Box( - modifier = Modifier.fillMaxSize() - ) { - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally + } else { + Box( + modifier = Modifier.fillMaxSize() ) { - Icon( - painter = painterResource(R.drawable.ic_extension_off), - contentDescription = null - ) - Text(stringResource(R.string.plugins_none_installed)) + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(R.drawable.ic_extension_off), + contentDescription = null + ) + Text(stringResource(R.string.plugins_none_installed)) + } } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsScreen.kt index e72a34dd..051c775f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.getScreenModel import com.aliucord.manager.R +import com.aliucord.manager.ui.components.BackButton import com.aliucord.manager.ui.components.Theme import com.aliucord.manager.ui.components.settings.* @@ -30,126 +31,136 @@ class SettingsScreen : Screen { override fun Content() { val model = getScreenModel() - Column( - modifier = Modifier - .verticalScroll(state = rememberScrollState()) - ) { - val preferences = model.preferences - - if (model.showThemeDialog) { - ThemeDialog( - currentTheme = preferences.theme, - onDismissRequest = model::hideThemeDialog, - onConfirm = model::setTheme + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.navigation_settings)) }, + navigationIcon = { BackButton() }, ) - } + }, + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .verticalScroll(state = rememberScrollState()) + ) { + val preferences = model.preferences - SettingsHeader(stringResource(R.string.settings_appearance)) + if (model.showThemeDialog) { + ThemeDialog( + currentTheme = preferences.theme, + onDismissRequest = model::hideThemeDialog, + onConfirm = model::setTheme + ) + } - SettingsItem( - modifier = Modifier.clickable(onClick = model::showThemeDialog), - icon = { Icon(painterResource(R.drawable.ic_brush), null) }, - text = { Text(stringResource(R.string.settings_theme)) } - ) { - FilledTonalButton(onClick = model::showThemeDialog) { - Text(preferences.theme.toDisplayName()) + SettingsHeader(stringResource(R.string.settings_appearance)) + + SettingsItem( + modifier = Modifier.clickable(onClick = model::showThemeDialog), + icon = { Icon(painterResource(R.drawable.ic_brush), null) }, + text = { Text(stringResource(R.string.settings_theme)) } + ) { + FilledTonalButton(onClick = model::showThemeDialog) { + Text(preferences.theme.toDisplayName()) + } } - } - SettingsSwitch( - label = stringResource(R.string.setting_dynamic_color), - pref = preferences.dynamicColor, - icon = { Icon(painterResource(R.drawable.ic_palette), null) } - ) { - preferences.dynamicColor = it - } + SettingsSwitch( + label = stringResource(R.string.setting_dynamic_color), + pref = preferences.dynamicColor, + icon = { Icon(painterResource(R.drawable.ic_palette), null) } + ) { + preferences.dynamicColor = it + } - SettingsHeader(stringResource(R.string.settings_advanced)) + SettingsHeader(stringResource(R.string.settings_advanced)) - SettingsTextField( - label = stringResource(R.string.setting_app_name), - pref = preferences.appName, - onPrefChange = model::setAppName - ) - Spacer(modifier = Modifier.height(4.dp)) + SettingsTextField( + label = stringResource(R.string.setting_app_name), + pref = preferences.appName, + onPrefChange = model::setAppName + ) + Spacer(modifier = Modifier.height(4.dp)) - SettingsSwitch( - label = stringResource(R.string.setting_replace_icon), - secondaryLabel = stringResource(R.string.setting_replace_icon_desc), - pref = preferences.replaceIcon, - icon = { Icon(painterResource(R.drawable.ic_app_shortcut), null) } - ) { - preferences.replaceIcon = it - } + SettingsSwitch( + label = stringResource(R.string.setting_replace_icon), + secondaryLabel = stringResource(R.string.setting_replace_icon_desc), + pref = preferences.replaceIcon, + icon = { Icon(painterResource(R.drawable.ic_app_shortcut), null) } + ) { + preferences.replaceIcon = it + } - SettingsSwitch( - label = stringResource(R.string.setting_keep_patched_apks), - secondaryLabel = stringResource(R.string.setting_keep_patched_apks_desc), - icon = { Icon(painterResource(R.drawable.ic_delete_forever), null) }, - pref = preferences.keepPatchedApks, - onPrefChange = { preferences.keepPatchedApks = it }, - ) + SettingsSwitch( + label = stringResource(R.string.setting_keep_patched_apks), + secondaryLabel = stringResource(R.string.setting_keep_patched_apks_desc), + icon = { Icon(painterResource(R.drawable.ic_delete_forever), null) }, + pref = preferences.keepPatchedApks, + onPrefChange = { preferences.keepPatchedApks = it }, + ) - Spacer(modifier = Modifier.height(14.dp)) + Spacer(modifier = Modifier.height(14.dp)) - SettingsSwitch( - label = stringResource(R.string.settings_developer_options), - pref = preferences.devMode, - icon = { Icon(painterResource(R.drawable.ic_code), null) } - ) { - preferences.devMode = it - } + SettingsSwitch( + label = stringResource(R.string.settings_developer_options), + pref = preferences.devMode, + icon = { Icon(painterResource(R.drawable.ic_code), null) } + ) { + preferences.devMode = it + } - AnimatedVisibility( - visible = preferences.devMode, - enter = expandVertically(), - exit = shrinkVertically() - ) { - Column( - verticalArrangement = Arrangement.spacedBy(6.dp) + AnimatedVisibility( + visible = preferences.devMode, + enter = expandVertically(), + exit = shrinkVertically() ) { - SettingsTextField( - label = stringResource(R.string.setting_package_name), - pref = preferences.packageName, - onPrefChange = model::setPackageName, - error = model.packageNameError - ) + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + SettingsTextField( + label = stringResource(R.string.setting_package_name), + pref = preferences.packageName, + onPrefChange = model::setPackageName, + error = model.packageNameError + ) - SettingsTextField( - label = stringResource(R.string.setting_target_version), - pref = preferences.version, - onPrefChange = model::setVersion - ) + SettingsTextField( + label = stringResource(R.string.setting_target_version), + pref = preferences.version, + onPrefChange = model::setVersion + ) - SettingsSwitch( - label = stringResource(R.string.setting_debuggable), - secondaryLabel = stringResource(R.string.setting_debuggable_desc), - pref = preferences.debuggable, - icon = { Icon(painterResource(R.drawable.ic_bug), null) }, - onPrefChange = { preferences.debuggable = it }, - ) + SettingsSwitch( + label = stringResource(R.string.setting_debuggable), + secondaryLabel = stringResource(R.string.setting_debuggable_desc), + pref = preferences.debuggable, + icon = { Icon(painterResource(R.drawable.ic_bug), null) }, + onPrefChange = { preferences.debuggable = it }, + ) - SettingsSwitch( - label = stringResource(R.string.setting_hermes_replace_libcpp), - secondaryLabel = stringResource(R.string.setting_hermes_replace_libcpp_desc), - icon = { Icon(painterResource(R.drawable.ic_copy_file), null) }, - pref = preferences.hermesReplaceLibCpp, - onPrefChange = { preferences.hermesReplaceLibCpp = it }, - ) + SettingsSwitch( + label = stringResource(R.string.setting_hermes_replace_libcpp), + secondaryLabel = stringResource(R.string.setting_hermes_replace_libcpp_desc), + icon = { Icon(painterResource(R.drawable.ic_copy_file), null) }, + pref = preferences.hermesReplaceLibCpp, + onPrefChange = { preferences.hermesReplaceLibCpp = it }, + ) + } } - } - Button( - modifier = Modifier - .fillMaxWidth() - .padding(18.dp), - shape = ShapeDefaults.Large, - onClick = model::clearCacheDir - ) { - Text( - text = stringResource(R.string.setting_clear_cache), - textAlign = TextAlign.Center - ) + Button( + modifier = Modifier + .fillMaxWidth() + .padding(18.dp), + shape = ShapeDefaults.Large, + onClick = model::clearCacheDir + ) { + Text( + text = stringResource(R.string.setting_clear_cache), + textAlign = TextAlign.Center + ) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c457cc4..26bf0ea5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ Website Installer + Cancel Retry Apply Confirm @@ -27,6 +28,7 @@ Expand Copied! Cleared cache! + Exit anyways Grant Permissions In order for Aliucord Manager to function, file permissions are required. Since shared data is stored in ~/Aliucord, permissions are required in order to access it. @@ -91,6 +93,7 @@ (Cached) Successfully installed Aliucord Aborted Aliucord installation + Failed to verify download Please uninstall your current version of Aliucord in order to continue! Failed to install (Unknown reason) @@ -102,9 +105,9 @@ Application is incompatible with this device Installation timed out - Download APKs - Download Libraries - Patch APKs + Prepare + Download dependencies + Patch APK Install APK Patching app icons @@ -128,6 +131,9 @@ A new update has been released for Aliucord Manager! It may be required in order to function properly. Would you like to update? Update to %1$s + Really exit? + Are you sure you really want to abort an in-progress installation? Cached files will be cleared to avoid corruption. + Download failed (Unknown) Download failed (Invalid response) Download failed (File exists) From 051280044dfb1b44bc3a16b74ebee7658760821a Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Tue, 23 Jan 2024 22:58:23 -0800 Subject: [PATCH 2/9] finish rewriting steps --- .../installer/steps/KotlinInstallContainer.kt | 39 ++++ .../installer/steps/KtStepContainer.kt | 19 -- .../manager/installer/steps/StepContainer.kt | 1 - .../installer/steps/install/AlignmentStep.kt | 39 ++++ .../installer/steps/install/CleanupStep.kt | 31 +++ .../installer/steps/install/InstallStep.kt | 32 +++ .../installer/steps/install/SigningStep.kt | 23 +++ .../installer/steps/patch/AddAliuhookStep.kt | 47 +++++ .../installer/steps/patch/AddInjectorStep.kt | 46 +++++ .../steps/patch/CopyDependenciesStep.kt | 39 ++++ .../steps/patch/PatchManifestStep.kt | 42 ++++ .../installer/steps/patch/ReplaceIconStep.kt | 38 +++- .../steps/prepare/DowngradeCheckStep.kt | 54 +++++ .../aliucord/manager/manager/PathManager.kt | 6 + .../ui/screens/install/InstallModel.kt | 187 +----------------- app/src/main/res/values/strings.xml | 19 +- 16 files changed, 449 insertions(+), 213 deletions(-) create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallContainer.kt delete mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/KtStepContainer.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallContainer.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallContainer.kt new file mode 100644 index 00000000..f44ae7c4 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallContainer.kt @@ -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 KotlinInstallContainer : StepContainer() { + override val steps = persistentListOf( + // Prepare + FetchInfoStep(), + DowngradeCheckStep(), + + // Download + DownloadDiscordStep(), + DownloadInjectorStep(), + DownloadAliuhookStep(), + DownloadKotlinStep(), + + // Patch + CopyDependenciesStep(), + ReplaceIconStep(), + PatchManifestStep(), + AddInjectorStep(), + AddAliuhookStep(), + + // Install + AlignmentStep(), + SigningStep(), + InstallStep(), + CleanupStep(), + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/KtStepContainer.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/KtStepContainer.kt deleted file mode 100644 index 35d5d35f..00000000 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/KtStepContainer.kt +++ /dev/null @@ -1,19 +0,0 @@ -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.prepare.FetchInfoStep -import kotlinx.collections.immutable.persistentListOf - -/** - * Used for installing the old Kotlin Discord app. - */ -class KtStepContainer : StepContainer() { - override val steps = persistentListOf( - FetchInfoStep(), - DownloadDiscordStep(), - DownloadInjectorStep(), - DownloadAliuhookStep(), - DownloadKotlinStep(), - ) -} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt index 431979ab..360f7358 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt @@ -40,7 +40,6 @@ abstract class StepContainer : KoinComponent { if (!preferences.devMode && step.durationMs < 1000) { delay(1000L - step.durationMs) } - } return null diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt new file mode 100644 index 00000000..c0344be4 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt @@ -0,0 +1,39 @@ +package com.aliucord.manager.installer.steps.install + +import android.os.Build +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.base.StepState +import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep +import com.github.diamondminer88.zip.* +import org.koin.core.component.KoinComponent + +/** + * Align certain files in the APK to a 4KiB boundary. + */ +class AlignmentStep : Step(), KoinComponent { + private val currentDeviceArch = Build.SUPPORTED_ABIS.first() + + override val group = StepGroup.Install + override val localizedName = R.string.install_step_alignment + + override suspend fun execute(container: StepContainer) { + val apk = container.getCompletedStep().patchedApk + + // Align resources.arsc due to targeting API 30 for silent install + if (Build.VERSION.SDK_INT >= 30) { + val bytes = ZipReader(apk) + .use { it.openEntry("resources.arsc")?.read() } + ?: throw IllegalArgumentException("APK is missing resources.arsc") + + ZipWriter(apk, /* append = */ true).use { + it.deleteEntry("resources.arsc") + it.writeEntry("resources.arsc", bytes, ZipCompression.NONE, 4096) + } + } else { + state = StepState.Skipped + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt new file mode 100644 index 00000000..503985b4 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt @@ -0,0 +1,31 @@ +package com.aliucord.manager.installer.steps.install + +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.base.StepState +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.manager.PreferencesManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Cleanup patching working directory once the installation has completed. + */ +class CleanupStep : Step(), KoinComponent { + private val paths: PathManager by inject() + private val prefs: PreferencesManager by inject() + + override val group = StepGroup.Install + override val localizedName = R.string.install_step_cleanup + + override suspend fun execute(container: StepContainer) { + if (prefs.keepPatchedApks) { + state = StepState.Skipped + } else { + if (!paths.patchingWorkingDir().deleteRecursively()) + throw IllegalStateException("Failed to delete patching working dir") + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt new file mode 100644 index 00000000..d62be491 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt @@ -0,0 +1,32 @@ +package com.aliucord.manager.installer.steps.install + +import android.app.Application +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep +import com.aliucord.manager.installer.util.installApks +import com.aliucord.manager.manager.PreferencesManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Install the final APK with the system's PackageManager. + */ +class InstallStep : Step(), KoinComponent { + private val application: Application by inject() + private val prefs: PreferencesManager by inject() + + override val group = StepGroup.Install + override val localizedName = R.string.install_step_installing + + override suspend fun execute(container: StepContainer) { + val apk = container.getCompletedStep().patchedApk + + application.installApks( + silent = !prefs.devMode, + apks = arrayOf(apk), + ) + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt new file mode 100644 index 00000000..8978aa47 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt @@ -0,0 +1,23 @@ +package com.aliucord.manager.installer.steps.install + +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep +import com.aliucord.manager.installer.util.Signer +import org.koin.core.component.KoinComponent + +/** + * Sign the APK with a keystore generated on-device. + */ +class SigningStep : Step(), KoinComponent { + override val group = StepGroup.Install + override val localizedName = R.string.install_step_signing + + override suspend fun execute(container: StepContainer) { + val apk = container.getCompletedStep().patchedApk + + Signer.signApk(apk) + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt new file mode 100644 index 00000000..27b4b937 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt @@ -0,0 +1,47 @@ +package com.aliucord.manager.installer.steps.patch + +import android.os.Build +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.download.DownloadAliuhookStep +import com.github.diamondminer88.zip.ZipReader +import com.github.diamondminer88.zip.ZipWriter +import org.koin.core.component.KoinComponent + +/** + * Add the Aliuhook library's native libs along with dex + */ +class AddAliuhookStep : Step(), KoinComponent { + private val currentDeviceArch = Build.SUPPORTED_ABIS.first() + + override val group = StepGroup.Patch + override val localizedName = R.string.install_step_add_aliuhook + + override suspend fun execute(container: StepContainer) { + val apk = container.getCompletedStep().patchedApk + val aliuhook = container.getCompletedStep().targetFile + + // Find the amount of .dex files in the apk + val dexCount = ZipReader(apk).use { + it.entryNames.count { name -> name.endsWith(".dex") } + } + + ZipWriter(apk, /* append = */ true).use { patchedApk -> + ZipReader(aliuhook).use { aliuhook -> + for (libFile in arrayOf("libaliuhook.so", "libc++_shared.so", "liblsplant.so")) { + val bytes = aliuhook.openEntry("jni/$currentDeviceArch/$libFile")?.read() + ?: throw IllegalStateException("Failed to read $libFile from aliuhook aar") + + patchedApk.writeEntry("lib/$currentDeviceArch/$libFile", bytes) + } + + val aliuhookDex = aliuhook.openEntry("classes.dex")?.read() + ?: throw IllegalStateException("No classes.dex in aliuhook aar") + + patchedApk.writeEntry("classes${dexCount + 1}.dex", aliuhookDex) + } + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt new file mode 100644 index 00000000..51af5c14 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt @@ -0,0 +1,46 @@ +package com.aliucord.manager.installer.steps.patch + +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.download.DownloadInjectorStep +import com.aliucord.manager.installer.steps.download.DownloadKotlinStep +import com.github.diamondminer88.zip.ZipReader +import com.github.diamondminer88.zip.ZipWriter +import org.koin.core.component.KoinComponent + +/** + * Reorder the existing dex files to add the Aliucord injector as the first `classes.dex` file. + */ +class AddInjectorStep : Step(), KoinComponent { + override val group = StepGroup.Patch + override val localizedName = R.string.install_step_add_injector + + override suspend fun execute(container: StepContainer) { + val apk = container.getCompletedStep().patchedApk + val injector = container.getCompletedStep().targetFile + val kotlinStdlib = container.getCompletedStep().targetFile + + val (dexCount, firstDexBytes) = ZipReader(apk).use { + Pair( + // Find the amount of .dex files in apk + it.entryNames.count { name -> name.endsWith(".dex") }, + + // Get the first dex + it.openEntry("classes.dex")?.read() + ?: throw IllegalStateException("No classes.dex in base apk") + ) + } + + ZipWriter(apk, /* append = */ true).use { + // Move copied dex to end of dex list + it.deleteEntry("classes.dex") + it.writeEntry("classes${dexCount + 1}.dex", firstDexBytes) + + // Add Kotlin & Aliucord's dex + it.writeEntry("classes.dex", injector.readBytes()) + it.writeEntry("classes${dexCount + 2}.dex", kotlinStdlib.readBytes()) + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt new file mode 100644 index 00000000..108286c8 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt @@ -0,0 +1,39 @@ +package com.aliucord.manager.installer.steps.patch + +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.download.DownloadDiscordStep +import com.aliucord.manager.manager.PathManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.io.File + +/** + * Step to duplicate the Discord APK to be worked on. + */ +class CopyDependenciesStep : Step(), KoinComponent { + private val paths: PathManager by inject() + + /** + * The target APK file which can be modified during patching + */ + val patchedApk: File = paths.patchingWorkingDir() + .resolve("patched.apk") + + override val group = StepGroup.Patch + override val localizedName = R.string.install_step_copy + + override suspend fun execute(container: StepContainer) { + val dir = paths.patchingWorkingDir() + + // TODO: move this to a prepare step + if (!dir.deleteRecursively()) + throw Error("Failed to clear existing patched dir") + + val srcApk = container.getCompletedStep().targetFile + + srcApk.copyTo(patchedApk) + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt new file mode 100644 index 00000000..941e2f6a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt @@ -0,0 +1,42 @@ +package com.aliucord.manager.installer.steps.patch + +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.util.ManifestPatcher +import com.aliucord.manager.manager.PreferencesManager +import com.github.diamondminer88.zip.ZipReader +import com.github.diamondminer88.zip.ZipWriter +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Patch the APK's AndroidManifest.xml + */ +class PatchManifestStep : Step(), KoinComponent { + private val prefs: PreferencesManager by inject() + + override val group = StepGroup.Patch + override val localizedName = R.string.install_step_patch_manifests + + override suspend fun execute(container: StepContainer) { + val apk = container.getCompletedStep().patchedApk + + val manifest = ZipReader(apk) + .use { zip -> zip.openEntry("AndroidManifest.xml")?.read() } + ?: throw IllegalArgumentException("No manifest found in APK") + + val patchedManifest = ManifestPatcher.patchManifest( + manifestBytes = manifest, + packageName = prefs.packageName, + appName = prefs.appName, + debuggable = prefs.debuggable, + ) + + ZipWriter(apk, /* append = */ true).use { + it.deleteEntry("AndroidManifest.xml") + it.writeEntry("AndroidManifest.xml", patchedManifest) + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt index 0c0f253d..9a4af5ad 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt @@ -6,17 +6,51 @@ import com.aliucord.manager.R import com.aliucord.manager.installer.steps.StepContainer import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.base.StepState +import com.aliucord.manager.manager.PreferencesManager +import com.github.diamondminer88.zip.ZipWriter import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import java.io.InputStream +/** + * Replace icons + */ @Stable class ReplaceIconStep : Step(), KoinComponent { - val context: Context by inject() + private val context: Context by inject() + private val prefs: PreferencesManager by inject() override val group = StepGroup.Patch override val localizedName = R.string.setting_replace_icon override suspend fun execute(container: StepContainer) { - TODO("Not yet implemented") + if (!prefs.replaceIcon) { + state = StepState.Skipped + return + } + + val apk = container.getCompletedStep().patchedApk + + ZipWriter(apk, /* append = */ true).use { + val foregroundIcon = readAsset("icons/ic_logo_foreground.png") + val squareIcon = readAsset("icons/ic_logo_square.png") + + val replacements = mapOf( + arrayOf("MbV.png", "kbF.png", "_eu.png", "EtS.png") to foregroundIcon, + arrayOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png") to squareIcon + ) + + for ((files, replacement) in replacements) { + for (file in files) { + val path = "res/$file" + it.deleteEntry(path) + it.writeEntry(path, replacement) + } + } + } } + + private fun readAsset(fileName: String): ByteArray = + context.assets.open(fileName).use(InputStream::readBytes) } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt new file mode 100644 index 00000000..6b9c0dba --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt @@ -0,0 +1,54 @@ +package com.aliucord.manager.installer.steps.prepare + +import android.content.Context +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.base.StepState +import com.aliucord.manager.installer.util.uninstallApk +import com.aliucord.manager.manager.PreferencesManager +import com.aliucord.manager.util.getPackageVersion +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 + +/** + * Prompt the user to uninstall a previous version of Aliucord if it has a larger version code. + * (Prevent conflicts from downgrading) + */ +class DowngradeCheckStep : Step(), KoinComponent { + private val context: Context by inject() + private val prefs: PreferencesManager by inject() + + override val group = StepGroup.Prepare + override val localizedName = R.string.install_step_downgrade_check + + override suspend fun execute(container: StepContainer) { + val (_, currentVersion) = try { + context.getPackageVersion(prefs.packageName) + } catch (_: Throwable) { + // Package is not installed + return + } + + val targetVersion = container + .getCompletedStep() + .data.versionCode.toIntOrNull() + ?: throw IllegalArgumentException("Invalid fetched Aliucord target Discord version") + + if (currentVersion > targetVersion) { + context.uninstallApk(prefs.packageName) + + withContext(Dispatchers.Main) { + context.showToast(R.string.installer_uninstall_new) + } + + throw Error("Newer version of Aliucord must be uninstalled prior to installing an older version") + } else { + state = StepState.Skipped + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt index 20ab4562..6070aa8d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt @@ -53,4 +53,10 @@ class PathManager(context: Context) { */ fun cachedKotlinDex() = externalCacheDir .resolve("kotlin.dex") + + /** + * The temporary working directory of a currently executing patching process. + */ + fun patchingWorkingDir() = externalCacheDir + .resolve("patched") } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt index 5b1474fa..11368889 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt @@ -4,34 +4,24 @@ import android.annotation.SuppressLint import android.app.Application import android.os.Build import android.util.Log -import androidx.annotation.StringRes import androidx.compose.runtime.* import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import com.aliucord.manager.BuildConfig import com.aliucord.manager.R -import com.aliucord.manager.domain.repository.AliucordMavenRepository -import com.aliucord.manager.domain.repository.GithubRepository +import com.aliucord.manager.installer.steps.KotlinInstallContainer import com.aliucord.manager.installer.util.* import com.aliucord.manager.manager.* -import com.aliucord.manager.network.utils.getOrThrow -import com.aliucord.manager.ui.components.installer.InstallStatus -import com.aliucord.manager.ui.components.installer.InstallStepData import com.aliucord.manager.util.* import com.github.diamondminer88.zip.* import kotlinx.coroutines.* import java.text.SimpleDateFormat import java.util.Date import java.util.concurrent.atomic.AtomicBoolean -import kotlin.time.measureTimedValue class InstallModel( private val application: Application, private val paths: PathManager, - private val downloadManager: DownloadManager, - private val preferences: PreferencesManager, - private val githubRepository: GithubRepository, - private val aliucordMaven: AliucordMavenRepository, ) : ScreenModel { private val installationRunning = AtomicBoolean(false) @@ -106,180 +96,11 @@ class InstallModel( } } - private suspend fun uninstallNewAliucord(targetVersion: Int) { - val (_, versionCode) = try { - application.getPackageVersion(preferences.packageName) - } catch (t: Throwable) { - return - } - - if (targetVersion < versionCode) { - application.uninstallApk(preferences.packageName) - - withContext(Dispatchers.Main) { - application.showToast(R.string.installer_uninstall_new) - } - - throw Error("Pleaser uninstall newer Aliucord prior to installing") - } - } - private suspend fun installKotlin() { - steps += listOfNotNull( - if (preferences.replaceIcon) InstallStep.PATCH_APP_ICON else null, - InstallStep.PATCH_MANIFEST, - InstallStep.PATCH_DEX, - InstallStep.PATCH_LIBS, - InstallStep.SIGN_APK, - InstallStep.INSTALL_APK, - ).map { - it to InstallStepData(it.nameResId, InstallStatus.QUEUED) - } - - val dataJson = step(InstallStep.FETCH_KT_VERSION) { - githubRepository.getDataJson().getOrThrow() - } - - val arch = Build.SUPPORTED_ABIS.first() - val cacheDir = externalCacheDir - val discordCacheDir = externalCacheDir.resolve(dataJson.versionCode) - val patchedDir = discordCacheDir.resolve("patched").also { it.deleteRecursively() } + val error = KotlinInstallContainer().executeAll() - dataJson.versionCode.toInt().also { - clearOldCache(it) - uninstallNewAliucord(it) + if (error != null) { + Log.e(BuildConfig.TAG, "Failed to perform installation process", error) } - - // Replace app icons - if (preferences.replaceIcon) { - step(InstallStep.PATCH_APP_ICON) { - ZipWriter(baseApkFile, true).use { baseApk -> - val foregroundIcon = application.assets.open("icons/ic_logo_foreground.png") - .use { it.readBytes() } - val squareIcon = application.assets.open("icons/ic_logo_square.png") - .use { it.readBytes() } - - val replacements = mapOf( - arrayOf("MbV.png", "kbF.png", "_eu.png", "EtS.png") to foregroundIcon, - arrayOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png") to squareIcon - ) - - for ((files, replacement) in replacements) { - for (file in files) { - val path = "res/$file" - baseApk.deleteEntry(path) - baseApk.writeEntry(path, replacement) - } - } - } - } - } - - // Patch manifests - step(InstallStep.PATCH_MANIFEST) { - val manifest = ZipReader(baseApkFile) - .use { zip -> zip.openEntry("AndroidManifest.xml")?.read() } - ?: throw IllegalStateException("No manifest in base apk") - - ZipWriter(baseApkFile, true).use { zip -> - val patchedManifestBytes = ManifestPatcher.patchManifest( - manifestBytes = manifest, - packageName = preferences.packageName, - appName = preferences.appName, - debuggable = preferences.debuggable, - ) - - zip.deleteEntry("AndroidManifest.xml") - zip.writeEntry("AndroidManifest.xml", patchedManifestBytes) - } - } - - // Re-order dex files - val dexCount = step(InstallStep.PATCH_DEX) { - val (dexCount, firstDexBytes) = ZipReader(baseApkFile).use { zip -> - Pair( - // Find the amount of .dex files in apk - zip.entryNames.count { it.endsWith(".dex") }, - - // Get the first dex - zip.openEntry("classes.dex")?.read() - ?: throw IllegalStateException("No classes.dex in base apk") - ) - } - - ZipWriter(baseApkFile, true).use { zip -> - // Move copied dex to end of dex list - zip.deleteEntry("classes.dex") - zip.writeEntry("classes${dexCount + 1}.dex", firstDexBytes) - - // Add Kotlin & Aliucord's dex - zip.writeEntry("classes.dex", injectorFile.readBytes()) - zip.writeEntry("classes${dexCount + 2}.dex", kotlinFile.readBytes()) - } - - dexCount - } - - // Replace libs - step(InstallStep.PATCH_LIBS) { - ZipWriter(baseApkFile, true).use { baseApk -> - ZipReader(aliuhookAarFile).use { aliuhookAar -> - for (libFile in arrayOf("libaliuhook.so", "libc++_shared.so", "liblsplant.so")) { - val bytes = aliuhookAar.openEntry("jni/$arch/$libFile")?.read() - ?: throw IllegalStateException("Failed to read $libFile from aliuhook aar") - - baseApk.writeEntry("lib/$arch/$libFile", bytes) - } - - // Add Aliuhook's dex file - val aliuhookDex = aliuhookAar.openEntry("classes.dex")?.read() - ?: throw IllegalStateException("No classes.dex in aliuhook aar") - - baseApk.writeEntry("classes${dexCount + 3}.dex", aliuhookDex) - } - } - } - - step(InstallStep.SIGN_APK) { - // Align resources.arsc due to targeting api 30 for silent install - if (Build.VERSION.SDK_INT >= 31) { - val bytes = ZipReader(baseApkFile).use { - if (it.entryNames.contains("resources.arsc")) { - it.openEntry("resources.arsc")?.read() - } else { - null - } - } - - ZipWriter(baseApkFile, true).use { - it.deleteEntry("resources.arsc", true) - it.writeEntry("resources.arsc", bytes, ZipCompression.NONE, 4096) - } - } - - Signer.signApk(baseApkFile) - } - - step(InstallStep.INSTALL_APK) { - application.installApks(silent = !preferences.devMode, baseApkFile) - - if (!preferences.keepPatchedApks) { - patchedDir.deleteRecursively() - } - } - } - - // Order matters, define it in the same order as it is patched - enum class InstallStep( - @StringRes - val nameResId: Int, - ) { - // Common - PATCH_APP_ICON(InstallStepGroup.PATCHING, R.string.install_step_patch_icons), - PATCH_MANIFEST(InstallStepGroup.PATCHING, R.string.install_step_patch_manifests), - PATCH_DEX(InstallStepGroup.PATCHING, R.string.install_step_patch_dex), - PATCH_LIBS(InstallStepGroup.PATCHING, R.string.install_step_patch_libs), - SIGN_APK(InstallStepGroup.INSTALLING, R.string.install_step_signing), - INSTALL_APK(InstallStepGroup.INSTALLING, R.string.install_step_installing); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 26bf0ea5..f6aea8ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -110,18 +110,21 @@ Patch APK Install APK + Fetching target Discord version + Checking for older installations + Downloading Discord APK + Downloading Kotlin Stdlib + Downloading Aliucord Injector + Downloading Aliuhook library + Copying dependencies Patching app icons Patching apk manifests - Adding aliucord dex into apk - Replacing libraries + Adding Aliucord injector + Adding Aliuhook library + Aligning APKs Signing APKs Installing APKs - - Fetching target Discord version - Downloading Discord APK - Downloading Kotlin library - Downloading injector - Downloading Aliuhook library + Cleaning up Success Ongoing From c073f291bda7da105dc07cbbb20854b3ee4ae971 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Tue, 23 Jan 2024 23:32:57 -0800 Subject: [PATCH 3/9] guh --- .../manager/installer/steps/prepare/DowngradeCheckStep.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt index 6b9c0dba..b75b2726 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt @@ -29,8 +29,10 @@ class DowngradeCheckStep : Step(), KoinComponent { override suspend fun execute(container: StepContainer) { val (_, currentVersion) = try { context.getPackageVersion(prefs.packageName) - } catch (_: Throwable) { - // Package is not installed + } + // Package is not installed + catch (_: Throwable) { + state = StepState.Skipped return } @@ -47,8 +49,6 @@ class DowngradeCheckStep : Step(), KoinComponent { } throw Error("Newer version of Aliucord must be uninstalled prior to installing an older version") - } else { - state = StepState.Skipped } } } From 0b601c1aa7b808e4b3394352c02b5dbae78bd0a2 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Tue, 23 Jan 2024 23:47:56 -0800 Subject: [PATCH 4/9] reformat imports --- .../kotlin/com/aliucord/manager/installer/steps/base/Step.kt | 2 +- .../manager/ui/components/dialogs/InstallerDialog.kt | 5 ++--- .../kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt | 2 +- .../aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt index 456cca7a..3f3a1f7d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt @@ -2,8 +2,8 @@ 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.StepContainer +import com.aliucord.manager.installer.steps.StepGroup import org.koin.core.time.measureTimedValue import kotlin.math.roundToInt diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallerDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallerDialog.kt index 0cb15353..08534d19 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallerDialog.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/InstallerDialog.kt @@ -5,9 +5,8 @@ package com.aliucord.manager.ui.components.dialogs -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import com.aliucord.manager.ui.screens.install.InstallData +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect @Composable fun InstallerDialog( diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt index 9586fe06..003a4283 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt @@ -8,9 +8,9 @@ import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import com.aliucord.manager.BuildConfig import com.aliucord.manager.R -import com.aliucord.manager.manager.PreferencesManager import com.aliucord.manager.domain.repository.GithubRepository import com.aliucord.manager.installer.util.uninstallApk +import com.aliucord.manager.manager.PreferencesManager import com.aliucord.manager.network.utils.fold import com.aliucord.manager.ui.util.DiscordVersion import com.aliucord.manager.util.getPackageVersion diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt index 22f0c881..55338188 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt @@ -5,9 +5,9 @@ import androidx.compose.runtime.* import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aliucord.manager.BuildConfig -import com.aliucord.manager.manager.DownloadManager import com.aliucord.manager.domain.repository.GithubRepository import com.aliucord.manager.installer.util.installApks +import com.aliucord.manager.manager.DownloadManager import com.aliucord.manager.network.utils.SemVer import com.aliucord.manager.network.utils.getOrNull import kotlinx.coroutines.Dispatchers From ae0ced04596dfbbb6b569af3b72667ecc9baaf70 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Sun, 4 Feb 2024 21:59:09 -0800 Subject: [PATCH 5/9] rewrite install screen + model --- ...allContainer.kt => KotlinInstallRunner.kt} | 2 +- .../steps/{StepContainer.kt => StepRunner.kt} | 6 +- .../installer/steps/base/DownloadStep.kt | 4 +- .../manager/installer/steps/base/Step.kt | 6 +- .../steps/download/DownloadAliuhookStep.kt | 4 +- .../steps/download/DownloadInjectorStep.kt | 6 +- .../installer/steps/install/AlignmentStep.kt | 6 +- .../installer/steps/install/CleanupStep.kt | 4 +- .../installer/steps/install/InstallStep.kt | 6 +- .../installer/steps/install/SigningStep.kt | 6 +- .../installer/steps/patch/AddAliuhookStep.kt | 8 +- .../installer/steps/patch/AddInjectorStep.kt | 10 +- .../steps/patch/CopyDependenciesStep.kt | 6 +- .../steps/patch/PatchManifestStep.kt | 6 +- .../installer/steps/patch/ReplaceIconStep.kt | 6 +- .../steps/prepare/DowngradeCheckStep.kt | 6 +- .../installer/steps/prepare/FetchInfoStep.kt | 4 +- .../ui/components/installer/InstallGroup.kt | 22 +-- .../ui/screens/install/InstallModel.kt | 143 ++++++++++-------- .../ui/screens/install/InstallScreen.kt | 79 +++++----- .../ui/screens/install/InstallScreenState.kt | 16 ++ .../manager/ui/util/UnsafeImmutables.kt | 29 ++++ 22 files changed, 215 insertions(+), 170 deletions(-) rename app/src/main/kotlin/com/aliucord/manager/installer/steps/{KotlinInstallContainer.kt => KotlinInstallRunner.kt} (95%) rename app/src/main/kotlin/com/aliucord/manager/installer/steps/{StepContainer.kt => StepRunner.kt} (89%) create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreenState.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/util/UnsafeImmutables.kt diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallContainer.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallRunner.kt similarity index 95% rename from app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallContainer.kt rename to app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallRunner.kt index f44ae7c4..6a6430fd 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallContainer.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallRunner.kt @@ -11,7 +11,7 @@ import kotlinx.collections.immutable.persistentListOf /** * Used for installing the old Kotlin Discord app. */ -class KotlinInstallContainer : StepContainer() { +class KotlinInstallRunner : StepRunner() { override val steps = persistentListOf( // Prepare FetchInfoStep(), diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt similarity index 89% rename from app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt rename to app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt index 360f7358..cc854d5b 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepContainer.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.delay import org.koin.core.component.KoinComponent import org.koin.core.component.inject -abstract class StepContainer : KoinComponent { +abstract class StepRunner : KoinComponent { private val preferences: PreferencesManager by inject() abstract val steps: ImmutableList @@ -17,7 +17,7 @@ abstract class StepContainer : KoinComponent { * Get a step that has already been successfully executed. * This is used to retrieve previously executed dependency steps from a later step. */ - inline fun getCompletedStep(): T { + inline fun getStep(): T { val step = steps.asSequence() .filterIsInstance() .filter { it.state == StepState.Success } @@ -32,7 +32,7 @@ abstract class StepContainer : KoinComponent { suspend fun executeAll(): Throwable? { for (step in steps) { - val error = step.executeCatching(this@StepContainer) + val error = step.executeCatching(this@StepRunner) if (error != null) return error // Add delay for human psychology and diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt index 72693934..f261926c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt @@ -3,7 +3,7 @@ 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.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.manager.DownloadManager import com.aliucord.manager.util.showToast @@ -43,7 +43,7 @@ abstract class DownloadStep : Step(), KoinComponent { override val group = StepGroup.Download - override suspend fun execute(container: StepContainer) { + override suspend fun execute(container: StepRunner) { if (targetFile.exists()) { if (targetFile.length() > 0) { state = StepState.Skipped diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt index 3f3a1f7d..06b2054c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt @@ -2,7 +2,7 @@ package com.aliucord.manager.installer.steps.base import androidx.annotation.StringRes import androidx.compose.runtime.* -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import org.koin.core.time.measureTimedValue import kotlin.math.roundToInt @@ -27,7 +27,7 @@ abstract class Step { * 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: StepContainer) + protected abstract suspend fun execute(container: StepRunner) /** * The current state of this step in the installation process. @@ -53,7 +53,7 @@ abstract class Step { * Thin wrapper over [execute] but handling errors. * @return An exception if the step failed to execute. */ - suspend fun executeCatching(container: StepContainer): Throwable? { + suspend fun executeCatching(container: StepRunner): Throwable? { if (state != StepState.Pending) throw IllegalStateException("Cannot execute a step that has already started") diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadAliuhookStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadAliuhookStep.kt index 11ed3dd1..87e5fb01 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadAliuhookStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadAliuhookStep.kt @@ -3,7 +3,7 @@ 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.StepContainer +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 @@ -27,7 +27,7 @@ class DownloadAliuhookStep : DownloadStep(), KoinComponent { override val targetUrl get() = AliucordMavenRepository.getAliuhookUrl(targetVersion) override val targetFile get() = paths.cachedAliuhookAAR(targetVersion) - override suspend fun execute(container: StepContainer) { + override suspend fun execute(container: StepRunner) { targetVersion = maven.getAliuhookVersion().getOrThrow() super.execute(container) diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadInjectorStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadInjectorStep.kt index be792237..831b54df 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadInjectorStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/download/DownloadInjectorStep.kt @@ -2,7 +2,7 @@ package com.aliucord.manager.installer.steps.download import androidx.compose.runtime.Stable import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.DownloadStep import com.aliucord.manager.installer.steps.prepare.FetchInfoStep import com.aliucord.manager.manager.PathManager @@ -28,9 +28,9 @@ class DownloadInjectorStep : DownloadStep(), KoinComponent { override val targetFile get() = paths.cachedInjectorDex(aliucordHash).resolve("discord.apk") - override suspend fun execute(container: StepContainer) { + override suspend fun execute(container: StepRunner) { aliucordHash = container - .getCompletedStep() + .getStep() .data.aliucordHash super.execute(container) diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt index c0344be4..4f374ce4 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt @@ -2,7 +2,7 @@ package com.aliucord.manager.installer.steps.install import android.os.Build import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.StepState @@ -19,8 +19,8 @@ class AlignmentStep : Step(), KoinComponent { override val group = StepGroup.Install override val localizedName = R.string.install_step_alignment - override suspend fun execute(container: StepContainer) { - val apk = container.getCompletedStep().patchedApk + override suspend fun execute(container: StepRunner) { + val apk = container.getStep().patchedApk // Align resources.arsc due to targeting API 30 for silent install if (Build.VERSION.SDK_INT >= 30) { diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt index 503985b4..30de3ed6 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt @@ -1,7 +1,7 @@ package com.aliucord.manager.installer.steps.install import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.StepState @@ -20,7 +20,7 @@ class CleanupStep : Step(), KoinComponent { override val group = StepGroup.Install override val localizedName = R.string.install_step_cleanup - override suspend fun execute(container: StepContainer) { + override suspend fun execute(container: StepRunner) { if (prefs.keepPatchedApks) { state = StepState.Skipped } else { diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt index d62be491..30d6460e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt @@ -2,7 +2,7 @@ package com.aliucord.manager.installer.steps.install import android.app.Application import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep @@ -21,8 +21,8 @@ class InstallStep : Step(), KoinComponent { override val group = StepGroup.Install override val localizedName = R.string.install_step_installing - override suspend fun execute(container: StepContainer) { - val apk = container.getCompletedStep().patchedApk + override suspend fun execute(container: StepRunner) { + val apk = container.getStep().patchedApk application.installApks( silent = !prefs.devMode, diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt index 8978aa47..1352d599 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt @@ -1,7 +1,7 @@ package com.aliucord.manager.installer.steps.install import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep @@ -15,8 +15,8 @@ class SigningStep : Step(), KoinComponent { override val group = StepGroup.Install override val localizedName = R.string.install_step_signing - override suspend fun execute(container: StepContainer) { - val apk = container.getCompletedStep().patchedApk + override suspend fun execute(container: StepRunner) { + val apk = container.getStep().patchedApk Signer.signApk(apk) } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt index 27b4b937..d2feab17 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt @@ -2,7 +2,7 @@ package com.aliucord.manager.installer.steps.patch import android.os.Build import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.download.DownloadAliuhookStep @@ -19,9 +19,9 @@ class AddAliuhookStep : Step(), KoinComponent { override val group = StepGroup.Patch override val localizedName = R.string.install_step_add_aliuhook - override suspend fun execute(container: StepContainer) { - val apk = container.getCompletedStep().patchedApk - val aliuhook = container.getCompletedStep().targetFile + override suspend fun execute(container: StepRunner) { + val apk = container.getStep().patchedApk + val aliuhook = container.getStep().targetFile // Find the amount of .dex files in the apk val dexCount = ZipReader(apk).use { diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt index 51af5c14..567e1018 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt @@ -1,7 +1,7 @@ package com.aliucord.manager.installer.steps.patch import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.download.DownloadInjectorStep @@ -17,10 +17,10 @@ class AddInjectorStep : Step(), KoinComponent { override val group = StepGroup.Patch override val localizedName = R.string.install_step_add_injector - override suspend fun execute(container: StepContainer) { - val apk = container.getCompletedStep().patchedApk - val injector = container.getCompletedStep().targetFile - val kotlinStdlib = container.getCompletedStep().targetFile + override suspend fun execute(container: StepRunner) { + val apk = container.getStep().patchedApk + val injector = container.getStep().targetFile + val kotlinStdlib = container.getStep().targetFile val (dexCount, firstDexBytes) = ZipReader(apk).use { Pair( diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt index 108286c8..1ed5d7cf 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt @@ -1,7 +1,7 @@ package com.aliucord.manager.installer.steps.patch import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.download.DownloadDiscordStep @@ -25,14 +25,14 @@ class CopyDependenciesStep : Step(), KoinComponent { override val group = StepGroup.Patch override val localizedName = R.string.install_step_copy - override suspend fun execute(container: StepContainer) { + override suspend fun execute(container: StepRunner) { val dir = paths.patchingWorkingDir() // TODO: move this to a prepare step if (!dir.deleteRecursively()) throw Error("Failed to clear existing patched dir") - val srcApk = container.getCompletedStep().targetFile + val srcApk = container.getStep().targetFile srcApk.copyTo(patchedApk) } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt index 941e2f6a..f7e3d219 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt @@ -1,7 +1,7 @@ package com.aliucord.manager.installer.steps.patch import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.util.ManifestPatcher @@ -20,8 +20,8 @@ class PatchManifestStep : Step(), KoinComponent { override val group = StepGroup.Patch override val localizedName = R.string.install_step_patch_manifests - override suspend fun execute(container: StepContainer) { - val apk = container.getCompletedStep().patchedApk + override suspend fun execute(container: StepRunner) { + val apk = container.getStep().patchedApk val manifest = ZipReader(apk) .use { zip -> zip.openEntry("AndroidManifest.xml")?.read() } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt index 9a4af5ad..30b5326a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt @@ -3,7 +3,7 @@ package com.aliucord.manager.installer.steps.patch import android.content.Context import androidx.compose.runtime.Stable import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.StepState @@ -24,13 +24,13 @@ class ReplaceIconStep : Step(), KoinComponent { override val group = StepGroup.Patch override val localizedName = R.string.setting_replace_icon - override suspend fun execute(container: StepContainer) { + override suspend fun execute(container: StepRunner) { if (!prefs.replaceIcon) { state = StepState.Skipped return } - val apk = container.getCompletedStep().patchedApk + val apk = container.getStep().patchedApk ZipWriter(apk, /* append = */ true).use { val foregroundIcon = readAsset("icons/ic_logo_foreground.png") diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt index b75b2726..566809aa 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt @@ -2,7 +2,7 @@ package com.aliucord.manager.installer.steps.prepare import android.content.Context import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.StepState @@ -26,7 +26,7 @@ class DowngradeCheckStep : Step(), KoinComponent { override val group = StepGroup.Prepare override val localizedName = R.string.install_step_downgrade_check - override suspend fun execute(container: StepContainer) { + override suspend fun execute(container: StepRunner) { val (_, currentVersion) = try { context.getPackageVersion(prefs.packageName) } @@ -37,7 +37,7 @@ class DowngradeCheckStep : Step(), KoinComponent { } val targetVersion = container - .getCompletedStep() + .getStep() .data.versionCode.toIntOrNull() ?: throw IllegalArgumentException("Invalid fetched Aliucord target Discord version") diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt index 79555f1a..917eb476 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt @@ -2,7 +2,7 @@ package com.aliucord.manager.installer.steps.prepare import androidx.compose.runtime.Stable import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepContainer +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.network.dto.Version @@ -23,7 +23,7 @@ class FetchInfoStep : Step(), KoinComponent { */ lateinit var data: Version - override suspend fun execute(container: StepContainer) { + override suspend fun execute(container: StepRunner) { data = github.getDataJson().getOrThrow() } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt index 461a7f14..6236a798 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt @@ -20,33 +20,15 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.base.Step import kotlinx.collections.immutable.ImmutableList import kotlin.math.floor -enum class InstallStatus { - ONGOING, - SUCCESSFUL, - UNSUCCESSFUL, - QUEUED -} - -@Stable -class InstallStepData( - val nameResId: Int, - status: InstallStatus, - duration: Float = 0f, - cached: Boolean = false, -) { - var status by mutableStateOf(status) - var duration by mutableStateOf(duration) - var cached by mutableStateOf(cached) -} - @Composable fun InstallGroup( name: String, isCurrent: Boolean, - subSteps: ImmutableList, + subSteps: ImmutableList, onClick: () -> Unit, ) { val status = when { diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt index a229cb58..ab9162b4 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt @@ -5,66 +5,53 @@ import android.app.Application import android.os.Build import android.util.Log import androidx.compose.runtime.* -import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import com.aliucord.manager.BuildConfig import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.KotlinInstallContainer -import com.aliucord.manager.installer.util.* -import com.aliucord.manager.manager.* +import com.aliucord.manager.installer.steps.KotlinInstallRunner +import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.ui.util.toUnsafeImmutable import com.aliucord.manager.util.* -import com.github.diamondminer88.zip.* +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap import kotlinx.coroutines.* import java.text.SimpleDateFormat import java.util.Date -import java.util.concurrent.atomic.AtomicBoolean class InstallModel( private val application: Application, private val paths: PathManager, -) : ScreenModel { - private val installationRunning = AtomicBoolean(false) +) : StateScreenModel(InstallScreenState.Pending) { + private lateinit var startTime: Date + private var installJob: Job? = null - var returnToHome by mutableStateOf(false) - - var isFinished by mutableStateOf(false) - private set - - var stacktrace by mutableStateOf("") + var installSteps by mutableStateOf>?>(null) private set - private val debugInfo: String - get() = """ - Aliucord Manager ${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()} - - Failed on: ${currentStep?.name} - """.trimIndent() + init { + restart() + } fun copyDebugToClipboard() { - val text = "$debugInfo\n\n$stacktrace" - // TODO: remove this useless replace - .replace("(\\\\*~_)".toRegex(), "\\$1") + val content = (state.value as? InstallScreenState.Failed)?.failureLog + ?: return - application.copyToClipboard(text) + application.copyToClipboard(content) application.showToast(R.string.action_copied) } - private var debugLogPath by mutableStateOf(null) + fun saveFailureLog() { + val failureLog = (state.value as? InstallScreenState.Failed)?.failureLog + ?: return - @SuppressLint("SimpleDateFormat") - fun saveDebugToFile() { - val name = if (debugLogPath != null) { - debugLogPath!! - } else { - "Aliucord Manager ${SimpleDateFormat("yyyy-MM-dd hh-mm-s a").format(Date())}.log" - .also { debugLogPath = it } - } + @SuppressLint("SimpleDateFormat") + val formattedDate = SimpleDateFormat("yyyy-MM-dd hh-mm-s a").format(startTime) + val fileName = "Aliucord Manager $formattedDate.log" - application.saveFile(name, "$debugInfo\n\n$stacktrace") + application.saveFile(fileName, failureLog) } fun clearCache() { @@ -72,36 +59,72 @@ class InstallModel( application.showToast(R.string.action_cleared_cache) } - private val installJob = screenModelScope.launch(Dispatchers.Main) { - if (installationRunning.getAndSet(true)) { - return@launch - } + fun restart() { + installJob?.cancel("Manual cancellation") + installSteps = null + + startTime = Date() + mutableState.value = InstallScreenState.Working + + val newInstallJob = screenModelScope.launch { + val runner = KotlinInstallRunner() - withContext(Dispatchers.IO) { - try { - installKotlin() + installSteps = runner.steps.groupBy { it.group } + .mapValues { it.value.toUnsafeImmutable() } + .toUnsafeImmutable() - isFinished = true - delay(20000) - returnToHome = true - } catch (t: Throwable) { - stacktrace = Log.getStackTraceString(t) + // Execute all the steps and catch any errors + when (val error = runner.executeAll()) { + // Successfully installed + null -> { + mutableState.value = InstallScreenState.Success - Log.e( - BuildConfig.TAG, - "$debugInfo\n\n${Log.getStackTraceString(t)}" - ) + // Wait 20s before returning to Home + delay(20_000) + mutableState.value = InstallScreenState.CloseScreen + } + + else -> { + Log.e(BuildConfig.TAG, "Failed to perform installation process", error) + + mutableState.value = InstallScreenState.Failed(failureLog = getFailureInfo(error)) + } } + } + + newInstallJob.invokeOnCompletion { error -> + when (error) { + // Successfully executed, already handled above + null -> {} - installationRunning.set(false) + // Job was cancelled before being able to finish setting state + is CancellationException -> { + Log.w(BuildConfig.TAG, "Installation was cancelled before completing", error) + mutableState.value = InstallScreenState.CloseScreen + } + + // This should never happen, all install errors are caught + else -> throw error + } } + + installJob = newInstallJob } - private suspend fun installKotlin() { - val error = KotlinInstallContainer().executeAll() + private fun getFailureInfo(stacktrace: Throwable): String { + val gitChanges = if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes present)" else "" + val soc = if (Build.VERSION.SDK_INT >= 31) (Build.SOC_MANUFACTURER + ' ' + Build.SOC_MODEL) else "Unknown" - if (error != null) { - Log.e(BuildConfig.TAG, "Failed to perform installation process", error) - } + return """ + Aliucord Manager v${BuildConfig.VERSION_NAME} + Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges + + Running Android ${Build.VERSION.RELEASE}, API level ${Build.VERSION.SDK_INT} + Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()} + Device: ${Build.MANUFACTURER} - ${Build.MODEL} (${Build.DEVICE}) + SOC: $soc + + ${Log.getStackTraceString(stacktrace)} + """.trimIndent() } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt index 8d3ce992..64a4db4c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt @@ -24,12 +24,10 @@ import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.ui.components.back import com.aliucord.manager.ui.components.dialogs.InstallerAbortDialog import com.aliucord.manager.ui.components.installer.InstallGroup -import com.aliucord.manager.ui.components.installer.InstallStatus -import com.aliucord.manager.ui.screens.install.InstallModel.InstallStepGroup -import kotlinx.collections.immutable.toImmutableList class InstallScreen : Screen { override val key = "Install" @@ -38,18 +36,15 @@ class InstallScreen : Screen { override fun Content() { val navigator = LocalNavigator.currentOrThrow val model = getScreenModel() + val state = model.state.collectAsState() - var expandedGroup by remember { mutableStateOf(null) } - - if (model.returnToHome) - navigator.back(null) - - LaunchedEffect(model.currentStep) { - expandedGroup = model.currentStep?.group + LaunchedEffect(model.state) { + if (model.state.value is InstallScreenState.CloseScreen) + navigator.back(currentActivity = null) } - // Exit warning dialog - var showAbortWarning by remember { mutableStateOf(false) } + // Exit warning dialog (cancel itself if install process state changes) + var showAbortWarning by remember(model.state.collectAsState()) { mutableStateOf(false) } if (showAbortWarning) { InstallerAbortDialog( onDismiss = { showAbortWarning = false }, @@ -82,13 +77,7 @@ class InstallScreen : Screen { } ) { paddingValues -> Column(Modifier.padding(paddingValues)) { - val isCurrentlyProcessing by remember { - derivedStateOf { - model.steps[model.currentStep]?.status == InstallStatus.ONGOING - } - } - - if (isCurrentlyProcessing) { + if (state.value is InstallScreenState.Working) { LinearProgressIndicator( modifier = Modifier .fillMaxWidth() @@ -104,16 +93,20 @@ class InstallScreen : Screen { .fillMaxWidth() .padding(16.dp) ) { - for (group in InstallStepGroup.entries) key(group) { - InstallGroup( - name = stringResource(group.nameResId), - isCurrent = expandedGroup == group, - onClick = { expandedGroup = group }, - subSteps = model.getSteps(group).toImmutableList(), - ) + var expandedGroup by remember { mutableStateOf(StepGroup.Prepare) } + + model.installSteps?.let { groupedSteps -> + for ((group, steps) in groupedSteps.entries) key(group) { + InstallGroup( + name = stringResource(group.localizedName), + isCurrent = group == expandedGroup, + onClick = remember { { expandedGroup = group } }, + subSteps = steps, + ) + } } - if (model.isFinished && model.stacktrace.isEmpty()) { + if (state.value.isFinished) { Row( horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth() @@ -124,20 +117,8 @@ class InstallScreen : Screen { } } - if (model.stacktrace.isNotEmpty()) { - SelectionContainer { - Text( - text = model.stacktrace, - style = MaterialTheme.typography.labelSmall, - fontFamily = FontFamily.Monospace, - softWrap = false, - modifier = Modifier - .clip(RoundedCornerShape(10.dp)) - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp)) - .padding(10.dp) - .horizontalScroll(rememberScrollState()) - ) - } + if (state.value is InstallScreenState.Failed) { + val failureLog = (state.value as InstallScreenState.Failed).failureLog Row( horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End), @@ -149,7 +130,7 @@ class InstallScreen : Screen { Spacer(Modifier.weight(1f, true)) - OutlinedButton(onClick = model::saveDebugToFile) { + OutlinedButton(onClick = model::saveFailureLog) { Text(stringResource(R.string.installer_save_file)) } @@ -157,6 +138,20 @@ class InstallScreen : Screen { Text(stringResource(R.string.action_copy)) } } + + SelectionContainer { + Text( + text = failureLog, + style = MaterialTheme.typography.labelSmall, + fontFamily = FontFamily.Monospace, + softWrap = false, + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp)) + .padding(10.dp) + .horizontalScroll(rememberScrollState()) + ) + } } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreenState.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreenState.kt new file mode 100644 index 00000000..f9fab47a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreenState.kt @@ -0,0 +1,16 @@ +package com.aliucord.manager.ui.screens.install + +sealed interface InstallScreenState { + data object Pending : InstallScreenState + data object Working : InstallScreenState + data object Success : InstallScreenState + + data class Failed( + val failureLog: String, + ) : InstallScreenState + + data object CloseScreen : InstallScreenState + + val isFinished: Boolean + get() = this !is Pending && this !is Working +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/util/UnsafeImmutables.kt b/app/src/main/kotlin/com/aliucord/manager/ui/util/UnsafeImmutables.kt new file mode 100644 index 00000000..6d27bbc3 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/util/UnsafeImmutables.kt @@ -0,0 +1,29 @@ +@file:Suppress("unused") + +package com.aliucord.manager.ui.util + +import kotlinx.collections.immutable.* +import kotlinx.collections.immutable.adapters.* + +/* + * Compose-stable wrappers over a list for performance. + * + * This does NOT guarantee stability. It is merely a stable wrapper over another collection, + * and assumes the user knows that it shouldn't change through crucial parts of rendering. + */ + +fun Collection.toUnsafeImmutable(): ImmutableCollection = + ImmutableCollectionAdapter(this) + +fun List.toUnsafeImmutable(): ImmutableList = + ImmutableListAdapter(this) + +fun Set.toUnsafeImmutable(): ImmutableSet = + ImmutableSetAdapter(this) + +fun Map.toUnsafeImmutable(): ImmutableMap = + ImmutableMapAdapter(this) + +fun emptyImmutableList(): ImmutableList = persistentListOf() +fun emptyImmutableSet(): ImmutableSet = persistentSetOf() +fun emptyImmutableMap(): ImmutableMap = persistentMapOf() From f0b83bb34a524db0b29bc6000527c60fdfc9b868 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Sun, 4 Feb 2024 22:00:03 -0800 Subject: [PATCH 6/9] reformat --- .../com/aliucord/manager/installer/steps/base/DownloadStep.kt | 2 +- .../kotlin/com/aliucord/manager/installer/steps/base/Step.kt | 2 +- .../aliucord/manager/installer/steps/install/AlignmentStep.kt | 2 +- .../com/aliucord/manager/installer/steps/install/CleanupStep.kt | 2 +- .../com/aliucord/manager/installer/steps/install/InstallStep.kt | 2 +- .../com/aliucord/manager/installer/steps/install/SigningStep.kt | 2 +- .../aliucord/manager/installer/steps/patch/AddAliuhookStep.kt | 2 +- .../aliucord/manager/installer/steps/patch/AddInjectorStep.kt | 2 +- .../manager/installer/steps/patch/CopyDependenciesStep.kt | 2 +- .../aliucord/manager/installer/steps/patch/PatchManifestStep.kt | 2 +- .../aliucord/manager/installer/steps/patch/ReplaceIconStep.kt | 2 +- .../manager/installer/steps/prepare/DowngradeCheckStep.kt | 2 +- .../aliucord/manager/installer/steps/prepare/FetchInfoStep.kt | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt index f261926c..2fe08d57 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt @@ -3,8 +3,8 @@ 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.StepRunner 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 diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt index 06b2054c..72b60b2a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt @@ -2,8 +2,8 @@ package com.aliucord.manager.installer.steps.base import androidx.annotation.StringRes import androidx.compose.runtime.* -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import org.koin.core.time.measureTimedValue import kotlin.math.roundToInt diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt index 4f374ce4..b4e9d758 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/AlignmentStep.kt @@ -2,8 +2,8 @@ package com.aliucord.manager.installer.steps.install import android.os.Build import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.StepState import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt index 30de3ed6..d57635f2 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/CleanupStep.kt @@ -1,8 +1,8 @@ package com.aliucord.manager.installer.steps.install import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.StepState import com.aliucord.manager.manager.PathManager diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt index 30d6460e..026b4c9c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt @@ -2,8 +2,8 @@ package com.aliucord.manager.installer.steps.install import android.app.Application import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep import com.aliucord.manager.installer.util.installApks diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt index 1352d599..bf84aced 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/SigningStep.kt @@ -1,8 +1,8 @@ package com.aliucord.manager.installer.steps.install import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep import com.aliucord.manager.installer.util.Signer diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt index d2feab17..b10f997f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddAliuhookStep.kt @@ -2,8 +2,8 @@ package com.aliucord.manager.installer.steps.patch import android.os.Build import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.download.DownloadAliuhookStep import com.github.diamondminer88.zip.ZipReader diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt index 567e1018..e60ce32d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/AddInjectorStep.kt @@ -1,8 +1,8 @@ package com.aliucord.manager.installer.steps.patch import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.download.DownloadInjectorStep import com.aliucord.manager.installer.steps.download.DownloadKotlinStep diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt index 1ed5d7cf..309f402d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/CopyDependenciesStep.kt @@ -1,8 +1,8 @@ package com.aliucord.manager.installer.steps.patch import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.download.DownloadDiscordStep import com.aliucord.manager.manager.PathManager diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt index f7e3d219..4f50ada3 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/PatchManifestStep.kt @@ -1,8 +1,8 @@ package com.aliucord.manager.installer.steps.patch import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.util.ManifestPatcher import com.aliucord.manager.manager.PreferencesManager diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt index 30b5326a..90597035 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt @@ -3,8 +3,8 @@ package com.aliucord.manager.installer.steps.patch import android.content.Context import androidx.compose.runtime.Stable import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.StepState import com.aliucord.manager.manager.PreferencesManager diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt index 566809aa..223af6f6 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/DowngradeCheckStep.kt @@ -2,8 +2,8 @@ package com.aliucord.manager.installer.steps.prepare import android.content.Context import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.StepState import com.aliucord.manager.installer.util.uninstallApk diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt index 917eb476..19dff13d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/prepare/FetchInfoStep.kt @@ -2,8 +2,8 @@ package com.aliucord.manager.installer.steps.prepare import androidx.compose.runtime.Stable import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepGroup +import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.network.dto.Version import com.aliucord.manager.network.service.AliucordGithubService From ed03128247ecb6a6b6ee254d9a66325e027eb202 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Mon, 5 Feb 2024 22:29:01 -0800 Subject: [PATCH 7/9] finish adapting install ui --- .../manager/installer/steps/StepRunner.kt | 6 +- .../installer/steps/base/DownloadStep.kt | 2 +- .../manager/installer/steps/base/Step.kt | 5 +- .../manager/installer/steps/base/StepState.kt | 6 +- .../manager/manager/DownloadManager.kt | 12 +- .../ui/components/installer/InstallGroup.kt | 176 ------------------ .../ui/screens/install/InstallModel.kt | 6 +- .../ui/screens/install/InstallScreen.kt | 8 +- .../install/components/StepGroupCard.kt | 104 +++++++++++ .../ui/screens/install/components/StepItem.kt | 44 +++++ .../install/components/StepStatusIcon.kt | 59 ++++++ .../aliucord/manager/ui/util/IfModifier.kt | 22 +++ app/src/main/res/values/strings.xml | 5 +- 13 files changed, 261 insertions(+), 194 deletions(-) delete mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepGroupCard.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepItem.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepStatusIcon.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/util/IfModifier.kt diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt index cc854d5b..800fe175 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt @@ -20,7 +20,7 @@ abstract class StepRunner : KoinComponent { inline fun getStep(): T { val step = steps.asSequence() .filterIsInstance() - .filter { it.state == StepState.Success } + .filter { it.state.isFinished } .firstOrNull() if (step == null) { @@ -37,8 +37,8 @@ abstract class StepRunner : KoinComponent { // Add delay for human psychology and // better group visibility in UI (the active group can change way too fast) - if (!preferences.devMode && step.durationMs < 1000) { - delay(1000L - step.durationMs) + if (!preferences.devMode && step.durationMs < 500) { + delay(500L - step.durationMs) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt index 2fe08d57..c35e6253 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt @@ -79,7 +79,7 @@ abstract class DownloadStep : Step(), KoinComponent { } is DownloadManager.Result.Cancelled -> - state = StepState.Cancelled + state = StepState.Error } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt index 72b60b2a..250d87e2 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt @@ -63,7 +63,10 @@ abstract class Step { val (error, executionTimeMs) = measureTimedValue { try { execute(container) - state = StepState.Success + + if (state != StepState.Skipped) + state = StepState.Success + null } catch (t: Throwable) { state = StepState.Error diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt index 1024f3f8..eac39e79 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt @@ -5,6 +5,8 @@ enum class StepState { Running, Success, Error, - Skipped, - Cancelled, // TODO: something like the discord dnd sign except its not red, but gray maybe + Skipped; + + val isFinished: Boolean + get() = this == Success || this == Error || this == Skipped } diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt index 8b58c948..46bce400 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt @@ -4,6 +4,8 @@ import android.app.Application import android.app.DownloadManager import android.database.Cursor import android.net.Uri +import android.os.Build +import android.util.Log import androidx.annotation.StringRes import androidx.core.content.getSystemService import com.aliucord.manager.BuildConfig @@ -40,10 +42,16 @@ class DownloadManager(application: Application) { .setTitle("Aliucord Manager") .setDescription("Downloading ${out.name}...") .setDestinationUri(Uri.fromFile(out)) - .setAllowedOverMetered(true) - .setAllowedOverRoaming(true) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) .addRequestHeader("User-Agent", "Aliucord Manager/${BuildConfig.VERSION_NAME}") + .apply { + // Disable gzip on emulator due to https compression bug + println(Build.PRODUCT) + // if (Build.PRODUCT == "google_sdk") { + Log.i(BuildConfig.TAG, "Disabling DownloadManager compression") + addRequestHeader("Accept-Encoding", null) + // } + } .let(downloadManager::enqueue) // Repeatedly request download state until it is finished diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt deleted file mode 100644 index 6236a798..00000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt +++ /dev/null @@ -1,176 +0,0 @@ -package com.aliucord.manager.ui.components.installer - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* -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.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.* -import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.base.Step -import kotlinx.collections.immutable.ImmutableList -import kotlin.math.floor - -@Composable -fun InstallGroup( - name: String, - isCurrent: Boolean, - subSteps: ImmutableList, - onClick: () -> Unit, -) { - val status = when { - subSteps.all { it.status == InstallStatus.QUEUED } -> - InstallStatus.QUEUED - - subSteps.all { it.status == InstallStatus.SUCCESSFUL } -> - InstallStatus.SUCCESSFUL - - subSteps.any { it.status == InstallStatus.ONGOING } -> - InstallStatus.ONGOING - - else -> InstallStatus.UNSUCCESSFUL - } - - Column( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .run { - if (isCurrent) { - background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) - } else this - } - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .clickable(true, onClick = onClick) - .fillMaxWidth() - .padding(16.dp) - ) { - StepIcon(status, 24.dp) - - Text(text = name) - - Spacer(modifier = Modifier.weight(1f)) - - if (status != InstallStatus.ONGOING && status != InstallStatus.QUEUED) Text( - "%.2fs".format(subSteps.map { it.duration }.sum()), - style = MaterialTheme.typography.labelMedium - ) - - if (isCurrent) { - Icon( - painter = painterResource(R.drawable.ic_arrow_up_small), - contentDescription = stringResource(R.string.action_collapse) - ) - } else { - Icon( - painter = painterResource(R.drawable.ic_arrow_down_small), - contentDescription = stringResource(R.string.action_expand) - ) - } - } - - AnimatedVisibility(visible = isCurrent) { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .background(MaterialTheme.colorScheme.background.copy(0.6f)) - .fillMaxWidth() - .padding(16.dp) - .padding(start = 4.dp) - ) { - subSteps.forEach { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - StepIcon(it.status, size = 18.dp) - - Text( - text = stringResource(it.nameResId), - style = MaterialTheme.typography.labelLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, true), - ) - - if (it.status != InstallStatus.ONGOING && it.status != InstallStatus.QUEUED) { - if (it.cached) { - val style = MaterialTheme.typography.labelSmall.copy( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), - fontStyle = FontStyle.Italic, - fontSize = 11.sp - ) - Text( - text = stringResource(R.string.installer_cached), - style = style, - maxLines = 1, - ) - } - - Text( - text = "%.2fs".format(it.duration), - style = MaterialTheme.typography.labelSmall, - maxLines = 1, - ) - } - } - } - } - } - } -} - -@Composable -private fun StepIcon(status: InstallStatus, size: Dp) { - val strokeWidth = Dp(floor(size.value / 10) + 1) - val context = LocalContext.current - - when (status) { - InstallStatus.ONGOING -> CircularProgressIndicator( - strokeWidth = strokeWidth, - modifier = Modifier - .size(size) - .semantics { - contentDescription = context.getString(R.string.status_ongoing) - } - ) - - InstallStatus.SUCCESSFUL -> Icon( - painter = painterResource(R.drawable.ic_check_circle), - contentDescription = stringResource(R.string.status_success), - tint = Color(0xFF59B463), - modifier = Modifier.size(size) - ) - - InstallStatus.UNSUCCESSFUL -> Icon( - painter = painterResource(R.drawable.ic_canceled), - contentDescription = stringResource(R.string.status_failed), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(size) - ) - - InstallStatus.QUEUED -> Icon( - painter = painterResource(R.drawable.ic_circle), - contentDescription = stringResource(R.string.status_queued), - tint = MaterialTheme.colorScheme.onSurface.copy(0.4f), - modifier = Modifier.size(size) - ) - } -} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt index ab9162b4..cedb5602 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt @@ -115,7 +115,7 @@ class InstallModel( val gitChanges = if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes present)" else "" val soc = if (Build.VERSION.SDK_INT >= 31) (Build.SOC_MANUFACTURER + ' ' + Build.SOC_MODEL) else "Unknown" - return """ + val header = """ Aliucord Manager v${BuildConfig.VERSION_NAME} Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges @@ -123,8 +123,8 @@ class InstallModel( Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()} Device: ${Build.MANUFACTURER} - ${Build.MODEL} (${Build.DEVICE}) SOC: $soc - - ${Log.getStackTraceString(stacktrace)} """.trimIndent() + + return header + "\n\n" + Log.getStackTraceString(stacktrace) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt index 64a4db4c..ca6ee4fb 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt @@ -27,7 +27,7 @@ import com.aliucord.manager.R import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.ui.components.back import com.aliucord.manager.ui.components.dialogs.InstallerAbortDialog -import com.aliucord.manager.ui.components.installer.InstallGroup +import com.aliucord.manager.ui.screens.install.components.StepGroupCard class InstallScreen : Screen { override val key = "Install" @@ -97,11 +97,11 @@ class InstallScreen : Screen { model.installSteps?.let { groupedSteps -> for ((group, steps) in groupedSteps.entries) key(group) { - InstallGroup( + StepGroupCard( name = stringResource(group.localizedName), - isCurrent = group == expandedGroup, - onClick = remember { { expandedGroup = group } }, subSteps = steps, + isExpanded = expandedGroup == group, + onExpand = { expandedGroup = group }, ) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepGroupCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepGroupCard.kt new file mode 100644 index 00000000..1d1471e8 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepGroupCard.kt @@ -0,0 +1,104 @@ +package com.aliucord.manager.ui.screens.install.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.base.StepState +import com.aliucord.manager.ui.util.thenIf +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun StepGroupCard( + name: String, + subSteps: ImmutableList, + isExpanded: Boolean, + onExpand: () -> Unit, +) { + val groupState by remember { + derivedStateOf { + when { + // If all steps are pending then show pending + subSteps.all { it.state == StepState.Pending } -> StepState.Pending + // If any step has finished with an error then default to error + subSteps.any { it.state == StepState.Error } -> StepState.Error + // If all steps have finished as Skipped/Success then show success + subSteps.all { it.state.isFinished } -> StepState.Success + + else -> StepState.Running + } + } + } + + LaunchedEffect(groupState) { + if (groupState != StepState.Pending) + onExpand() + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .thenIf(isExpanded) { + background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) + } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .clickable(true, onClick = onExpand) + .fillMaxWidth() + .padding(16.dp) + ) { + StepStatusIcon(groupState, 24.dp) + + Text(text = name) + + Spacer(modifier = Modifier.weight(1f)) + + if (groupState.isFinished) Text( + "%.2fs".format(subSteps.sumOf { it.durationMs } / 1000f), + style = MaterialTheme.typography.labelMedium + ) + + if (isExpanded) { + Icon( + painter = painterResource(R.drawable.ic_arrow_up_small), + contentDescription = stringResource(R.string.action_collapse) + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_arrow_down_small), + contentDescription = stringResource(R.string.action_expand) + ) + } + } + + AnimatedVisibility(visible = isExpanded) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .background(MaterialTheme.colorScheme.background.copy(0.6f)) + .fillMaxWidth() + .padding(16.dp) + .padding(start = 4.dp) + ) { + for (step in subSteps) key(step) { + StepItem(step) + } + } + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepItem.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepItem.kt new file mode 100644 index 00000000..95507357 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepItem.kt @@ -0,0 +1,44 @@ +package com.aliucord.manager.ui.screens.install.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.aliucord.manager.installer.steps.base.Step + +@Composable +fun StepItem( + step: Step, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier, + ) { + StepStatusIcon(step.state, size = 18.dp) + + Text( + text = stringResource(step.localizedName), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, true), + ) + + // TODO: live step duration counter + if (step.state.isFinished) { + Text( + text = "%.2fs".format(step.durationMs / 1000f), + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + ) + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepStatusIcon.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepStatusIcon.kt new file mode 100644 index 00000000..983c52a8 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepStatusIcon.kt @@ -0,0 +1,59 @@ +package com.aliucord.manager.ui.screens.install.components + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.base.StepState +import kotlin.math.floor + +@Composable +fun StepStatusIcon(status: StepState, size: Dp) { + val strokeWidth = Dp(floor(size.value / 10) + 1) + val context = LocalContext.current + + when (status) { + StepState.Pending -> Icon( + painter = painterResource(R.drawable.ic_circle), + contentDescription = stringResource(R.string.status_queued), + tint = MaterialTheme.colorScheme.onSurface.copy(0.4f), + modifier = Modifier.size(size) + ) + + StepState.Running -> CircularProgressIndicator( + strokeWidth = strokeWidth, + modifier = Modifier + .size(size) + .semantics { contentDescription = context.getString(R.string.status_ongoing) } + ) + + StepState.Success -> Icon( + painter = painterResource(R.drawable.ic_check_circle), + contentDescription = stringResource(R.string.status_success), + tint = Color(0xFF59B463), + modifier = Modifier.size(size) + ) + + StepState.Error -> Icon( + painter = painterResource(R.drawable.ic_canceled), + contentDescription = stringResource(R.string.status_failed), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(size) + ) + + StepState.Skipped -> Icon( + painter = painterResource(R.drawable.ic_check_circle), + contentDescription = stringResource(R.string.status_skipped), + tint = Color(0xFFAEAEAE), + modifier = Modifier.size(size) + ) + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/util/IfModifier.kt b/app/src/main/kotlin/com/aliucord/manager/ui/util/IfModifier.kt new file mode 100644 index 00000000..7e82b946 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/util/IfModifier.kt @@ -0,0 +1,22 @@ +@file:Suppress("unused") + +package com.aliucord.manager.ui.util + +import androidx.compose.ui.Modifier + +/** + * Apply additional modifiers if [value] is not null. + */ +inline fun Modifier.thenIf(value: T?, block: Modifier.(T) -> Modifier): Modifier = + value?.let { block(it) } ?: this + +/** + * Apply additional modifiers if [predicate] is true. + */ +inline fun Modifier.thenIf(predicate: Boolean, block: Modifier.() -> Modifier): Modifier { + return if (predicate) { + block() + } else { + this + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b8265a0..7fdb02c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -126,10 +126,11 @@ Installing APKs Cleaning up - Success + Queued Ongoing + Skipped + Success Failed - Queued Really exit? Are you sure you really want to abort an in-progress installation? Cached files will be cleared to avoid corruption. From f5947f20111d1430207bd968d69aaf043017d420 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Tue, 6 Feb 2024 22:39:00 -0800 Subject: [PATCH 8/9] small fixes --- .../manager/installer/steps/StepRunner.kt | 15 ++++++++++----- .../aliucord/manager/installer/steps/base/Step.kt | 6 +++++- .../aliucord/manager/manager/DownloadManager.kt | 7 +++---- .../manager/ui/screens/install/InstallModel.kt | 4 ++++ .../manager/ui/screens/install/InstallScreen.kt | 9 ++++++++- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt index 800fe175..bac13c98 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt @@ -1,13 +1,19 @@ package com.aliucord.manager.installer.steps import com.aliucord.manager.installer.steps.base.Step -import com.aliucord.manager.installer.steps.base.StepState 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() @@ -35,10 +41,9 @@ abstract class StepRunner : KoinComponent { val error = step.executeCatching(this@StepRunner) if (error != null) return error - // Add delay for human psychology and - // better group visibility in UI (the active group can change way too fast) - if (!preferences.devMode && step.durationMs < 500) { - delay(500L - step.durationMs) + // Skip minimum run time when in dev mode + if (!preferences.devMode && step.durationMs < MINIMUM_STEP_DELAY) { + delay(MINIMUM_STEP_DELAY - step.durationMs) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt index 250d87e2..1b870d1c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt @@ -4,6 +4,8 @@ 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 @@ -62,7 +64,9 @@ abstract class Step { // Execute this steps logic while timing it val (error, executionTimeMs) = measureTimedValue { try { - execute(container) + withContext(Dispatchers.Default) { + execute(container) + } if (state != StepState.Skipped) state = StepState.Success diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt index 46bce400..55b7993d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt @@ -46,11 +46,10 @@ class DownloadManager(application: Application) { .addRequestHeader("User-Agent", "Aliucord Manager/${BuildConfig.VERSION_NAME}") .apply { // Disable gzip on emulator due to https compression bug - println(Build.PRODUCT) - // if (Build.PRODUCT == "google_sdk") { - Log.i(BuildConfig.TAG, "Disabling DownloadManager compression") + if (Build.FINGERPRINT.contains("emulator")) { + Log.d(BuildConfig.TAG, "Disabling DownloadManager compression") addRequestHeader("Accept-Encoding", null) - // } + } } .let(downloadManager::enqueue) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt index cedb5602..951cff09 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt @@ -73,6 +73,10 @@ class InstallModel( .mapValues { it.value.toUnsafeImmutable() } .toUnsafeImmutable() + // Intentionally delay to show the state change of the first step in UI when it runs + // without it, on a fast internet it just immediately shows as "Success" + delay(600) + // Execute all the steps and catch any errors when (val error = runner.executeAll()) { // Successfully installed diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt index ca6ee4fb..840d032e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt @@ -65,6 +65,7 @@ class InstallScreen : Screen { title = { Text(stringResource(R.string.installer)) }, navigationIcon = { IconButton( + // TODO: only show warning when in progress onClick = { showAbortWarning = true }, ) { Icon( @@ -93,7 +94,13 @@ class InstallScreen : Screen { .fillMaxWidth() .padding(16.dp) ) { - var expandedGroup by remember { mutableStateOf(StepGroup.Prepare) } + var expandedGroup by remember { mutableStateOf(StepGroup.Prepare) } + + // Close all groups when successfully finished everything + LaunchedEffect(state.value) { + if (state.value == InstallScreenState.Success) + expandedGroup = null + } model.installSteps?.let { groupedSteps -> for ((group, steps) in groupedSteps.entries) key(group) { From f16184cb5b6a4030b4b25a32cc6a05f1a5d296ea Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Tue, 6 Feb 2024 23:06:59 -0800 Subject: [PATCH 9/9] fix abort warning --- .../ui/screens/install/InstallScreen.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt index 840d032e..9cfcc3bc 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt @@ -43,8 +43,20 @@ class InstallScreen : Screen { navigator.back(currentActivity = null) } - // Exit warning dialog (cancel itself if install process state changes) + // Exit warning dialog (dismiss itself if install process state changes, esp. for Success) var showAbortWarning by remember(model.state.collectAsState()) { mutableStateOf(false) } + + // Only show exit warning if currently working + val onTryExit: () -> Unit = remember { + { + if (state.value == InstallScreenState.Working) { + showAbortWarning = true + } else { + navigator.back(currentActivity = null) + } + } + } + if (showAbortWarning) { InstallerAbortDialog( onDismiss = { showAbortWarning = false }, @@ -54,9 +66,7 @@ class InstallScreen : Screen { }, ) } else { - BackHandler { - showAbortWarning = true - } + BackHandler(onBack = onTryExit) } Scaffold( @@ -64,10 +74,7 @@ class InstallScreen : Screen { TopAppBar( title = { Text(stringResource(R.string.installer)) }, navigationIcon = { - IconButton( - // TODO: only show warning when in progress - onClick = { showAbortWarning = true }, - ) { + IconButton(onClick = onTryExit) { Icon( painter = painterResource(R.drawable.ic_back), contentDescription = stringResource(R.string.navigation_back),