mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-22 16:42:38 +00:00
proper initial state check
This commit is contained in:
4 files changed
+71
-39
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadFailure != null) {
|
|
||||||
// last launch crashed, so cancel
|
// 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) {
|
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))
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
releaseManager.cancelUpdate()
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun afterUseCachedUpdate() {
|
||||||
checkCachedLauncherUpdate()
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,10 +398,14 @@ 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) {
|
||||||
|
if (isManual || !shouldUseCache()) {
|
||||||
_uiState.value = ReleaseManagerState.InUpdateCheck
|
_uiState.value = ReleaseManagerState.InUpdateCheck
|
||||||
updateJob = GlobalScope.launch {
|
updateJob = GlobalScope.launch {
|
||||||
checkForNewRelease(isManual)
|
checkForNewRelease(isManual)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
afterUseCachedUpdate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _uiState.asStateFlow()
|
return _uiState.asStateFlow()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user