Skip to content

Commit

Permalink
modify updater to be global
Browse files Browse the repository at this point in the history
  • Loading branch information
qimiko committed Jan 4, 2024
1 parent 1cf63b5 commit 65e9e6a
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 157 deletions.
11 changes: 9 additions & 2 deletions app/src/main/java/com/geode/launcher/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -86,8 +87,6 @@ fun UpdateCard(state: ReleaseViewModel.ReleaseUIState, modifier: Modifier = Modi

when (state) {
is ReleaseViewModel.ReleaseUIState.Failure -> {
state.exception.printStackTrace()

val message = state.exception.message

Text(
Expand Down Expand Up @@ -158,12 +157,20 @@ fun MainScreen(
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
)

val shouldUpdate by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)

val autoUpdateState by releaseViewModel.uiState.collectAsState()

val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
?.hasUpdated ?: false
val geodeInstalled = geodePreinstalled || geodeJustInstalled

LaunchedEffect(shouldUpdate) {
if (shouldUpdate && !releaseViewModel.hasPerformedCheck) {
releaseViewModel.runReleaseCheck()
}
}

Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
Expand Down
9 changes: 1 addition & 8 deletions app/src/main/java/com/geode/launcher/SettingsActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -113,8 +112,6 @@ fun UpdateIndicator(

when (updateStatus) {
is ReleaseViewModel.ReleaseUIState.Failure -> {
updateStatus.exception.printStackTrace()

snackbarHostState.showSnackbar(
context.getString(R.string.preference_check_for_updates_failed),
)
Expand Down Expand Up @@ -146,7 +143,6 @@ fun SettingsScreen(
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
val updateStatus by releaseViewModel.uiState.collectAsState()

var showUpdateProgress by remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }

Scaffold(
Expand Down Expand Up @@ -219,12 +215,9 @@ fun SettingsScreen(
modifier = Modifier
.clickable(onClick = {
releaseViewModel.runReleaseCheck()
showUpdateProgress = true
}, role = Role.Button)
) {
if (showUpdateProgress) {
UpdateIndicator(snackbarHostState, updateStatus)
}
UpdateIndicator(snackbarHostState, updateStatus)
}
}
/*
Expand Down
25 changes: 4 additions & 21 deletions app/src/main/java/com/geode/launcher/api/ReleaseRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,18 @@ class ReleaseRepository {

class HttpException(message: String) : IOException(message)

private var foundNightlyRelease: Release? = null
private var foundRelease: Release? = null

suspend fun getLatestNightlyRelease(isRefresh: Boolean = false): Release? {
if (!isRefresh && foundNightlyRelease != null) {
return foundNightlyRelease
}

suspend fun getLatestNightlyRelease(): Release? {
val nightlyPath = "$GITHUB_API_BASE/releases/tags/nightly"
val url = URL(nightlyPath)

val release = getReleaseByUrl(url)
foundNightlyRelease = release

return release
return getReleaseByUrl(url)
}

suspend fun getLatestRelease(isRefresh: Boolean = false): Release? {
if (!isRefresh && foundRelease != null) {
return foundRelease
}

suspend fun getLatestRelease(): Release? {
val releasePath = "$GITHUB_API_BASE/releases/latest"
val url = URL(releasePath)

val release = getReleaseByUrl(url)
foundRelease = release

return release
return getReleaseByUrl(url)
}

@OptIn(ExperimentalSerializationApi::class)
Expand Down
163 changes: 37 additions & 126 deletions app/src/main/java/com/geode/launcher/api/ReleaseViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,20 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.AP
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.geode.launcher.utils.DownloadUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.delay
import com.geode.launcher.utils.ReleaseManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.launch
import java.io.File
import java.io.IOException

class ReleaseViewModel(private val releaseRepository: ReleaseRepository, private val sharedPreferences: PreferenceUtils, private val application: Application): ViewModel() {

class ReleaseViewModel(private val application: Application): ViewModel() {
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = this[APPLICATION_KEY] as Application
val preferences = PreferenceUtils.get(application)

ReleaseViewModel(
releaseRepository = ReleaseRepository(),
sharedPreferences = preferences,
application = application
)
}
Expand All @@ -39,141 +32,59 @@ class ReleaseViewModel(private val releaseRepository: ReleaseRepository, private
data class Failure(val exception: Exception) : ReleaseUIState()
data class InDownload(val downloaded: Long, val outOf: Long) : ReleaseUIState()
data class Finished(val hasUpdated: Boolean = false) : ReleaseUIState()
}

private val _uiState = MutableStateFlow<ReleaseUIState>(ReleaseUIState.Finished())
val uiState = _uiState.asStateFlow()

private var isInUpdateCheck = false

private suspend fun <R> retry(block: suspend () -> R): R {
val maxAttempts = 5
val initialDelay = 1000L

repeat(maxAttempts - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
// only retry on exceptions that can be handled
if (e !is IOException) {
throw e
companion object {
fun managerStateToUI(state: ReleaseManager.ReleaseManagerState): ReleaseUIState {
return when (state) {
is ReleaseManager.ReleaseManagerState.InUpdateCheck -> InUpdateCheck
is ReleaseManager.ReleaseManagerState.Failure -> Failure(state.exception)
is ReleaseManager.ReleaseManagerState.InDownload ->
InDownload(state.downloaded, state.outOf)
is ReleaseManager.ReleaseManagerState.Finished -> Finished(state.hasUpdated)
}
}

delay(initialDelay * attempt)
}

// run final time for exceptions
return block()
}

private suspend fun getLatestRelease(): Release? {
_uiState.value = ReleaseUIState.InUpdateCheck

val useNightly = sharedPreferences.getBoolean(PreferenceUtils.Key.RELEASE_CHANNEL)
val latestRelease = retry {
if (useNightly) {
releaseRepository.getLatestNightlyRelease(true)
} else {
releaseRepository.getLatestRelease(true)
}
}

return latestRelease
}

private suspend fun checkForNewRelease() {
val release = try {
getLatestRelease()
} catch (e: Exception) {
_uiState.value = ReleaseUIState.Failure(e)
return
}

if (release == null) {
_uiState.value = ReleaseUIState.Finished()
return
}

val currentVersion = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_VERSION_TIMESTAMP)
val latestVersion = release.getDescriptor()

if (latestVersion <= currentVersion) {
_uiState.value = ReleaseUIState.Finished()
return
}

val releaseAsset = release.getAndroidDownload()
if (releaseAsset == null) {
val noAssetException = Exception("missing Android download")
_uiState.value = ReleaseUIState.Failure(noAssetException)

return
}

try {
createDownload(releaseAsset)
} catch (e: Exception) {
_uiState.value = ReleaseUIState.Failure(e)
return
}
private val _uiState = MutableStateFlow<ReleaseUIState>(ReleaseUIState.Finished())
val uiState = _uiState.asStateFlow()

// extraction performed
updatePreferences(release)
_uiState.value = ReleaseUIState.Finished(true)
}
private var isInUpdateCheck = false
var hasPerformedCheck = false
private set

fun runReleaseCheck() {
// don't run multiple checks
if (isInUpdateCheck) {
return
}

hasPerformedCheck = true

viewModelScope.launch {
isInUpdateCheck = true
checkForNewRelease()
isInUpdateCheck = false
}
}

private suspend fun createDownload(asset: Asset) {
_uiState.value = ReleaseUIState.InDownload(0, asset.size.toLong())

val outputFile = DownloadUtils.downloadFile(application, asset.browserDownloadUrl, asset.name) { progress, outOf ->
_uiState.value = ReleaseUIState.InDownload(progress, outOf)
}

try {
val geodeName = LaunchUtils.getGeodeFilename()

val fallbackPath = File(application.filesDir, "launcher")
val geodeDirectory = application.getExternalFilesDir("") ?: fallbackPath

val geodeFile = File(geodeDirectory, geodeName)

DownloadUtils.extractFileFromZip(outputFile, geodeFile, geodeName)
} finally {
// delete file now that it's no longer needed
outputFile.delete()
}
}
val releaseFlow = ReleaseManager.get(application)
.checkForUpdates(
// use the current scope so (hopefully) flow piping ends once collection is finished
CoroutineScope(coroutineContext)
)

private fun updatePreferences(release: Release) {
sharedPreferences.setString(
PreferenceUtils.Key.CURRENT_VERSION_TAG,
release.getDescription()
)
releaseFlow
.transformWhile {
// map the ui state into something that can be used
emit(ReleaseUIState.managerStateToUI(it))

sharedPreferences.setLong(
PreferenceUtils.Key.CURRENT_VERSION_TIMESTAMP,
release.getDescriptor()
)
}
// end collection once ReleaseManager reaches a state of "completion"
it !is ReleaseManager.ReleaseManagerState.Finished &&
it !is ReleaseManager.ReleaseManagerState.Failure
}
.collect {
// send the mapped state to the ui
_uiState.value = it
}

init {
val shouldUpdate = sharedPreferences.getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
if (shouldUpdate) {
runReleaseCheck()
isInUpdateCheck = false
}
}
}
Loading

0 comments on commit 65e9e6a

Please sign in to comment.