diff --git a/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt b/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt index 083a5e3..e694d97 100644 --- a/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt +++ b/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt @@ -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, diff --git a/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt index 0a28d2e..bea049c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt +++ b/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt @@ -11,4 +11,5 @@ val viewModelModule = module { viewModelOf(::AboutViewModel) viewModelOf(::InstallViewModel) viewModelOf(::SettingsViewModel) + viewModelOf(::UpdaterViewModel) } diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt index 5dd404f..4813c31 100644 --- a/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt @@ -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 diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt b/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt index 5eaf83c..ef1d1bb 100644 --- a/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt +++ b/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt @@ -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() } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/util/PackageInstaller.kt b/app/src/main/kotlin/com/aliucord/manager/installer/util/PackageInstaller.kt index 5827a18..b225b76 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/util/PackageInstaller.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/util/PackageInstaller.kt @@ -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) diff --git a/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt b/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt index 6f23965..4a003a5 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt @@ -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> = 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" } } diff --git a/app/src/main/kotlin/com/aliucord/manager/network/utils/ApiResponse.kt b/app/src/main/kotlin/com/aliucord/manager/network/utils/ApiResponse.kt index b426047..77d8923 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/utils/ApiResponse.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/utils/ApiResponse.kt @@ -54,6 +54,13 @@ inline fun ApiResponse.getOrThrow(): T { ) } +inline fun ApiResponse.getOrNull(): T? { + return fold( + success = { it }, + fail = { null } + ) +} + @Suppress("UNCHECKED_CAST") inline fun ApiResponse.chain(block: (T) -> ApiResponse): ApiResponse { return if (this !is ApiResponse.Success) { diff --git a/app/src/main/kotlin/com/aliucord/manager/network/utils/SemVer.kt b/app/src/main/kotlin/com/aliucord/manager/network/utils/SemVer.kt new file mode 100644 index 0000000..6ea09b6 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/network/utils/SemVer.kt @@ -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 { + 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]) + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/dialog/UpdaterDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/dialog/UpdaterDialog.kt new file mode 100644 index 0000000..8bc37aa --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/dialog/UpdaterDialog.kt @@ -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 + ) + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/UpdaterViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/UpdaterViewModel.kt new file mode 100644 index 0000000..eeeabfd --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/UpdaterViewModel.kt @@ -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(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 + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cdafc5c..aaa93e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ Apply Confirm Cancel + Dismiss Install Reinstall Update @@ -125,4 +126,7 @@ Ongoing Failed Queued + + 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