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
+76 -44

No files matched your search

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