proper initial state check

This commit is contained in:
Chloe committed 2026-02-15 04:05:37 -07:00
1 parent 4c0d9b88d2
commit 9b7d174704
4 files changed
+71 -39

No files matched your search

@@ -151,6 +151,8 @@ class MainActivity : ComponentActivity() {
launchViewModel.launchArguments = launchArguments
}
launchViewModel.beginLaunchFlow()
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
Surface(
@@ -176,6 +178,7 @@ fun LaunchCancelledIcon(cancelReason: LaunchViewModel.LaunchCancelReason, modifi
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED,
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> Icon(painterResource(R.drawable.icon_error), contentDescription = null, modifier)
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED_AUTOMATIC,
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(painterResource(R.drawable.icon_warning), contentDescription = null, modifier)
else -> Icon(painterResource(R.drawable.icon_info), contentDescription = null, modifier)
}
@@ -187,6 +190,9 @@ fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): Lau
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_crash)
)
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED_AUTOMATIC -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_crash_instant)
)
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> LaunchStatusInfo(
title = stringResource(R.string.geode_download_title)
)
@@ -323,6 +329,7 @@ fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_cancelled_resume))
}
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED_AUTOMATIC,
LaunchViewModel.LaunchCancelReason.MANUAL,
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
Icon(
@@ -659,10 +666,6 @@ fun AltMainScreen(
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
launchViewModel.beginLaunchFlow()
}
val launchUIState by launchViewModel.uiState.collectAsState()
val coroutineScope = rememberCoroutineScope()
var launchInSafeMode by remember {
@@ -65,17 +65,25 @@ class LaunchViewModel(private val application: Application): ViewModel() {
}
enum class LaunchCancelReason {
MANUAL, AUTOMATIC, LAST_LAUNCH_CRASHED, GEODE_NOT_FOUND, GAME_MISSING, GAME_OUTDATED;
MANUAL, AUTOMATIC, LAST_LAUNCH_CRASHED, LAST_LAUNCH_CRASHED_AUTOMATIC,
GEODE_NOT_FOUND, GAME_MISSING, GAME_OUTDATED;
fun allowsRetry() = when (this) {
MANUAL,
AUTOMATIC,
LAST_LAUNCH_CRASHED,
LAST_LAUNCH_CRASHED_AUTOMATIC,
GEODE_NOT_FOUND -> true
GAME_MISSING,
GAME_OUTDATED -> false
}
fun isCrash() = when (this) {
LAST_LAUNCH_CRASHED,
LAST_LAUNCH_CRASHED_AUTOMATIC -> true
else -> false
}
fun isGameInstallIssue() = when (this) {
GAME_OUTDATED,
GAME_MISSING -> true
@@ -100,20 +108,7 @@ class LaunchViewModel(private val application: Application): ViewModel() {
}
}
private val _uiState: MutableStateFlow<LaunchUIState>
init {
val preferenceUtils = PreferenceUtils.get(application)
val initialCancel = !preferenceUtils.getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
&& !preferenceUtils.getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
// fixes a single frame of progress being displayed when we're not actually going to do work
_uiState = MutableStateFlow<LaunchUIState>(
if (initialCancel) LaunchUIState.Cancelled(LaunchCancelReason.AUTOMATIC)
else LaunchUIState.Initial
)
}
private val _uiState = MutableStateFlow<LaunchUIState>(LaunchUIState.Initial)
val uiState = _uiState.asStateFlow()
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdateTag
@@ -153,7 +148,7 @@ class LaunchViewModel(private val application: Application): ViewModel() {
fun currentCrashInfo(): LoadFailureInfo? {
val currentState = _uiState.value
if (currentState is LaunchUIState.Cancelled && currentState.reason == LaunchCancelReason.LAST_LAUNCH_CRASHED) {
if (currentState is LaunchUIState.Cancelled && currentState.reason.isCrash()) {
return loadFailure
}
@@ -162,12 +157,12 @@ class LaunchViewModel(private val application: Application): ViewModel() {
fun clearCrashInfo() {
val currentState = _uiState.value
if (currentState is LaunchUIState.Cancelled && currentState.reason == LaunchCancelReason.LAST_LAUNCH_CRASHED) {
if (currentState is LaunchUIState.Cancelled && currentState.reason.isCrash()) {
loadFailure = null
}
}
private suspend fun preReadyCheck() {
private fun preReadyCheck() {
if (isCancelling) {
// don't let the game start if cancelled
return
@@ -176,7 +171,8 @@ class LaunchViewModel(private val application: Application): ViewModel() {
val packageManager = application.packageManager
if (!GamePackageUtils.isGameInstalled(packageManager)) {
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
return
}
if (ReleaseManager.get(application).isInUpdate) {
@@ -186,17 +182,25 @@ class LaunchViewModel(private val application: Application): ViewModel() {
return
}
if (loadFailure != null) {
// last launch crashed, so cancel
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.LAST_LAUNCH_CRASHED))
if (loadFailure != null) {
// show a different message if this is the first thing users see
val cancelType = if (_uiState.value == LaunchUIState.Initial) {
LaunchCancelReason.LAST_LAUNCH_CRASHED_AUTOMATIC
} else LaunchCancelReason.LAST_LAUNCH_CRASHED
_uiState.tryEmit(LaunchUIState.Cancelled(cancelType))
return
}
if (GamePackageUtils.getGameVersionCode(packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN) {
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
return
}
if (!LaunchUtils.isGeodeInstalled(application)) {
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
return
}
// if forcing immediate launch, then act as if it's manually started (skips timers)
@@ -212,13 +216,14 @@ class LaunchViewModel(private val application: Application): ViewModel() {
}
if (!readyTimerPassed && !hasManuallyStarted) {
return _uiState.emit(LaunchUIState.Working)
_uiState.tryEmit(LaunchUIState.Working)
return
}
_uiState.emit(LaunchUIState.Ready)
_uiState.tryEmit(LaunchUIState.Ready)
}
suspend fun beginLaunchFlow(isRestart: Boolean = false) {
fun beginLaunchFlow(isRestart: Boolean = false) {
if (isRestart) {
hasManuallyStarted = true
}
@@ -234,17 +239,23 @@ class LaunchViewModel(private val application: Application): ViewModel() {
isCancelling = false
if (!GamePackageUtils.isGameInstalled(application.packageManager)) {
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
return
}
if (readyTimer == null) {
initReadyTimer()
}
val releaseManager = ReleaseManager.get(application)
val hasGeode = LaunchUtils.isGeodeInstalled(application)
val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) {
ReleaseManager.get(application).checkForUpdates(false)
releaseManager.checkForUpdates(false)
} else {
releaseManager.afterUseCachedUpdate()
}
preReadyCheck()
@@ -261,7 +272,7 @@ class LaunchViewModel(private val application: Application): ViewModel() {
}
}
suspend fun cancelLaunch(isAutomatic: Boolean = false) {
fun cancelLaunch(isAutomatic: Boolean = false) {
// no need to double cancel
if (_uiState.value is LaunchUIState.Cancelled) {
return
@@ -273,11 +284,14 @@ class LaunchViewModel(private val application: Application): ViewModel() {
val releaseManager = ReleaseManager.get(application)
if (releaseManager.isInUpdate) {
_uiState.emit(LaunchUIState.Cancelled(reason, true))
_uiState.tryEmit(LaunchUIState.Cancelled(reason, true))
viewModelScope.launch {
releaseManager.cancelUpdate()
}
}
_uiState.emit(LaunchUIState.Cancelled(reason, false))
_uiState.tryEmit(LaunchUIState.Cancelled(reason, false))
}
override fun onCleared() {
@@ -266,7 +266,7 @@ class ReleaseManager private constructor(
}
}
private fun checkCachedLauncherUpdate() {
fun checkCachedLauncherUpdate() {
val lastUpdate = PreferenceUtils.get(applicationContext)
.getString(PreferenceUtils.Key.LAST_LAUNCHER_UPDATE) ?: return
@@ -283,15 +283,25 @@ class ReleaseManager private constructor(
return downloadLauncherUpdate(update)
}
private suspend fun checkForNewRelease(isManual: Boolean = false) {
fun shouldUseCache(): Boolean {
val sharedPreferences = PreferenceUtils.get(applicationContext)
val lastCheckTime = sharedPreferences.getLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME)
// only check for updates if it's been over 15 minutes since last check
val checkMinTime = Clock.System.now().minus(15.minutes).toEpochMilliseconds()
if (!isManual && lastCheckTime > checkMinTime && !fileWasExternallyModified()) {
return lastCheckTime > checkMinTime && !fileWasExternallyModified()
}
fun afterUseCachedUpdate() {
checkCachedLauncherUpdate()
_uiState.value = ReleaseManagerState.Finished()
_uiState.tryEmit(ReleaseManagerState.Finished())
}
private suspend fun checkForNewRelease(isManual: Boolean = false) {
val sharedPreferences = PreferenceUtils.get(applicationContext)
if (!isManual && shouldUseCache()) {
afterUseCachedUpdate()
return
}
@@ -388,10 +398,14 @@ class ReleaseManager private constructor(
@OptIn(DelicateCoroutinesApi::class)
fun checkForUpdates(isManual: Boolean = false): StateFlow<ReleaseManagerState> {
if (!isInUpdate) {
if (isManual || !shouldUseCache()) {
_uiState.value = ReleaseManagerState.InUpdateCheck
updateJob = GlobalScope.launch {
checkForNewRelease(isManual)
}
} else {
afterUseCachedUpdate()
}
}
return _uiState.asStateFlow()
+1
View File
@@ -78,6 +78,7 @@
<string name="launcher_cancelled_manual">Launch paused.</string>
<string name="launcher_cancelled_navigated_away">Launch paused.</string>
<string name="launcher_cancelled_crash">Launch paused due to a crash.</string>
<string name="launcher_cancelled_crash_instant">Geode crashed.</string>
<string name="launcher_cancelled_error">Geode cannot be used.</string>
<string name="launcher_cancelled_outdated">A Geometry Dash update is required.</string>
<string name="launcher_cancelled_restart">Restart</string>