feat: auto updater (#35)
This commit is contained in:
parent
8452fc5d28
commit
2c4be41821
11 changed files with 238 additions and 7 deletions
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.ui.dialog.StoragePermissionsDialog
|
||||
import com.aliucord.manager.ui.dialog.UpdaterDialog
|
||||
import com.aliucord.manager.ui.navigation.AppDestination
|
||||
import com.aliucord.manager.ui.navigation.HomeDestination
|
||||
import com.aliucord.manager.ui.screen.*
|
||||
|
|
@ -54,6 +55,15 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
StoragePermissionsDialog()
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (
|
||||
BuildConfig.GIT_BRANCH == "release" &&
|
||||
!BuildConfig.GIT_LOCAL_CHANGES &&
|
||||
!BuildConfig.GIT_LOCAL_COMMITS
|
||||
) {
|
||||
UpdaterDialog()
|
||||
}
|
||||
|
||||
Taxi(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
navigator = navigator,
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ val viewModelModule = module {
|
|||
viewModelOf(::AboutViewModel)
|
||||
viewModelOf(::InstallViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::UpdaterViewModel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@ package com.aliucord.manager.domain.manager
|
|||
import android.app.Application
|
||||
import android.app.DownloadManager
|
||||
import android.content.*
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.domain.repository.AliucordMavenRepository
|
||||
import com.aliucord.manager.network.service.GithubService
|
||||
import java.io.File
|
||||
|
|
|
|||
|
|
@ -15,6 +15,6 @@ class GithubRepository(
|
|||
suspend fun getDiscordKtVersion() = service.getVersion()
|
||||
|
||||
suspend fun getHermesReleases() = service.getHermesReleases()
|
||||
|
||||
suspend fun getAliucordNativeReleases() = service.getAliucordNativeReleases()
|
||||
suspend fun getManagerReleases() = service.getManagerReleases()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import android.os.Build
|
|||
import com.aliucord.manager.installer.service.InstallService
|
||||
import java.io.File
|
||||
|
||||
fun Application.installApks(vararg apks: File) =
|
||||
packageManager.packageInstaller.installApks(this, *apks)
|
||||
|
||||
fun PackageInstaller.installApks(application: Application, vararg apks: File) {
|
||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
val sessionId = createSession(params)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package com.aliucord.manager.network.service
|
||||
|
||||
import com.aliucord.manager.network.dto.Commit
|
||||
import com.aliucord.manager.network.dto.GithubRelease
|
||||
import com.aliucord.manager.network.dto.GithubUser
|
||||
import com.aliucord.manager.network.dto.Version
|
||||
import com.aliucord.manager.network.dto.*
|
||||
import com.aliucord.manager.network.utils.ApiResponse
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -43,11 +40,18 @@ class GithubService(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getManagerReleases(): ApiResponse<List<GithubRelease>> = withContext(Dispatchers.IO) {
|
||||
http.request {
|
||||
url(MANAGER_RELEASES_URL)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ORG = "Aliucord"
|
||||
private const val REPO = "Aliucord"
|
||||
private const val HERMES_REPO = "Hermes"
|
||||
private const val NATIVE_REPO = "AliucordNative"
|
||||
private const val MANAGER_REPO = "AliucordManager"
|
||||
|
||||
private const val COMMITS_URL = "https://api.github.com/repos/$ORG/$REPO/commits"
|
||||
private const val CONTRIBUTORS_URL = "https://api.github.com/repos/$ORG/$REPO/contributors"
|
||||
|
|
@ -58,5 +62,7 @@ class GithubService(
|
|||
|
||||
private const val HERMES_RELEASES_URL = "https://api.github.com/repos/$ORG/$HERMES_REPO/releases"
|
||||
private const val ALIUCORD_NATIVE_RELEASES_URL = "https://api.github.com/repos/$ORG/$NATIVE_REPO/releases"
|
||||
|
||||
const val MANAGER_RELEASES_URL = "https://api.github.com/repos/$ORG/$MANAGER_REPO/releases"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,13 @@ inline fun <T> ApiResponse<T>.getOrThrow(): T {
|
|||
)
|
||||
}
|
||||
|
||||
inline fun <T> ApiResponse<T>.getOrNull(): T? {
|
||||
return fold(
|
||||
success = { it },
|
||||
fail = { null }
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <T, R> ApiResponse<T>.chain(block: (T) -> ApiResponse<R>): ApiResponse<R> {
|
||||
return if (this !is ApiResponse.Success) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package com.aliucord.manager.network.utils
|
||||
|
||||
/**
|
||||
* Parses a Semantic version in the format of `v1.0.0` (v[major].[minor].[patch])
|
||||
*/
|
||||
data class SemVer(
|
||||
val major: Int,
|
||||
val minor: Int,
|
||||
val patch: Int,
|
||||
private val vPrefix: Boolean = false
|
||||
) : Comparable<SemVer> {
|
||||
override fun compareTo(other: SemVer): Int {
|
||||
val pairs = arrayOf(
|
||||
major to other.major,
|
||||
minor to other.minor,
|
||||
patch to other.patch
|
||||
)
|
||||
|
||||
return pairs
|
||||
.map { (first, second) -> first.compareTo(second) }
|
||||
.find { it != 0 }
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
val ver = other as? SemVer
|
||||
?: return false
|
||||
|
||||
return ver.major == major &&
|
||||
ver.minor == minor &&
|
||||
ver.patch == patch
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "${if (vPrefix) "v" else ""}$major.$minor.$patch"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = major
|
||||
result = 31 * result + minor
|
||||
result = 31 * result + patch
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(version: String, vPrefix: Boolean = false): SemVer? {
|
||||
if (vPrefix && version.getOrNull(0) != 'v')
|
||||
return null
|
||||
|
||||
val str = if (vPrefix) version.substring(1) else version
|
||||
val parts = str
|
||||
.split(".")
|
||||
.mapNotNull { it.toIntOrNull() }
|
||||
.takeIf { it.size == 3 }
|
||||
?: return null
|
||||
|
||||
return SemVer(parts[0], parts[1], parts[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.aliucord.manager.ui.dialog
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.ui.viewmodel.UpdaterViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@Composable
|
||||
fun UpdaterDialog(
|
||||
viewModel: UpdaterViewModel = getViewModel()
|
||||
) {
|
||||
if (!viewModel.showDialog) return
|
||||
|
||||
AlertDialog(
|
||||
confirmButton = {
|
||||
Button(onClick = viewModel::triggerUpdate) {
|
||||
if (!viewModel.isWorking) {
|
||||
Text(stringResource(R.string.action_update))
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.onSecondary,
|
||||
strokeWidth = 2.5.dp,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
onClick = viewModel::dismissDialog,
|
||||
enabled = !viewModel.isWorking
|
||||
) {
|
||||
Text(stringResource(R.string.action_dismiss))
|
||||
}
|
||||
},
|
||||
onDismissRequest = {},
|
||||
title = {
|
||||
Text(stringResource(R.string.updater_title, viewModel.targetVersion ?: ""))
|
||||
},
|
||||
text = { Text(stringResource(R.string.updater_body)) },
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = false,
|
||||
dismissOnClickOutside = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.aliucord.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.domain.manager.DownloadManager
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import com.aliucord.manager.installer.util.installApks
|
||||
import com.aliucord.manager.network.utils.SemVer
|
||||
import com.aliucord.manager.network.utils.getOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UpdaterViewModel(
|
||||
private val github: GithubRepository,
|
||||
private val downloadManager: DownloadManager,
|
||||
private val application: Application
|
||||
) : ViewModel() {
|
||||
var showDialog by mutableStateOf(false)
|
||||
private set
|
||||
var targetVersion by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var isWorking by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private var targetApkUrl: String? = null
|
||||
|
||||
fun dismissDialog() {
|
||||
showDialog = false
|
||||
}
|
||||
|
||||
fun triggerUpdate() {
|
||||
if (isWorking) return
|
||||
val url = targetApkUrl ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
isWorking = true
|
||||
|
||||
application.externalCacheDir!!.resolve("manager.apk").apply {
|
||||
delete()
|
||||
|
||||
downloadManager.download(url, this)
|
||||
application.installApks(this)
|
||||
delay(10000)
|
||||
|
||||
showDialog = false
|
||||
delete()
|
||||
}
|
||||
|
||||
isWorking = false
|
||||
}
|
||||
}
|
||||
|
||||
// This updates to the latest release based on the tag name
|
||||
// It finds all releases that have an asset named aliucord-manager-[tagname].apk (this is in case the filename changes in the future)
|
||||
// Then compares the semantic versioning of the tag name which should be in the v1.0.0 format
|
||||
// Finds the latest and downloads/installs it
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val (version, asset) = github.getManagerReleases().getOrNull()
|
||||
?.mapNotNull { release ->
|
||||
val version = SemVer.parse(release.tagName, vPrefix = true)
|
||||
?: return@mapNotNull null
|
||||
|
||||
val asset = release.assets.find { it.name == "aliucord-manager-${release.tagName}.apk" }
|
||||
?: return@mapNotNull null
|
||||
|
||||
version to asset
|
||||
}
|
||||
?.maxByOrNull { it.first }
|
||||
?: return@launch
|
||||
|
||||
val currentVersion = SemVer.parse(BuildConfig.VERSION_NAME)
|
||||
?: throw Error("Failed to parse app version")
|
||||
|
||||
if (currentVersion >= version)
|
||||
return@launch
|
||||
|
||||
targetVersion = version.toString()
|
||||
targetApkUrl = asset.browserDownloadUrl
|
||||
showDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
<string name="action_apply">Apply</string>
|
||||
<string name="action_confirm">Confirm</string>
|
||||
<string name="action_cancel">Cancel</string>
|
||||
<string name="action_dismiss">Dismiss</string>
|
||||
<string name="action_install">Install</string>
|
||||
<string name="action_reinstall">Reinstall</string>
|
||||
<string name="action_update">Update</string>
|
||||
|
|
@ -125,4 +126,7 @@
|
|||
<string name="status_ongoing">Ongoing</string>
|
||||
<string name="status_failed">Failed</string>
|
||||
<string name="status_queued">Queued</string>
|
||||
|
||||
<string name="updater_body">A new update has been released for Aliucord Manager! It may be required in order to function properly. Would you like to update?</string>
|
||||
<string name="updater_title">Update to %1$s</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue