forked from geode-sdk/android-launcher
redo launch logic
This commit is contained in:
4 files changed
+368
-130
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
package com.geode.launcher
|
package com.geode.launcher
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.format.Formatter
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
@@ -15,19 +16,18 @@ import androidx.compose.foundation.layout.offset
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedCard
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -35,6 +35,7 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -48,11 +49,10 @@ import com.geode.launcher.main.*
|
|||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
import com.geode.launcher.ui.theme.LocalTheme
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
import com.geode.launcher.ui.theme.Theme
|
import com.geode.launcher.ui.theme.Theme
|
||||||
import com.geode.launcher.updater.ReleaseViewModel
|
|
||||||
import com.geode.launcher.utils.Constants
|
|
||||||
import com.geode.launcher.utils.GamePackageUtils
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AltMainActivity : ComponentActivity() {
|
class AltMainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -84,6 +84,12 @@ class AltMainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
|
||||||
|
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
||||||
|
|
||||||
|
if (loadFailureInfo != null) {
|
||||||
|
launchViewModel.loadFailure = loadFailureInfo
|
||||||
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||||
Surface(
|
Surface(
|
||||||
@@ -91,7 +97,7 @@ class AltMainActivity : ComponentActivity() {
|
|||||||
color = Color.Transparent
|
color = Color.Transparent
|
||||||
) {
|
) {
|
||||||
WithWave {
|
WithWave {
|
||||||
AltMainScreen(gdInstalled, geodeInstalled, loadFailureInfo)
|
AltMainScreen(launchViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,17 +114,140 @@ class AltMainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
data class LaunchStatusInfo(
|
||||||
fun LaunchProgressBody(title: String, details: String, modifier: Modifier = Modifier) {
|
val title: String,
|
||||||
|
val details: String? = null,
|
||||||
|
val progress: (() -> Float)? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchCancelledIcon(cancelReason: LaunchViewModel.LaunchCancelReason) {
|
||||||
|
when (cancelReason) {
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED,
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> Icon(painterResource(R.drawable.icon_error), contentDescription = null)
|
||||||
|
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
|
||||||
|
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(Icons.Default.Warning, contentDescription = null)
|
||||||
|
else -> Icon(Icons.Default.Info, contentDescription = null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LaunchProgressCard(modifier: Modifier = Modifier) {
|
fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): LaunchStatusInfo {
|
||||||
Card(modifier = modifier) {
|
return when (cancelReason) {
|
||||||
Column {
|
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_crash)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.geode_download_title)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_error),
|
||||||
|
details = stringResource(R.string.game_not_found)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_error),
|
||||||
|
details = stringResource(R.string.launcher_cancelled_outdated)
|
||||||
|
)
|
||||||
|
else -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_manual)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState): LaunchStatusInfo {
|
||||||
|
return when (state) {
|
||||||
|
is LaunchViewModel.LaunchUIState.Initial,
|
||||||
|
is LaunchViewModel.LaunchUIState.Working,
|
||||||
|
is LaunchViewModel.LaunchUIState.Ready -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_starting_game)
|
||||||
|
)
|
||||||
|
is LaunchViewModel.LaunchUIState.UpdateCheck -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.release_fetch_in_progress)
|
||||||
|
)
|
||||||
|
is LaunchViewModel.LaunchUIState.Updating -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val downloaded = remember(state.downloaded) {
|
||||||
|
Formatter.formatShortFileSize(context, state.downloaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
val outOf = remember(state.outOf) {
|
||||||
|
Formatter.formatShortFileSize(context, state.outOf)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_downloading_update),
|
||||||
|
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf),
|
||||||
|
progress = {
|
||||||
|
val progress = state.downloaded / state.outOf.toDouble()
|
||||||
|
progress.toFloat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
||||||
|
return mapCancelReasonToInfo(state.reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Unit, inProgress: Boolean) {
|
||||||
|
Row {
|
||||||
|
icon()
|
||||||
|
Text(statusInfo.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inProgress) {
|
||||||
|
LinearProgressIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusInfo.details != null) {
|
||||||
|
Text(statusInfo.details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifier) {
|
||||||
|
Column {
|
||||||
|
Text(statusInfo.title)
|
||||||
|
|
||||||
|
if (statusInfo.progress != null) {
|
||||||
|
LinearProgressIndicator(statusInfo.progress)
|
||||||
|
} else {
|
||||||
|
LinearProgressIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusInfo.details != null) {
|
||||||
|
Text(statusInfo.details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchProgressCard(uiState: LaunchViewModel.LaunchUIState, onCancel: () -> Unit, onResume: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
val status = mapLaunchStatusToInfo(uiState)
|
||||||
|
Card(modifier = modifier) {
|
||||||
|
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||||
|
LaunchCancelledBody(
|
||||||
|
statusInfo = status,
|
||||||
|
icon = { LaunchCancelledIcon(uiState.reason) },
|
||||||
|
inProgress = uiState.inProgress
|
||||||
|
)
|
||||||
|
|
||||||
|
if (uiState.reason.allowsRetry()) {
|
||||||
|
TextButton(onClick = onResume) {
|
||||||
|
Text(stringResource(R.string.launcher_cancelled_restart))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchProgressBody(statusInfo = status)
|
||||||
|
|
||||||
|
TextButton(onClick = onCancel) {
|
||||||
|
Text(stringResource(R.string.release_fetch_button_cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,126 +273,41 @@ fun GeodeLogo(modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AltMainScreen(
|
fun AltMainScreen(
|
||||||
gdInstalled: Boolean = true,
|
launchViewModel: LaunchViewModel = viewModel(factory = LaunchViewModel.Factory)
|
||||||
geodePreinstalled: Boolean = true,
|
|
||||||
loadFailureInfo: LoadFailureInfo? = null,
|
|
||||||
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val shouldUpdate by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
LaunchedEffect(Unit) {
|
||||||
val autoUpdateState by releaseViewModel.uiState.collectAsState()
|
launchViewModel.beginLaunchFlow()
|
||||||
|
}
|
||||||
|
|
||||||
val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
|
val launchUIState by launchViewModel.uiState.collectAsState()
|
||||||
?.hasUpdated ?: false
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val geodeInstalled = geodePreinstalled || geodeJustInstalled
|
|
||||||
|
|
||||||
var beginLaunch by remember { mutableStateOf(false) }
|
|
||||||
var launchInSafeMode by remember { mutableStateOf(false) }
|
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(shouldUpdate) {
|
|
||||||
if (shouldUpdate && !releaseViewModel.hasPerformedCheck && gdInstalled) {
|
|
||||||
releaseViewModel.runReleaseCheck()
|
|
||||||
} else {
|
|
||||||
releaseViewModel.useGlobalCheckState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
|
|
||||||
// automatically show load failed dialog (but not crash)
|
|
||||||
var showErrorInfo by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
loadFailureInfo != null && loadFailureInfo.title != LaunchUtils.LauncherError.CRASHED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasError = loadFailureInfo != null
|
|
||||||
LaunchedEffect(hasError, showErrorInfo) {
|
|
||||||
if (!showErrorInfo && loadFailureInfo != null) {
|
|
||||||
showFailureSnackbar(context, loadFailureInfo, snackbarHostState) {
|
|
||||||
showErrorInfo = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showErrorInfo && loadFailureInfo != null) {
|
|
||||||
ErrorInfoSheet(loadFailureInfo, onDismiss = { showErrorInfo = false })
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = Color.White,
|
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
|
||||||
) { innerPadding ->
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.padding(innerPadding)
|
|
||||||
.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
GeodeLogo(modifier = Modifier.offset(y = (-90).dp))
|
GeodeLogo(modifier = Modifier.offset(y = (-90).dp))
|
||||||
|
|
||||||
when {
|
LaunchProgressCard(
|
||||||
gdInstalled && geodeInstalled -> {
|
launchUIState,
|
||||||
val gdVersion = remember {
|
onCancel = {
|
||||||
GamePackageUtils.getGameVersionCode(context.packageManager)
|
launchInSafeMode = false
|
||||||
|
coroutineScope.launch {
|
||||||
|
launchViewModel.cancelLaunch()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
if (gdVersion < Constants.SUPPORTED_VERSION_CODE) {
|
onResume = {
|
||||||
val versionName = remember {
|
launchViewModel.loadFailure = null
|
||||||
GamePackageUtils.getUnifiedVersionName(context.packageManager)
|
coroutineScope.launch {
|
||||||
|
launchViewModel.beginLaunchFlow()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
LaunchBlockedLabel(stringResource(R.string.game_outdated, versionName))
|
modifier = Modifier.offset(y = 90.dp)
|
||||||
} else {
|
|
||||||
val waitForUpdate = (shouldUpdate && !releaseViewModel.hasPerformedCheck) || releaseViewModel.isInUpdate
|
|
||||||
LaunchedEffect(waitForUpdate) {
|
|
||||||
if (!waitForUpdate) {
|
|
||||||
// beginLaunch = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gdInstalled -> {
|
|
||||||
Text(
|
|
||||||
context.getString(R.string.geode_download_title),
|
|
||||||
modifier = Modifier.padding(12.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Row {
|
|
||||||
Button(
|
|
||||||
onClick = { releaseViewModel.runReleaseCheck(true) },
|
|
||||||
enabled = !releaseViewModel.isInUpdate
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_download),
|
|
||||||
contentDescription = context.getString(R.string.launcher_download_icon_alt)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(context.getString(R.string.launcher_download))
|
|
||||||
}
|
|
||||||
Spacer(Modifier.size(2.dp))
|
|
||||||
IconButton(onClick = { onSettings(context) }) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Settings,
|
|
||||||
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> LaunchBlockedLabel(stringResource(R.string.game_not_found))
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedCard(
|
|
||||||
modifier = Modifier.offset(y = 70.dp)
|
|
||||||
) {
|
|
||||||
UpdateCard(
|
|
||||||
releaseViewModel,
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -272,7 +316,13 @@ fun AltMainScreen(
|
|||||||
verticalAlignment = Alignment.Bottom
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { onSettings(context) },
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
launchViewModel.cancelLaunch()
|
||||||
|
}
|
||||||
|
|
||||||
|
onSettings(context)
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Settings,
|
Icons.Filled.Settings,
|
||||||
@@ -283,12 +333,13 @@ fun AltMainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (beginLaunch) {
|
if (launchUIState is LaunchViewModel.LaunchUIState.Ready) {
|
||||||
UpdateWarning(launchInSafeMode) {
|
UpdateWarning(launchInSafeMode) {
|
||||||
beginLaunch = false
|
|
||||||
launchInSafeMode = false
|
launchInSafeMode = false
|
||||||
|
coroutineScope.launch {
|
||||||
|
launchViewModel.cancelLaunch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,11 +16,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.max
|
|
||||||
import androidx.compose.ui.unit.min
|
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@@ -45,9 +42,6 @@ fun WithWave(modifier: Modifier = Modifier, contents: @Composable () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
contents()
|
contents()
|
||||||
|
|
||||||
val configuration = LocalConfiguration.current
|
|
||||||
val screenHeight = configuration.screenHeightDp.dp
|
|
||||||
|
|
||||||
WaveBackground(
|
WaveBackground(
|
||||||
color = WAVE_COLOR_1,
|
color = WAVE_COLOR_1,
|
||||||
period = 500.dp,
|
period = 500.dp,
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package com.geode.launcher.main
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
|
import com.geode.launcher.updater.ReleaseManager
|
||||||
|
import com.geode.launcher.utils.Constants
|
||||||
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class LaunchViewModel(private val application: Application): ViewModel() {
|
||||||
|
companion object {
|
||||||
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
|
initializer {
|
||||||
|
val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
|
||||||
|
|
||||||
|
LaunchViewModel(
|
||||||
|
application = application
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var readyTimerPassed = false
|
||||||
|
private var readyTimer: CountDownTimer? = null
|
||||||
|
|
||||||
|
private fun initReadyTimer() {
|
||||||
|
readyTimer = object : CountDownTimer(1000, 1000) {
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
// no tick necessary
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
readyTimerPassed = true
|
||||||
|
|
||||||
|
if (_uiState.value is LaunchUIState.Working) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
preReadyCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class LaunchCancelReason {
|
||||||
|
MANUAL, LAST_LAUNCH_CRASHED, GEODE_NOT_FOUND, GAME_MISSING, GAME_OUTDATED;
|
||||||
|
|
||||||
|
fun allowsRetry() = when (this) {
|
||||||
|
MANUAL,
|
||||||
|
LAST_LAUNCH_CRASHED,
|
||||||
|
GEODE_NOT_FOUND -> true
|
||||||
|
GAME_MISSING,
|
||||||
|
GAME_OUTDATED -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class LaunchUIState {
|
||||||
|
data object Initial : LaunchUIState()
|
||||||
|
data object UpdateCheck : LaunchUIState()
|
||||||
|
data class Updating(val downloaded: Long, val outOf: Long) : LaunchUIState()
|
||||||
|
data class Cancelled(val reason: LaunchCancelReason, val inProgress: Boolean = false) : LaunchUIState()
|
||||||
|
data object Working : LaunchUIState()
|
||||||
|
data object Ready : LaunchUIState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<LaunchUIState>(LaunchUIState.Initial)
|
||||||
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
var loadFailure: LoadFailureInfo? = null
|
||||||
|
private var hasCheckedForUpdates = false
|
||||||
|
private var isCancelling = false
|
||||||
|
|
||||||
|
private suspend fun determineUpdateStatus() {
|
||||||
|
ReleaseManager.get(application).uiState.takeWhile {
|
||||||
|
it is ReleaseManager.ReleaseManagerState.InUpdateCheck || it is ReleaseManager.ReleaseManagerState.InDownload
|
||||||
|
}.map { when (it) {
|
||||||
|
is ReleaseManager.ReleaseManagerState.InUpdateCheck -> LaunchUIState.UpdateCheck
|
||||||
|
is ReleaseManager.ReleaseManagerState.InDownload -> LaunchUIState.Updating(it.downloaded, it.outOf)
|
||||||
|
else -> LaunchUIState.UpdateCheck
|
||||||
|
}
|
||||||
|
}.collect {
|
||||||
|
_uiState.emit(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// flow terminates once update check is finished
|
||||||
|
hasCheckedForUpdates = true
|
||||||
|
preReadyCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun preReadyCheck() {
|
||||||
|
if (isCancelling) {
|
||||||
|
// don't let the game start if cancelled
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val packageManager = application.packageManager
|
||||||
|
|
||||||
|
if (!GamePackageUtils.isGameInstalled(packageManager)) {
|
||||||
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReleaseManager.get(application).isInUpdate) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
determineUpdateStatus()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadFailure != null) {
|
||||||
|
// last launch crashed, so cancel
|
||||||
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.LAST_LAUNCH_CRASHED))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GamePackageUtils.getGameVersionCode(packageManager) < Constants.SUPPORTED_VERSION_CODE) {
|
||||||
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LaunchUtils.isGeodeInstalled(application)) {
|
||||||
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readyTimerPassed) {
|
||||||
|
return _uiState.emit(LaunchUIState.Working)
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.emit(LaunchUIState.Ready)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun beginLaunchFlow() {
|
||||||
|
if (_uiState.value !is LaunchUIState.Initial && _uiState.value !is LaunchUIState.Cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isCancelling = false
|
||||||
|
|
||||||
|
if (!GamePackageUtils.isGameInstalled(application.packageManager)) {
|
||||||
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readyTimer == null) {
|
||||||
|
initReadyTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
||||||
|
if (shouldUpdate && !hasCheckedForUpdates) {
|
||||||
|
ReleaseManager.get(application).checkForUpdates(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
preReadyCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun cancelLaunch() {
|
||||||
|
isCancelling = true
|
||||||
|
_uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.MANUAL, true))
|
||||||
|
|
||||||
|
val releaseManager = ReleaseManager.get(application)
|
||||||
|
if (releaseManager.isInUpdate) {
|
||||||
|
releaseManager.cancelUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.MANUAL, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
|
||||||
|
readyTimer?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,20 @@
|
|||||||
<string name="launcher_error_export_crash">Share crash</string>
|
<string name="launcher_error_export_crash">Share crash</string>
|
||||||
<string name="launcher_error_export_missing">No crash dump was found!</string>
|
<string name="launcher_error_export_missing">No crash dump was found!</string>
|
||||||
<string name="launcher_error_device_info">Android %2$s, %1$s</string>
|
<string name="launcher_error_device_info">Android %2$s, %1$s</string>
|
||||||
<string name="launcher_settings_notification_body">Launcher Settings</string>
|
|
||||||
|
<string name="launcher_notification_settings">Launcher Settings</string>
|
||||||
|
<string name="launcher_notification_update_failed">Failed to check for updates.</string>
|
||||||
|
<string name="launcher_notification_update_cancelled">Update cancelled.</string>
|
||||||
|
<string name="launcher_notification_update_success">Geode updated.</string>
|
||||||
|
|
||||||
|
<string name="launcher_starting_game">Starting Geometry Dash…</string>
|
||||||
|
<string name="launcher_downloading_update">Downloading update…</string>
|
||||||
|
<string name="launcher_downloading_update_details">(%1$s/%2$s)</string>
|
||||||
|
<string name="launcher_cancelled_manual">Launch paused.</string>
|
||||||
|
<string name="launcher_cancelled_crash">Launch paused due to a crash.</string>
|
||||||
|
<string name="launcher_cancelled_error">Could not start Geode.</string>
|
||||||
|
<string name="launcher_cancelled_outdated">A Geometry Dash update is required.</string>
|
||||||
|
<string name="launcher_cancelled_restart">Restart</string>
|
||||||
|
|
||||||
<!-- Updater strings -->
|
<!-- Updater strings -->
|
||||||
<string name="release_fetch_failed">Update failed.\n%1$s</string>
|
<string name="release_fetch_failed">Update failed.\n%1$s</string>
|
||||||
@@ -146,4 +159,5 @@
|
|||||||
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
|
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
|
||||||
<string name="application_logs_crash_only">Show Crash Buffer</string>
|
<string name="application_logs_crash_only">Show Crash Buffer</string>
|
||||||
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
|
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
|
||||||
|
<string name="title_activity_alt_main">AltMainActivity</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user