mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-22 00:42:38 +00:00
make updater abstract
This commit is contained in:
8 files changed
+188
-66
No files matched your search
@@ -185,16 +185,18 @@ fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Bool
|
||||
}
|
||||
|
||||
val outOf = remember(state.outOf) {
|
||||
Formatter.formatShortFileSize(context, state.outOf)
|
||||
state.outOf?.apply {
|
||||
Formatter.formatShortFileSize(context, this)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_downloading_update),
|
||||
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf),
|
||||
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf ?: "…"),
|
||||
progress = {
|
||||
val progress = state.downloaded / state.outOf.toDouble()
|
||||
val progress = state.downloaded / state.outOf!!.toDouble()
|
||||
progress.toFloat()
|
||||
}
|
||||
}.takeIf { state.outOf != null }
|
||||
)
|
||||
}
|
||||
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
||||
|
||||
@@ -70,7 +70,7 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
sealed class LaunchUIState {
|
||||
data object Initial : LaunchUIState()
|
||||
data object UpdateCheck : LaunchUIState()
|
||||
data class Updating(val downloaded: Long, val outOf: Long) : LaunchUIState()
|
||||
data class Updating(val downloaded: Long, val outOf: Long?) : LaunchUIState()
|
||||
data class Cancelled(val reason: LaunchCancelReason, val inProgress: Boolean = false) : LaunchUIState()
|
||||
data object Working : LaunchUIState()
|
||||
data object Ready : LaunchUIState()
|
||||
|
||||
@@ -73,10 +73,10 @@ fun generateInstallIntent(uri: Uri): Intent {
|
||||
|
||||
fun installLauncherUpdate(context: Context) {
|
||||
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
|
||||
val launcherDownload = nextUpdate?.getLauncherDownload()
|
||||
val launcherDownload = nextUpdate?.getDownload()
|
||||
|
||||
if (launcherDownload != null) {
|
||||
val outputFile = launcherDownload.name
|
||||
val outputFile = launcherDownload.filename
|
||||
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||
|
||||
val outputPathFile = File(baseDirectory, outputFile)
|
||||
@@ -237,21 +237,23 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
|
||||
}
|
||||
|
||||
val outOf = remember(state.outOf) {
|
||||
Formatter.formatShortFileSize(context, state.outOf)
|
||||
state.outOf?.run {
|
||||
Formatter.formatShortFileSize(context, this)
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProgressIndicator(
|
||||
stringResource(
|
||||
R.string.release_fetch_downloading,
|
||||
downloaded,
|
||||
outOf
|
||||
outOf ?: "…"
|
||||
),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel,
|
||||
progress = {
|
||||
val progress = state.downloaded / state.outOf.toDouble()
|
||||
val progress = state.downloaded / state.outOf!!.toDouble()
|
||||
progress.toFloat()
|
||||
}
|
||||
}.takeIf { state.outOf != null }
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
|
||||
|
||||
@@ -151,11 +151,16 @@ fun UpdateIndicator(
|
||||
is ReleaseViewModel.ReleaseUIState.InDownload -> {
|
||||
// is this the ideal design? idk
|
||||
enablePopup = true
|
||||
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = { progress.toFloat() },
|
||||
)
|
||||
if (updateStatus.outOf != null) {
|
||||
CircularProgressIndicator(
|
||||
progress = {
|
||||
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
|
||||
progress.toFloat()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ data class Asset(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Release(
|
||||
data class Release(
|
||||
val url: String,
|
||||
val id: Int,
|
||||
val targetCommitish: String,
|
||||
@@ -25,39 +25,83 @@ class Release(
|
||||
val publishedAt: Instant,
|
||||
val assets: List<Asset>,
|
||||
val htmlUrl: String
|
||||
) {
|
||||
fun getDescription(): String {
|
||||
if (tagName == "nightly") {
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoaderVersion(
|
||||
val tag: String,
|
||||
val version: String,
|
||||
val createdAt: Instant,
|
||||
val commitHash: String,
|
||||
val prerelease: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoaderPayload<T>(
|
||||
val payload: T?,
|
||||
val error: String
|
||||
)
|
||||
|
||||
data class DownloadableAsset(val url: String, val filename: String, val size: Long? = null)
|
||||
|
||||
abstract class Downloadable {
|
||||
abstract fun getDescription(): String
|
||||
abstract fun getDescriptor(): Long
|
||||
abstract fun getDownload(): DownloadableAsset?
|
||||
}
|
||||
|
||||
class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadable() {
|
||||
override fun getDescription(): String {
|
||||
if (release.tagName == "nightly") {
|
||||
// get the commit from the assets
|
||||
// otherwise, a separate request is needed to get the hash (ew)
|
||||
val asset = assets.first()
|
||||
val asset = release.assets.first()
|
||||
val commit = asset.name.substring(6..12)
|
||||
|
||||
return "nightly-$commit"
|
||||
}
|
||||
|
||||
return tagName
|
||||
return release.tagName
|
||||
}
|
||||
|
||||
fun getDescriptor(): Long {
|
||||
return createdAt.epochSeconds
|
||||
override fun getDescriptor(): Long {
|
||||
return release.createdAt.epochSeconds
|
||||
}
|
||||
|
||||
fun getGeodeDownload(): Asset? {
|
||||
private fun getGitHubDownload(): Asset? {
|
||||
// try to find an asset that matches the architecture first
|
||||
val platform = LaunchUtils.platformName
|
||||
|
||||
val releaseSuffix = "$platform.zip"
|
||||
return assets.find {
|
||||
return release.assets.find {
|
||||
it.name.endsWith(releaseSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLauncherDownload(): Asset? {
|
||||
override fun getDownload(): DownloadableAsset? {
|
||||
val download = getGitHubDownload() ?: return null
|
||||
return DownloadableAsset(
|
||||
url = download.browserDownloadUrl,
|
||||
filename = download.name,
|
||||
size = download.size.toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadableLauncherRelease(val release: Release) : Downloadable() {
|
||||
override fun getDescription(): String {
|
||||
return release.tagName
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Long {
|
||||
return release.createdAt.epochSeconds
|
||||
}
|
||||
|
||||
private fun getGitHubDownload(): Asset? {
|
||||
val platform = LaunchUtils.platformName
|
||||
val use32BitPlatform = platform == "android32"
|
||||
|
||||
return assets.find { asset ->
|
||||
return release.assets.find { asset ->
|
||||
/* you know it's good when you pull out the truth table
|
||||
* u32bp | contains | found
|
||||
* 1 | 1 | 1
|
||||
@@ -70,4 +114,31 @@ class Release(
|
||||
(asset.name.contains("android32")) == use32BitPlatform
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDownload(): DownloadableAsset? {
|
||||
val download = getGitHubDownload() ?: return null
|
||||
return DownloadableAsset(
|
||||
url = download.browserDownloadUrl,
|
||||
filename = download.name,
|
||||
size = download.size.toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadable() {
|
||||
override fun getDescription(): String {
|
||||
return version.tag
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Long {
|
||||
return version.createdAt.epochSeconds
|
||||
}
|
||||
|
||||
override fun getDownload(): DownloadableAsset? {
|
||||
val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
|
||||
return DownloadableAsset(
|
||||
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename",
|
||||
filename = filename
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class ReleaseManager private constructor(
|
||||
sealed class ReleaseManagerState {
|
||||
data object InUpdateCheck : ReleaseManagerState()
|
||||
data class Failure(val exception: Exception) : ReleaseManagerState()
|
||||
data class InDownload(val downloaded: Long, val outOf: Long) : ReleaseManagerState()
|
||||
data class InDownload(val downloaded: Long, val outOf: Long?) : ReleaseManagerState()
|
||||
data class Finished(val hasUpdated: Boolean = false) : ReleaseManagerState()
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class ReleaseManager private constructor(
|
||||
val isInUpdate: Boolean
|
||||
get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished
|
||||
|
||||
private val _availableLauncherUpdate = MutableStateFlow<Release?>(null)
|
||||
private val _availableLauncherUpdate = MutableStateFlow<DownloadableLauncherRelease?>(null)
|
||||
val availableLauncherUpdate = _availableLauncherUpdate.asStateFlow()
|
||||
|
||||
private fun sendError(e: Exception) {
|
||||
@@ -106,24 +106,21 @@ class ReleaseManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBestReleaseForGameVersion(): String? {
|
||||
private fun getBestReleaseForGameVersion(gameVersion: Long): String? = when {
|
||||
gameVersion >= 40L -> mapSelectedReleaseToTag()
|
||||
gameVersion == 39L -> "v3.9.2"
|
||||
gameVersion == 38L -> "v2.0.0-beta.27"
|
||||
gameVersion == 37L -> "v2.0.0-beta.4"
|
||||
else -> null
|
||||
}
|
||||
|
||||
private suspend fun getLatestRelease(): Downloadable? {
|
||||
if (!GamePackageUtils.isGameInstalled(applicationContext.packageManager)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val gameVersion = GamePackageUtils.getGameVersionCode(applicationContext.packageManager)
|
||||
|
||||
return when {
|
||||
gameVersion >= 40L -> mapSelectedReleaseToTag()
|
||||
gameVersion == 39L -> "v3.9.2"
|
||||
gameVersion == 38L -> "v2.0.0-beta.27"
|
||||
gameVersion == 37L -> "v2.0.0-beta.4"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getLatestRelease(): Release? {
|
||||
val targetTag = getBestReleaseForGameVersion() ?: return null
|
||||
val targetTag = getBestReleaseForGameVersion(gameVersion) ?: return null
|
||||
|
||||
return when (targetTag) {
|
||||
TAG_LATEST -> releaseRepository.getLatestGeodeRelease()
|
||||
@@ -132,23 +129,23 @@ class ReleaseManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadLauncherUpdate(release: Release) {
|
||||
val download = release.getLauncherDownload() ?: return
|
||||
private suspend fun downloadLauncherUpdate(release: Downloadable) {
|
||||
val download = release.getDownload() ?: return
|
||||
|
||||
val outputDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||
val outputFile = File(outputDirectory, download.name)
|
||||
val outputFile = File(outputDirectory, download.filename)
|
||||
|
||||
if (outputFile.exists()) {
|
||||
// only download the apk once
|
||||
return
|
||||
}
|
||||
|
||||
_uiState.value = ReleaseManagerState.InDownload(0, download.size.toLong())
|
||||
_uiState.value = ReleaseManagerState.InDownload(0, download.size)
|
||||
|
||||
try {
|
||||
val fileStream = DownloadUtils.downloadStream(
|
||||
httpClient,
|
||||
download.browserDownloadUrl
|
||||
download.url
|
||||
) { progress, outOf ->
|
||||
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
|
||||
}
|
||||
@@ -160,8 +157,8 @@ class ReleaseManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performUpdate(release: Release) {
|
||||
val releaseAsset = release.getGeodeDownload()
|
||||
private suspend fun performUpdate(release: Downloadable) {
|
||||
val releaseAsset = release.getDownload()
|
||||
if (releaseAsset == null) {
|
||||
val noAssetException = Exception("missing Android download")
|
||||
_uiState.value = ReleaseManagerState.Failure(noAssetException)
|
||||
@@ -170,12 +167,12 @@ class ReleaseManager private constructor(
|
||||
}
|
||||
|
||||
// set an initial download size
|
||||
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size.toLong())
|
||||
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size)
|
||||
|
||||
try {
|
||||
val fileStream = DownloadUtils.downloadStream(
|
||||
httpClient,
|
||||
releaseAsset.browserDownloadUrl
|
||||
releaseAsset.url
|
||||
) { progress, outOf ->
|
||||
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
|
||||
}
|
||||
@@ -223,8 +220,8 @@ class ReleaseManager private constructor(
|
||||
return originalFileHash != currentFileHash
|
||||
}
|
||||
|
||||
private fun checkLauncherUpdate(launcherUpdate: Release) {
|
||||
if (launcherUpdate.tagName != BuildConfig.VERSION_NAME) {
|
||||
private fun checkLauncherUpdate(launcherUpdate: DownloadableLauncherRelease) {
|
||||
if (launcherUpdate.release.tagName != BuildConfig.VERSION_NAME) {
|
||||
_availableLauncherUpdate.value = launcherUpdate
|
||||
}
|
||||
}
|
||||
@@ -286,7 +283,7 @@ class ReleaseManager private constructor(
|
||||
performUpdate(release)
|
||||
}
|
||||
|
||||
private fun updatePreferences(release: Release) {
|
||||
private fun updatePreferences(release: Downloadable) {
|
||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||
|
||||
sharedPreferences.setString(
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.IOException
|
||||
@@ -22,32 +23,34 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
|
||||
private const val GITHUB_RATELIMIT_REMAINING = "x-ratelimit-remaining"
|
||||
private const val GITHUB_RATELIMIT_RESET = "x-ratelimit-reset"
|
||||
|
||||
private const val GEODE_API_BASE = "https://api.geode-sdk.org/v1"
|
||||
}
|
||||
|
||||
suspend fun getLatestLauncherRelease(): Release? {
|
||||
suspend fun getLatestLauncherRelease(): DownloadableLauncherRelease? {
|
||||
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
|
||||
|
||||
val url = URL(releasePath)
|
||||
|
||||
return getReleaseByUrl(url)
|
||||
return getReleaseByUrl(url)?.let(::DownloadableLauncherRelease)
|
||||
}
|
||||
|
||||
suspend fun getLatestGeodeRelease(): Release? {
|
||||
suspend fun getLatestGeodeRelease(): DownloadableGitHubLoaderRelease? {
|
||||
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases/latest"
|
||||
val url = URL(releasePath)
|
||||
|
||||
return getReleaseByUrl(url)
|
||||
return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
|
||||
}
|
||||
|
||||
suspend fun getReleaseByTag(tag: String): Release? {
|
||||
suspend fun getReleaseByTag(tag: String): DownloadableGitHubLoaderRelease? {
|
||||
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases/tags/$tag"
|
||||
val url = URL(releasePath)
|
||||
|
||||
return getReleaseByUrl(url)
|
||||
return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun getLatestGeodePreRelease(): Release? {
|
||||
suspend fun getLatestGeodePreRelease(): DownloadableGitHubLoaderRelease? {
|
||||
val releasesUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases?per_page=2"
|
||||
val url = URL(releasesUrl)
|
||||
|
||||
@@ -71,7 +74,8 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
response.body!!.source()
|
||||
)
|
||||
|
||||
release.find { it.tagName != "nightly" }
|
||||
(release.find { it.tagName != "nightly" })
|
||||
?.let(::DownloadableGitHubLoaderRelease)
|
||||
}
|
||||
403 -> {
|
||||
// determine if the error code is a ratelimit
|
||||
@@ -137,8 +141,8 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
// determine if the error code is a ratelimit
|
||||
// (github docs say it sends 429 too, but haven't seen that)
|
||||
|
||||
val limitRemaining = response.headers.get(GITHUB_RATELIMIT_REMAINING)?.toInt()
|
||||
val limitReset = response.headers.get(GITHUB_RATELIMIT_RESET)?.toLong()
|
||||
val limitRemaining = response.headers[GITHUB_RATELIMIT_REMAINING]?.toInt()
|
||||
val limitReset = response.headers[GITHUB_RATELIMIT_RESET]?.toLong()
|
||||
|
||||
if (limitRemaining == 0 && limitReset != null) {
|
||||
// handle ratelimit with a custom error
|
||||
@@ -159,4 +163,45 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun getLatestIndexRelease(gd: String, prerelease: Boolean = false): DownloadableLoaderRelease? {
|
||||
val url = "$GEODE_API_BASE/loader/versions/latest".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("gd", gd)
|
||||
.addQueryParameter("platform", "android")
|
||||
.addQueryParameter("prerelease", prerelease.toString())
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val call = httpClient.newCall(request)
|
||||
val response = call.executeCoroutine()
|
||||
|
||||
return when (response.code) {
|
||||
200 -> {
|
||||
val format = Json {
|
||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
val body = format.decodeFromBufferedSource<LoaderPayload<LoaderVersion>>(
|
||||
response.body!!.source()
|
||||
)
|
||||
|
||||
val release = body.payload ?: throw IOException("index error: ${body.error}")
|
||||
|
||||
DownloadableLoaderRelease(release)
|
||||
}
|
||||
404 -> {
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
throw IOException("unknown response ${response.code}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
|
||||
sealed class ReleaseUIState {
|
||||
data object InUpdateCheck : ReleaseUIState()
|
||||
data class Failure(val exception: Exception) : ReleaseUIState()
|
||||
data class InDownload(val downloaded: Long, val outOf: Long) : ReleaseUIState()
|
||||
data class InDownload(val downloaded: Long, val outOf: Long?) : ReleaseUIState()
|
||||
data class Finished(val hasUpdated: Boolean = false) : ReleaseUIState()
|
||||
data class Cancelled(val isCancelling: Boolean = false) : ReleaseUIState()
|
||||
|
||||
|
||||
Reference in new issue
Block a user