diff --git a/app/libs/lspatch.aar b/app/libs/lspatch.aar new file mode 100644 index 0000000..727c4ee Binary files /dev/null and b/app/libs/lspatch.aar differ diff --git a/app/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so b/app/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so new file mode 100644 index 0000000..d817464 Binary files /dev/null and b/app/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so differ diff --git a/app/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so b/app/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so new file mode 100644 index 0000000..5db43df Binary files /dev/null and b/app/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so differ diff --git a/app/src/main/assets/lspatch/so/x86/liblspatch.so b/app/src/main/assets/lspatch/so/x86/liblspatch.so new file mode 100644 index 0000000..e2634c2 Binary files /dev/null and b/app/src/main/assets/lspatch/so/x86/liblspatch.so differ diff --git a/app/src/main/assets/lspatch/so/x86_64/liblspatch.so b/app/src/main/assets/lspatch/so/x86_64/liblspatch.so new file mode 100644 index 0000000..ff08cb1 Binary files /dev/null and b/app/src/main/assets/lspatch/so/x86_64/liblspatch.so differ diff --git a/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt b/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt index a7decaa..b76862f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt @@ -48,6 +48,8 @@ class ManagerApplication : Application() { singleOf(::HttpService) singleOf(::AliucordGithubService) singleOf(::AliucordMavenService) + singleOf(::RNATrackerService) + singleOf(::WintryGithubService) }) // UI Models 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 0227147..28c15ba 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt @@ -116,6 +116,10 @@ class PathManager( fun cachedKotlinDex() = patchingDownloadDir .resolve("kotlin.dex") + fun cachedWintryXposed(version: SemVer) = patchingDownloadDir + .resolve("wt-xposed") + .resolve("$version.apk") + /** * The temporary working directory of a currently executing patching process. */ diff --git a/app/src/main/kotlin/com/aliucord/manager/network/models/GithubRelease.kt b/app/src/main/kotlin/com/aliucord/manager/network/models/GithubRelease.kt index b36baad..a9bfd84 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/models/GithubRelease.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/models/GithubRelease.kt @@ -10,6 +10,8 @@ data class GithubRelease( val assets: List, @SerialName("tag_name") val tagName: String, + @SerialName("name") + val name: String, ) { @Serializable data class GithubReleaseAssets( diff --git a/app/src/main/kotlin/com/aliucord/manager/network/models/RNATrackerIndex.kt b/app/src/main/kotlin/com/aliucord/manager/network/models/RNATrackerIndex.kt new file mode 100644 index 0000000..ee25969 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/network/models/RNATrackerIndex.kt @@ -0,0 +1,15 @@ +package com.aliucord.manager.network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class RNATrackerIndex( + val latest: Versions, +) { + @Serializable + data class Versions( + val alpha: Int, + val beta: Int, + val stable: Int, + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/network/services/RNATrackerService.kt b/app/src/main/kotlin/com/aliucord/manager/network/services/RNATrackerService.kt new file mode 100644 index 0000000..f9442f9 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/network/services/RNATrackerService.kt @@ -0,0 +1,20 @@ +package com.aliucord.manager.network.services + +import com.aliucord.manager.network.models.RNATrackerIndex +import io.ktor.client.request.url +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class RNATrackerService( + private val httpService: HttpService, +) { + suspend fun getLatestDiscordVersions() = withContext(Dispatchers.IO) { + httpService.request { + url("${BASE_URL}/tracker/index") + } + } + + companion object { + const val BASE_URL = "https://tracker.vendetta.rocks" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/network/services/WintryGithubService.kt b/app/src/main/kotlin/com/aliucord/manager/network/services/WintryGithubService.kt new file mode 100644 index 0000000..75f4209 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/network/services/WintryGithubService.kt @@ -0,0 +1,37 @@ +package com.aliucord.manager.network.services + +import com.aliucord.manager.network.models.GithubRelease +import com.aliucord.manager.network.utils.ApiResponse +import io.ktor.client.request.header +import io.ktor.client.request.url +import io.ktor.http.HttpHeaders + +class WintryGithubService( + private val http: HttpService, +) { + /** + * Fetches all the Manager releases with a 60s local cache. + */ + suspend fun getManagerReleases(): ApiResponse> { + return http.request { + url("https://api.github.com/repos/$ORG/$MANAGER_REPO/releases") + header(HttpHeaders.CacheControl, "public, max-age=60, s-maxage=60") + } + } + + /** + * Fetches the latest Xposed release with a 60s local cache. + */ + suspend fun getLatestXposedRelease(): ApiResponse { + return http.request { + url("https://api.github.com/repos/$ORG/$XPOSED_REPO/releases/latest") + header(HttpHeaders.CacheControl, "public, max-age=60, s-maxage=60") + } + } + + companion object { + const val ORG = "wtcord" + const val MANAGER_REPO = "wt-manager" + const val XPOSED_REPO = "wt-xposed" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/InstallMetadata.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/InstallMetadata.kt index 4ca3d71..1be68db 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/InstallMetadata.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/InstallMetadata.kt @@ -5,7 +5,7 @@ import com.aliucord.manager.ui.screens.patchopts.PatchOptions import kotlinx.serialization.Serializable /** - * Data stored inside patched APKs as "aliucord.json" in order to preserve install-time information about Aliucord and the Manager. + * Data stored inside patched APKs as ~~"aliucord.json"~~ "wintry.json" in order to preserve install-time information about Aliucord and the Manager. */ @Serializable data class InstallMetadata( @@ -38,4 +38,14 @@ data class InstallMetadata( * Version of the smali patches that were applied onto the APK. */ val patchesVersion: SemVer?, + + /** + * Version of LSPatch used to inject Xposed module into the APK. + */ + val lspatchVersion: Int?, + + /** + * Version of WintryXposed embedded into the APK. + */ + val wintryXposedVersion: SemVer?, ) diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/WintryPatchRunner.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/WintryPatchRunner.kt index 8b8a0b5..f0da3c2 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/WintryPatchRunner.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/WintryPatchRunner.kt @@ -3,16 +3,21 @@ package com.aliucord.manager.patcher import com.aliucord.manager.patcher.steps.download.* import com.aliucord.manager.patcher.steps.install.* import com.aliucord.manager.patcher.steps.patch.* +import com.aliucord.manager.patcher.steps.prepare.* import com.aliucord.manager.ui.screens.patchopts.PatchOptions import kotlinx.collections.immutable.persistentListOf class WintryPatchRunner(options: PatchOptions) : StepRunner() { override val steps = persistentListOf( + FetchDiscordRNAStep(options), + DowngradeCheckStep(options), + RestoreDownloadsStep(), + DownloadDiscordRNABaseStep(), DownloadDiscordRNALangStep(), DownloadDiscordRNAResourcesStep(), DownloadDiscordRNALibStep(), - DownloadWintryXposed(), + DownloadWintryXposedStep(), CopyDependenciesStep(), PatchIconsStep(options), diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadDiscordRNAStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadDiscordRNAStep.kt new file mode 100644 index 0000000..eb4487b --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadDiscordRNAStep.kt @@ -0,0 +1,15 @@ +package com.aliucord.manager.patcher.steps.base + +import com.aliucord.manager.patcher.StepRunner +import com.aliucord.manager.patcher.steps.prepare.FetchDiscordRNAStep +import kotlin.properties.Delegates + +abstract class DownloadDiscordRNAStep : DownloadStep() { + protected var version by Delegates.notNull() + private set + + override suspend fun execute(container: StepRunner) { + version = container.getStep().targetVersion + super.execute(container) + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt index 9ba25cd..c861d05 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt @@ -22,9 +22,6 @@ abstract class DownloadStep : Step(), KoinComponent { private val context: Context by inject() private val downloader: KtorDownloadManager by inject() - // Temporary - protected val version: Int = 289210 - /** * The remote url to download */ diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt index 070b5a8..a2ddb98 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt @@ -42,7 +42,7 @@ class CopyDependenciesStep : Step(), KoinComponent { override val localizedName = R.string.patch_step_copy_deps override suspend fun execute(container: StepRunner) { - val srcApk = container.getStep().targetFile + val srcApk = container.getStepOrNull()?.targetFile ?: container.getStepOrNull()!!.targetFile val langApk = container.getStepOrNull()?.targetFile val libApk = container.getStepOrNull()?.targetFile val resApk = container.getStepOrNull()?.targetFile diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNABaseStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNABaseStep.kt index 54ddf39..bc3b528 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNABaseStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNABaseStep.kt @@ -1,9 +1,16 @@ package com.aliucord.manager.patcher.steps.download import com.aliucord.manager.R +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.network.services.RNATrackerService.Companion.BASE_URL +import com.aliucord.manager.patcher.steps.base.DownloadDiscordRNAStep +import org.koin.core.component.inject -class DownloadDiscordRNABaseStep : DownloadDiscordStep() { - override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/base" - override val targetFile = paths.discordApkVersionCache(version).resolve("base.apk") +class DownloadDiscordRNABaseStep : DownloadDiscordRNAStep() { + private val paths: PathManager by inject() + + override val targetUrl get() = "$BASE_URL/tracker/download/$version/base" + override val targetFile get() = paths.discordApkVersionCache(version).resolve("base.apk") override val localizedName = R.string.downloading_base_apk + } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNALangStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNALangStep.kt index bc093b0..8c15f3c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNALangStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNALangStep.kt @@ -2,14 +2,15 @@ package com.aliucord.manager.patcher.steps.download import com.aliucord.manager.R import com.aliucord.manager.manager.PathManager -import com.aliucord.manager.patcher.steps.base.DownloadStep -import org.koin.core.component.KoinComponent +import com.aliucord.manager.network.services.RNATrackerService.Companion.BASE_URL +import com.aliucord.manager.patcher.steps.base.DownloadDiscordRNAStep import org.koin.core.component.inject -class DownloadDiscordRNALangStep : DownloadStep(), KoinComponent { +class DownloadDiscordRNALangStep : DownloadDiscordRNAStep() { private val paths: PathManager by inject() - override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/config.en" - override val targetFile = paths.discordApkVersionCache(version).resolve("config.en.apk") + override val targetUrl get() = "$BASE_URL/tracker/download/$version/config.en" + override val targetFile get() = paths.discordApkVersionCache(version).resolve("config.en.apk") + override val localizedName = R.string.download_libs_apk } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNALibStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNALibStep.kt index 480f8a3..7242f36 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNALibStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNALibStep.kt @@ -3,15 +3,15 @@ package com.aliucord.manager.patcher.steps.download import android.os.Build import com.aliucord.manager.R import com.aliucord.manager.manager.PathManager -import com.aliucord.manager.patcher.steps.base.DownloadStep -import org.koin.core.component.KoinComponent +import com.aliucord.manager.network.services.RNATrackerService.Companion.BASE_URL +import com.aliucord.manager.patcher.steps.base.DownloadDiscordRNAStep import org.koin.core.component.inject -class DownloadDiscordRNALibStep : DownloadStep(), KoinComponent { +class DownloadDiscordRNALibStep : DownloadDiscordRNAStep() { private val paths: PathManager by inject() private val arch = Build.SUPPORTED_ABIS.first().replace("-v", "_v") - override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/config.$arch" - override val targetFile = paths.discordApkVersionCache(version).resolve("config.$arch.apk") + override val targetUrl get() = "$BASE_URL/tracker/download/$version/config.$arch" + override val targetFile get() = paths.discordApkVersionCache(version).resolve("config.$arch.apk") override val localizedName = R.string.downloading_libs_apk } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNAResourcesStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNAResourcesStep.kt index cb29bcb..eb6572a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNAResourcesStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordRNAResourcesStep.kt @@ -2,14 +2,14 @@ package com.aliucord.manager.patcher.steps.download import com.aliucord.manager.R import com.aliucord.manager.manager.PathManager -import com.aliucord.manager.patcher.steps.base.DownloadStep -import org.koin.core.component.KoinComponent +import com.aliucord.manager.network.services.RNATrackerService.Companion.BASE_URL +import com.aliucord.manager.patcher.steps.base.DownloadDiscordRNAStep import org.koin.core.component.inject -class DownloadDiscordRNAResourcesStep : DownloadStep(), KoinComponent { +class DownloadDiscordRNAResourcesStep : DownloadDiscordRNAStep() { private val paths: PathManager by inject() - override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/config.xxhdpi" - override val targetFile = paths.discordApkVersionCache(version).resolve("config.xxhdpi.apk") + override val targetUrl get() = "$BASE_URL/tracker/download/$version/config.xxhdpi" + override val targetFile get() = paths.discordApkVersionCache(version).resolve("config.xxhdpi.apk") override val localizedName = R.string.downloading_resources_apk } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordStep.kt index c135558..7b4ffd2 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadDiscordStep.kt @@ -17,6 +17,7 @@ open class DownloadDiscordStep : DownloadStep(), KoinComponent { protected val paths: PathManager by inject() override val localizedName = R.string.patch_step_dl_kt_apk + override val targetUrl = getDiscordApkUrl(DISCORD_KT_VERSION) override val targetFile = paths.discordApkVersionCache(DISCORD_KT_VERSION) .resolve("discord.apk") diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadWintryXposed.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadWintryXposed.kt deleted file mode 100644 index 5f5b906..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadWintryXposed.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.aliucord.manager.patcher.steps.download - -import com.aliucord.manager.R -import com.aliucord.manager.manager.PathManager -import com.aliucord.manager.patcher.steps.base.DownloadStep -import org.koin.core.component.inject - -class DownloadWintryXposed : DownloadStep() { - private val paths: PathManager by inject() - - override val localizedName = R.string.downloading_wintryxposed - override val targetFile = paths.patchingDownloadDir.resolve("xposed.apk") - override val targetUrl = "https://github.com/wtcord/wt-xposed/releases/download/1002/app-release.apk" -} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadWintryXposedStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadWintryXposedStep.kt new file mode 100644 index 0000000..f35931f --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadWintryXposedStep.kt @@ -0,0 +1,31 @@ +package com.aliucord.manager.patcher.steps.download + +import com.aliucord.manager.R +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.network.services.WintryGithubService +import com.aliucord.manager.network.utils.SemVer +import com.aliucord.manager.network.utils.getOrThrow +import com.aliucord.manager.patcher.StepRunner +import com.aliucord.manager.patcher.steps.base.DownloadStep +import org.koin.core.component.inject + +class DownloadWintryXposedStep : DownloadStep() { + private val paths: PathManager by inject() + private val wintryGithubService: WintryGithubService by inject() + + override val localizedName = R.string.downloading_wintryxposed + override val targetFile get() = paths.cachedWintryXposed(targetVersion) + override lateinit var targetUrl: String + + lateinit var targetVersion: SemVer + private set + + override suspend fun execute(container: StepRunner) { + val latestRelease = wintryGithubService.getLatestXposedRelease().getOrThrow() + container.log("Latest WintryXposed release is ${latestRelease.name}") + + targetVersion = SemVer.parse(latestRelease.name) + targetUrl = latestRelease.assets.first { it.name == "app-release.apk" }.browserDownloadUrl + super.execute(container) + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/InjectWintryXposedStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/InjectWintryXposedStep.kt index 7d4d5ce..8b38e97 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/InjectWintryXposedStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/InjectWintryXposedStep.kt @@ -5,7 +5,7 @@ import com.aliucord.manager.patcher.StepRunner import com.aliucord.manager.patcher.steps.StepGroup import com.aliucord.manager.patcher.steps.base.Step import com.aliucord.manager.patcher.steps.download.CopyDependenciesStep -import com.aliucord.manager.patcher.steps.download.DownloadWintryXposed +import com.aliucord.manager.patcher.steps.download.DownloadWintryXposedStep import com.aliucord.manager.patcher.util.Signer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -58,7 +58,7 @@ class InjectWintryXposedStep : Step() { override suspend fun execute(container: StepRunner) { val apks = container.getStep().patchedApks - val xposed = container.getStep().targetFile + val xposed = container.getStep().targetFile container.log("Adding WintryXposed module with LSPatch") container.log("WintryXposed path = ${xposed.absolutePath}") diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt index 6abbc28..c7c1581 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt @@ -13,6 +13,7 @@ import com.github.diamondminer88.zip.ZipWriter import kotlinx.serialization.json.Json import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import org.lsposed.lspatch.share.LSPConfig /** * Store the install options and additional data inside the APK for future use, @@ -30,6 +31,7 @@ class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponen val aliuhook = container.getStepOrNull() val injector = container.getStepOrNull() val patches = container.getStepOrNull() + val wintryXposed = container.getStepOrNull() val metadata = InstallMetadata( options = options, @@ -38,11 +40,13 @@ class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponen aliuhookVersion = aliuhook?.targetVersion, injectorVersion = injector?.targetVersion, patchesVersion = patches?.targetVersion, + lspatchVersion = LSPConfig.instance.VERSION_CODE, + wintryXposedVersion = wintryXposed?.targetVersion, ) container.log("Writing serialized install metadata to APK") ZipWriter(apk, /* append = */ true).use { - it.writeEntry("aliucord.json", json.encodeToString(metadata)) + it.writeEntry("wintry.json", json.encodeToString(metadata)) } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/DowngradeCheckStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/DowngradeCheckStep.kt index 48a24e0..bc99a19 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/DowngradeCheckStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/DowngradeCheckStep.kt @@ -38,9 +38,15 @@ class DowngradeCheckStep(private val options: PatchOptions) : Step(), KoinCompon } container.log("Version of installed Discord app: $currentVersion") - val targetVersion = container - .getStep() - .data.discordVersionCode + var targetVersion: Int + + val fetchInfo = container.getStepOrNull() + if (fetchInfo != null) { + targetVersion = fetchInfo.data.discordVersionCode + } else { + val rnaFetchInfo = container.getStep() + targetVersion = rnaFetchInfo.targetVersion + } container.log("Target discord version: $targetVersion") diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchDiscordRNAStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchDiscordRNAStep.kt new file mode 100644 index 0000000..6ad533a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchDiscordRNAStep.kt @@ -0,0 +1,43 @@ +package com.aliucord.manager.patcher.steps.prepare + +import com.aliucord.manager.R +import com.aliucord.manager.network.services.RNATrackerService +import com.aliucord.manager.network.utils.getOrThrow +import com.aliucord.manager.patcher.StepRunner +import com.aliucord.manager.patcher.steps.StepGroup +import com.aliucord.manager.patcher.steps.base.Step +import com.aliucord.manager.ui.screens.patchopts.PatchOptions +import com.aliucord.manager.ui.screens.patchopts.VersionPreference +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import kotlin.properties.Delegates + +class FetchDiscordRNAStep(val options: PatchOptions) : Step(), KoinComponent { + private val rnaTrackerService: RNATrackerService by inject() + + override val group: StepGroup = StepGroup.Prepare + override val localizedName: Int = R.string.fetching_latest_discord_info + + var targetVersion by Delegates.notNull() + private set + + override suspend fun execute(container: StepRunner) { + if (options.versionPreference == VersionPreference.Custom) { + container.log("Custom version selected, skipping fetch.") + targetVersion = options.customVersionCode.toInt() + return + } + + val latestVal = rnaTrackerService.getLatestDiscordVersions().getOrThrow() + container.log("Fetched latest discord info: $latestVal") + + targetVersion = when (options.versionPreference) { + VersionPreference.Stable -> latestVal.latest.stable + VersionPreference.Beta -> latestVal.latest.beta + VersionPreference.Alpha -> latestVal.latest.alpha + else -> throw IllegalArgumentException("Unsupported version preference: ${options.versionPreference}") + } + + container.log("Selected Discord version: $targetVersion") + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/util/ManifestPatcher.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/util/ManifestPatcher.kt index f6c2b5b..5782809 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/util/ManifestPatcher.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/util/ManifestPatcher.kt @@ -35,7 +35,7 @@ object ManifestPatcher { vmSafeMode: Boolean? = true, useEmbeddedDex: Boolean? = true, extractNativeLibs: Boolean? = false, - addAliucordMetadata: Boolean? = true, + addManagerMetadata: Boolean? = true, modifyPermissions: Boolean? = true, modifyProviders: Boolean? = true, modifyActivities: Boolean? = true, @@ -128,7 +128,7 @@ object ManifestPatcher { private var shouldAddLegacyStorage = requestLegacyExternalStorage ?: false private var shouldAddUseEmbeddedDex = useEmbeddedDex ?: false private var shouldAddExtractNativeLibs = extractNativeLibs ?: false - private var shouldAddMetadata = addAliucordMetadata ?: false + private var shouldAddMetadata = addManagerMetadata ?: false override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { if (name == NETWORK_SECURITY_CONFIG) return @@ -142,10 +142,16 @@ object ManifestPatcher { override fun child(ns: String?, name: String): NodeVisitor { val visitor = super.child(ns, name) - if (shouldAddMetadata && addAliucordMetadata == true) { + if (shouldAddMetadata && addManagerMetadata == true) { shouldAddMetadata = false super.child(ANDROID_NAMESPACE, "meta-data").apply { - attr(ANDROID_NAMESPACE, "name", android.R.attr.name, TYPE_STRING, "isAliucord") + attr( + ANDROID_NAMESPACE, + "name", + android.R.attr.name, + TYPE_STRING, + "isWintry" /* originally "isAliucord" */ + ) attr(ANDROID_NAMESPACE, "value", android.R.attr.value, TYPE_INT_BOOLEAN, 1) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/previews/screens/PatchOptionsScreenPreview.kt b/app/src/main/kotlin/com/aliucord/manager/ui/previews/screens/PatchOptionsScreenPreview.kt index ae59b35..adf6c90 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/previews/screens/PatchOptionsScreenPreview.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/previews/screens/PatchOptionsScreenPreview.kt @@ -32,6 +32,11 @@ private fun PatchOptionsScreenPreview( packageName = parameters.packageName, packageNameState = parameters.packageNameState, setPackageName = {}, + versionPreference = parameters.versionPreference, + setVersionPreference = {}, + customVersionCode = parameters.customVersionCode, + customVersionCodeIsError = parameters.customVersionCodeIsError, + setCustomVersionCode = {}, isConfigValid = parameters.isConfigValid, onInstall = {}, ) @@ -50,6 +55,9 @@ private data class PatchOptionsParameters( val appNameIsError: Boolean, val packageName: String, val packageNameState: PackageNameState, + val versionPreference: VersionPreference, + val customVersionCode: String, + val customVersionCodeIsError: Boolean, val isConfigValid: Boolean, ) @@ -67,6 +75,9 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider(InstallsState.Fetching) @@ -45,6 +46,8 @@ class HomeModel( private val refreshingLock = Mutex() private var remoteDataJson: BuildInfo? = null + private var trackerIndexJson: RNATrackerIndex? = null + private var latestWintryXposedVersion: SemVer? = null private var latestAliuhookVersion: SemVer? = null init { @@ -106,7 +109,7 @@ class HomeModel( val metadata = try { val applicationInfo = application.packageManager.getApplicationInfo(packageName, 0) val metadataFile = ZipReader(applicationInfo.publicSourceDir) - .use { it.openEntry("aliucord.json")?.read() } + .use { it.openEntry("wintry.json")?.read() } @OptIn(ExperimentalSerializationApi::class) metadataFile?.let { json.decodeFromStream(it.inputStream()) } @@ -187,21 +190,35 @@ class HomeModel( private suspend fun fetchRemoteData() { listOf( + // // These aren't needed by Wintry + // screenModelScope.launch(Dispatchers.IO) { + // github.getBuildData().fold( + // success = { remoteDataJson = it }, + // fail = { Log.w(BuildConfig.TAG, "Failed to fetch remote build data", it) }, + // ) + // }, + // screenModelScope.launch(Dispatchers.IO) { + // maven.getAliuhookVersion().fold( + // success = { latestAliuhookVersion = it }, + // fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest Aliuhook version", it) }, + // ) + // }, screenModelScope.launch(Dispatchers.IO) { - github.getBuildData().fold( - success = { remoteDataJson = it }, - fail = { Log.w(BuildConfig.TAG, "Failed to fetch remote build data", it) }, + rnaTracker.getLatestDiscordVersions().fold( + success = { trackerIndexJson = it }, + fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest Discord RNA versions", it) }, ) }, screenModelScope.launch(Dispatchers.IO) { - maven.getAliuhookVersion().fold( - success = { latestAliuhookVersion = it }, - fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest Aliuhook version", it) }, + wtGithub.getLatestXposedRelease().fold( + success = { latestWintryXposedVersion = SemVer.parse(it.name) }, + fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest WintryXposed version", it) }, ) }, + ).joinAll() - if (remoteDataJson == null || latestAliuhookVersion == null) { + if (trackerIndexJson == null /* remoteDataJson == null || latestAliuhookVersion == null */ || latestWintryXposedVersion == null) { mainThread { application.showToast(R.string.home_network_fail) } } } @@ -213,34 +230,34 @@ class HomeModel( return application.packageManager .getInstalledPackages(PackageManager.GET_META_DATA) .filter { - // Packages installed via the legacy Installer do not have the metadata marker - val isAliucordPkg = it.packageName == "com.aliucord" - val hasAliucordMeta = it.applicationInfo?.metaData?.containsKey("isAliucord") == true - isAliucordPkg || hasAliucordMeta + // Wintry doesn't have "legacy installer" whatsoever + return@filter it.applicationInfo?.metaData?.containsKey("isWintry") == true + + // // Packages installed via the legacy Installer do not have the metadata marker + // val isAliucordPkg = it.packageName == "com.aliucord" + // val hasAliucordMeta = it.applicationInfo?.metaData?.containsKey("isAliucord") == true + // isAliucordPkg || hasAliucordMeta } } /** - * Attempts to determine whether the Aliucord installation is up-to-date. - * If `null` is returned, then the build data was not fetched, and as such - * the status cannot be determined. + * Checks whether the current Wintry installation is up-to-date. + * + * Currently mirrors the behavior of Bunny Manager by directly comparing against + * the latest available Discord and WintryXposed release. This is a temporary approach and will remain + * in place until Wintry adds support for version pinning. */ private fun isInstallationUpToDate(pkg: PackageInfo): Boolean? { - // Assume up-to-date when remote data hasn't been fetched yet - val remoteBuildData = remoteDataJson ?: return null - val latestAliuhookVersion = latestAliuhookVersion ?: return null + val trackerData = trackerIndexJson ?: return null // `longVersionCode` is unnecessary since Discord doesn't use `versionCodeMajor` @Suppress("DEPRECATION") val versionCode = pkg.versionCode - // Check if the base APK version is a mismatch - if (remoteBuildData.discordVersionCode != versionCode) return false - // Try to parse install metadata. If none present, install was made via legacy installer. val apkPath = pkg.applicationInfo?.publicSourceDir ?: return false val installMetadata = try { - val metadataFile = ZipReader(apkPath).use { it.openEntry("aliucord.json")?.read() } + val metadataFile = ZipReader(apkPath).use { it.openEntry("wintry.json")?.read() } ?: return false @OptIn(ExperimentalSerializationApi::class) @@ -250,11 +267,13 @@ class HomeModel( return false } - // Check that all the installation components are up-to-date (skip not installed) - val injectorUpToDate = installMetadata.injectorVersion == null || remoteBuildData.injectorVersion == installMetadata.injectorVersion - val patchesUpToDate = installMetadata.patchesVersion == null || remoteBuildData.patchesVersion == installMetadata.patchesVersion - val aliuhookUpToDate = installMetadata.aliuhookVersion == null || latestAliuhookVersion == installMetadata.aliuhookVersion + val latestByPref = when (installMetadata.options.versionPreference) { + VersionPreference.Stable -> trackerData.latest.stable + VersionPreference.Beta -> trackerData.latest.beta + VersionPreference.Alpha -> trackerData.latest.alpha + VersionPreference.Custom -> return true + } - return injectorUpToDate && patchesUpToDate && aliuhookUpToDate + return latestByPref == versionCode && installMetadata.wintryXposedVersion == latestWintryXposedVersion } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt index 8b386ec..ecf81ca 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt @@ -88,25 +88,21 @@ fun InstalledItemCard( horizontalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.clip(MaterialTheme.shapes.large), ) { - SegmentedButton( - icon = painterResource(R.drawable.ic_extension), - text = stringResource(R.string.plugins_title), - onClick = onOpenPlugins, - ) - SegmentedButton( - icon = painterResource(R.drawable.ic_info), - text = stringResource(R.string.action_open_info), - onClick = onOpenInfo, - ) + // // Plugins cannot be managed externally yet in Wintry + // SegmentedButton( + // icon = painterResource(R.drawable.ic_extension), + // text = stringResource(R.string.plugins_title), + // onClick = onOpenPlugins, + // ) // If the up-to-date status cannot be determined, assume it is up-to-date - if (data.isUpToDate ?: true) { + if (data.isUpToDate != false) SegmentedButton( - icon = painterResource(R.drawable.ic_launch), - text = stringResource(R.string.action_launch), - onClick = onOpenApp, + icon = painterResource(R.drawable.ic_refresh), + text = stringResource(R.string.reinstall), + onClick = onUpdate, ) - } else { + else { val warningColor = Color(0xFFFFBB33) SegmentedButton( @@ -117,6 +113,16 @@ fun InstalledItemCard( onClick = onUpdate, ) } + SegmentedButton( + icon = painterResource(R.drawable.ic_info), + text = stringResource(R.string.action_open_info), + onClick = onOpenInfo, + ) + SegmentedButton( + icon = painterResource(R.drawable.ic_launch), + text = stringResource(R.string.action_launch), + onClick = onOpenApp, + ) } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptions.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptions.kt index 549a189..f79cc4e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptions.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptions.kt @@ -10,6 +10,13 @@ import kotlinx.parcelize.TypeParceler import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +enum class VersionPreference { + Stable, + Beta, + Alpha, + Custom; +} + @Immutable @Parcelize @Serializable @@ -33,6 +40,16 @@ data class PatchOptions( * Replacement of the user-facing launcher icon. */ val iconReplacement: IconReplacement, + + /** + * Version preference to fetch and install + */ + val versionPreference: VersionPreference, + + /** + * Version to fetch and install when versionPreference is set to VersionPreference.Custom. + */ + val customVersionCode: String, ) : Parcelable { @Immutable @Parcelize @@ -109,6 +126,8 @@ data class PatchOptions( packageName = "com.aliucord", debuggable = false, iconReplacement = IconReplacement.CustomColor(IconReplacement.AliucordColor), + versionPreference = VersionPreference.Stable, + customVersionCode = "126021", ) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptionsModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptionsModel.kt index 38bf51d..420f168 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptionsModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptionsModel.kt @@ -9,6 +9,7 @@ import androidx.core.content.getSystemService import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import com.aliucord.manager.manager.PreferencesManager +import com.aliucord.manager.ui.util.DiscordVersion import com.aliucord.manager.util.* class PatchOptionsModel( @@ -48,6 +49,26 @@ class PatchOptionsModel( debuggable = value } + // ---------- Version preference state ---------- + var versionPreference by mutableStateOf(prefilledOptions.versionPreference) + private set + + fun changeVersionPreference(value: VersionPreference) { + versionPreference = value + } + + // ---------- Custom version code state ---------- + var customVersionCode by mutableStateOf(prefilledOptions.customVersionCode) + private set + + var customVersionCodeIsError by mutableStateOf(false) + private set + + fun changeCustomVersionCode(value: String) { + customVersionCode = value + customVersionCodeIsError = !DiscordVersion.isValid(value) + } + // ---------- Other ---------- var showNetworkWarningDialog by mutableStateOf(!alreadyShownNetworkWarning && isNetworkDangerous()) private set @@ -76,6 +97,8 @@ class PatchOptionsModel( packageName = packageName, debuggable = debuggable, iconReplacement = icon, + versionPreference = versionPreference, + customVersionCode = customVersionCode, ) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptionsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptionsScreen.kt index 98e535e..c34c786 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptionsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/PatchOptionsScreen.kt @@ -1,6 +1,7 @@ package com.aliucord.manager.ui.screens.patchopts import android.os.Parcelable +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -30,8 +31,7 @@ import com.aliucord.manager.ui.screens.patching.PatchingScreen import com.aliucord.manager.ui.screens.patchopts.components.PackageNameStateLabel import com.aliucord.manager.ui.screens.patchopts.components.PatchOptionsAppBar import com.aliucord.manager.ui.screens.patchopts.components.options.* -import com.aliucord.manager.ui.util.InstallNotifications -import com.aliucord.manager.ui.util.spacedByLastAtBottom +import com.aliucord.manager.ui.util.* import com.aliucord.manager.util.* import kotlinx.coroutines.delay import kotlinx.parcelize.IgnoredOnParcel @@ -101,6 +101,13 @@ class PatchOptionsScreen( packageNameState = model.packageNameState, setPackageName = model::changePackageName, + versionPreference = model.versionPreference, + setVersionPreference = model::changeVersionPreference, + + customVersionCode = model.customVersionCode, + customVersionCodeIsError = model.customVersionCodeIsError, + setCustomVersionCode = model::changeCustomVersionCode, + isConfigValid = model.isConfigValid, onInstall = onInstall@{ val iconConfig = iconModel.generateConfig() @@ -137,6 +144,13 @@ fun PatchOptionsScreenContent( packageNameState: PackageNameState, setPackageName: (String) -> Unit, + versionPreference: VersionPreference, + setVersionPreference: (VersionPreference) -> Unit, + + customVersionCode: String, + customVersionCodeIsError: Boolean, + setCustomVersionCode: (String) -> Unit, + isConfigValid: Boolean, onInstall: () -> Unit, ) { @@ -236,6 +250,40 @@ fun PatchOptionsScreenContent( ) } + SelectPatchOption( + icon = painterResource(R.drawable.ic_app_shortcut), + name = "Preferred version", + description = "Choose which release channel or input a custom version code to use for installation.", + options = VersionPreference.entries.map { it.name }, + selectedOption = versionPreference.name, + onOptionSelected = { selected -> + setVersionPreference(VersionPreference.valueOf(selected)) + } + ) + + AnimatedVisibility( + visible = versionPreference == VersionPreference.Custom, + label = "Custom version code option visibility" + ) { + TextPatchOption( + name = "Custom version code", + description = "Input a custom version code to use for installation.", + value = customVersionCode, + valueIsError = customVersionCodeIsError, + valueIsDefault = customVersionCode == PatchOptions.Default.customVersionCode, + onValueChange = setCustomVersionCode, + onValueReset = { + setCustomVersionCode(PatchOptions.Default.customVersionCode) + } + ) { + VersionDisplay( + version = DiscordVersion.parseFromString(customVersionCode), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 4.dp), + ) + } + } + if (isDevMode) { TextDivider( text = stringResource(R.string.patchopts_divider_advanced), diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/components/options/SelectPatchOption.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/components/options/SelectPatchOption.kt new file mode 100644 index 0000000..b2c7821 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patchopts/components/options/SelectPatchOption.kt @@ -0,0 +1,64 @@ +package com.aliucord.manager.ui.screens.patchopts.components.options + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.style.TextOverflow + +@Composable +fun SelectPatchOption( + icon: Painter, + name: String, + description: String, + options: List, + selectedOption: String, + onOptionSelected: (String) -> Unit, + enabled: Boolean = true, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + val interactionSource = remember(::MutableInteractionSource) + + IconPatchOption( + icon = icon, + name = name, + description = description, + modifier = modifier.fillMaxWidth() + ) { + FilledTonalButton( + onClick = { if (enabled) expanded = true }, + enabled = enabled, + interactionSource = interactionSource, + ) { + Text( + text = selectedOption, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + options.forEach { option -> + DropdownMenuItem( + text = { + Text( + text = option, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + onClick = { + expanded = false + onOptionSelected(option) + }, + enabled = enabled, + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/util/DiscordVersion.kt b/app/src/main/kotlin/com/aliucord/manager/ui/util/DiscordVersion.kt index 17559e8..7ce4df9 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/util/DiscordVersion.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/util/DiscordVersion.kt @@ -10,6 +10,9 @@ import kotlinx.parcelize.Parcelize @Parcelize @Immutable sealed interface DiscordVersion : Comparable, Parcelable { + @Parcelize + data object Invalid : DiscordVersion + @Parcelize data object Error : DiscordVersion @@ -31,6 +34,7 @@ sealed interface DiscordVersion : Comparable, Parcelable { return when (this) { is Error -> 0 is None -> 0 + is Invalid -> 0 is Existing -> { if (other is Existing) { other.typelessCode.compareTo(typelessCode) @@ -44,6 +48,7 @@ sealed interface DiscordVersion : Comparable, Parcelable { @Composable fun toDisplayName() = when (this) { is Error -> stringResource(R.string.version_load_fail) + is Invalid -> stringResource(R.string.invalid_version_code_format) is None -> stringResource(R.string.version_none) is Existing -> when (type) { Type.STABLE -> stringResource(R.string.version_stable) @@ -61,6 +66,23 @@ sealed interface DiscordVersion : Comparable, Parcelable { } companion object { + val DISCORD_VERSION_REGEX = Regex("""^\d{3}[0-2]\d{2}$""") + + fun parseFromString(version: String): DiscordVersion { + if (!DISCORD_VERSION_REGEX.matches(version)) { + return Invalid + } + + val versionCode = version.toInt() + val type = parseVersionType(versionCode) + + return Existing( + type = type, + name = "${versionCode / 1000}.${versionCode % 100}", + code = versionCode, + ) + } + fun parseVersionType(versionCode: Int?): Type { return when (versionCode?.div(100)?.mod(10)) { 0 -> Type.STABLE @@ -69,5 +91,9 @@ sealed interface DiscordVersion : Comparable, Parcelable { else -> Type.UNKNOWN } } + + fun isValid(versionCode: String): Boolean { + return DISCORD_VERSION_REGEX.matches(versionCode) + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c212da3..35009bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -252,4 +252,8 @@ Downloading resources apk Injecting WintryXposed with LSPatch Downloading WintryXposed + Version Code + Fetching latest Discord info + Invalid version code format + Reinstall