perform launcher download during update

This commit is contained in:
Chloe committed 2026-02-01 21:04:32 -07:00
1 parent 2754cc4bb8
commit 9d4c27ac92
3 files changed
+118 -103

No files matched your search

@@ -14,13 +14,10 @@ import androidx.compose.foundation.layout.Spacer
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.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
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.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@@ -29,7 +26,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
@@ -51,10 +47,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.geode.launcher.InstallReceiver import com.geode.launcher.InstallReceiver
import com.geode.launcher.R import com.geode.launcher.R
import com.geode.launcher.main.LauncherUpdater.downloadUpdate
import com.geode.launcher.ui.theme.Typography import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.updater.ReleaseManager import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
@@ -63,9 +58,12 @@ import com.mikepenz.markdown.compose.LocalBulletListHandler
import com.mikepenz.markdown.m3.Markdown import com.mikepenz.markdown.m3.Markdown
import com.mikepenz.markdown.m3.markdownTypography import com.mikepenz.markdown.m3.markdownTypography
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import java.io.File import java.io.File
@@ -101,6 +99,7 @@ fun clearDownloadedApks(context: Context) {
object LauncherUpdater { object LauncherUpdater {
enum class LauncherUpdateState { enum class LauncherUpdateState {
Began, Began,
Downloading,
Active, Active,
Inactive, Inactive,
Finished; Finished;
@@ -113,12 +112,16 @@ object LauncherUpdater {
_uiState.emit(LauncherUpdateState.Inactive) _uiState.emit(LauncherUpdateState.Inactive)
} }
suspend fun installPackage(context: Context, file: File) { suspend fun downloadUpdate(context: Context): File? {
if (!file.exists()) { _uiState.emit(LauncherUpdateState.Downloading)
_uiState.emit(LauncherUpdateState.Finished)
return
}
return withContext(Dispatchers.IO) {
ReleaseManager.get(context)
.downloadLatestLauncherUpdate()
}
}
suspend fun installPackage(context: Context, file: File) {
_uiState.emit(LauncherUpdateState.Began) _uiState.emit(LauncherUpdateState.Began)
val installer = context.packageManager.packageInstaller val installer = context.packageManager.packageInstaller
@@ -147,7 +150,7 @@ object LauncherUpdater {
} }
}) })
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) { runInterruptible {
file.inputStream().use { apkStream -> file.inputStream().use { apkStream ->
val length = file.length() val length = file.length()
@@ -160,46 +163,39 @@ object LauncherUpdater {
} }
val sessionId = installer.createSession(params) val sessionId = installer.createSession(params)
val session = installer.openSession(sessionId) installer.openSession(sessionId).use { session ->
session.openWrite("package", 0, length).use { sessionStream ->
apkStream.copyTo(sessionStream)
session.fsync(sessionStream)
}
session.openWrite("package", 0, length).use { sessionStream -> val intent = Intent(context, InstallReceiver::class.java)
apkStream.copyTo(sessionStream) val pi = PendingIntent.getBroadcast(
session.fsync(sessionStream) context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
session.commit(pi.intentSender)
} }
val intent = Intent(context, InstallReceiver::class.java)
val pi = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
session.commit(pi.intentSender)
session.close()
} }
} }}
} }
} }
suspend fun installLauncherUpdate(context: Context) { suspend fun installLauncherUpdate(context: Context) {
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value val launcherDownload = downloadUpdate(context)
val launcherDownload = nextUpdate?.getDownload()
if (launcherDownload != null) { if (launcherDownload != null) {
val outputFile = launcherDownload.filename if (!launcherDownload.exists()) {
val baseDirectory = context.filesDir
val outputPathFile = File(baseDirectory, outputFile)
if (!outputPathFile.exists()) {
return return
} }
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.CLEANUP_APKS, true) PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.CLEANUP_APKS, true)
try { try {
LauncherUpdater.installPackage(context, outputPathFile) LauncherUpdater.installPackage(context, launcherDownload)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
val msg = context.getString(R.string.launcher_self_update_failed, e.message) val msg = context.getString(R.string.launcher_self_update_failed, e.message)
@@ -213,7 +209,7 @@ suspend fun installLauncherUpdate(context: Context) {
} }
@Composable @Composable
fun LauncherUpdateDialog(onDismiss: () -> Unit) { fun LauncherUpdateDialog(isCancelling: Boolean, onDismiss: () -> Unit) {
val updateState by LauncherUpdater.uiState.collectAsState() val updateState by LauncherUpdater.uiState.collectAsState()
val cancellable = updateState != LauncherUpdater.LauncherUpdateState.Active val cancellable = updateState != LauncherUpdater.LauncherUpdateState.Active
@@ -224,36 +220,32 @@ fun LauncherUpdateDialog(onDismiss: () -> Unit) {
} }
} }
if (cancellable) { AlertDialog(
return icon = {
} Icon(
painterResource(R.drawable.icon_update),
Dialog( contentDescription = null
onDismissRequest = { )
if (cancellable) onDismiss()
}, },
properties = DialogProperties(usePlatformDefaultWidth = false) title = {
) { if (isCancelling) {
Card( Text(stringResource(R.string.release_fetch_cancelling))
shape = RoundedCornerShape(16.dp), } else if (updateState == LauncherUpdater.LauncherUpdateState.Downloading) {
colors = CardDefaults.cardColors( Text(stringResource(R.string.launcher_downloading_update))
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh } else {
), Text(stringResource(R.string.launcher_self_update_progress))
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(18.dp),
modifier = Modifier.padding(24.dp)
) {
CircularProgressIndicator()
Text(
stringResource(R.string.launcher_self_update_progress),
style = MaterialTheme.typography.bodyLarge
)
} }
} },
} text = {
LinearProgressIndicator()
},
confirmButton = {
TextButton(onClick = onDismiss, enabled = cancellable && !isCancelling) {
Text(stringResource(R.string.message_box_cancel))
}
},
onDismissRequest = {}
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -265,10 +257,19 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var updateJob by remember { mutableStateOf<Job?>(null) }
var cancellingUpdate by remember { mutableStateOf(false) }
var beginUpdate by remember { mutableStateOf(false) } var beginUpdate by remember { mutableStateOf(false) }
if (beginUpdate) { if (beginUpdate) {
LauncherUpdateDialog(onDismiss = { LauncherUpdateDialog(cancellingUpdate, onDismiss = {
beginUpdate = false cancellingUpdate = true
coroutineScope.launch {
updateJob?.cancelAndJoin()
beginUpdate = false
}
}) })
} }
@@ -295,9 +296,11 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
Row(modifier = Modifier.padding(vertical = 12.dp)) { Row(modifier = Modifier.padding(vertical = 12.dp)) {
Button( Button(
onClick = { onClick = {
coroutineScope.launch { updateJob = coroutineScope.launch {
installLauncherUpdate(context) cancellingUpdate = false
beginUpdate = true beginUpdate = true
installLauncherUpdate(context)
} }
}, },
) { ) {
@@ -362,10 +365,19 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
} }
} }
var updateJob by remember { mutableStateOf<Job?>(null) }
var cancellingUpdate by remember { mutableStateOf(false) }
var beginUpdate by remember { mutableStateOf(false) } var beginUpdate by remember { mutableStateOf(false) }
if (beginUpdate) { if (beginUpdate) {
LauncherUpdateDialog { LauncherUpdateDialog(cancellingUpdate) {
beginUpdate = false cancellingUpdate = true
coroutineScope.launch {
updateJob?.cancelAndJoin()
updateJob = null
beginUpdate = false
}
} }
} }
@@ -394,9 +406,11 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
TextButton( TextButton(
onClick = { onClick = {
coroutineScope.launch { updateJob = coroutineScope.launch {
installLauncherUpdate(context) cancellingUpdate = false
beginUpdate = true beginUpdate = true
installLauncherUpdate(context)
} }
}, },
) { ) {
@@ -133,16 +133,18 @@ class ReleaseManager private constructor(
} }
} }
private suspend fun downloadLauncherUpdate(release: Downloadable) { private suspend fun downloadLauncherUpdate(release: Downloadable): File? {
val download = release.getDownload() ?: return val download = release.getDownload() ?: return null
val outputDirectory = applicationContext.filesDir val outputDirectory = applicationContext.filesDir
val outputFile = File(outputDirectory, download.filename) val outputFile = File(outputDirectory, download.filename)
/*
if (outputFile.exists()) { if (outputFile.exists()) {
// only download the apk once // only download the apk once
return return
} }
*/
_uiState.value = ReleaseManagerState.InDownload(0, download.size) _uiState.value = ReleaseManagerState.InDownload(0, download.size)
@@ -159,8 +161,10 @@ class ReleaseManager private constructor(
} }
} catch (e: Exception) { } catch (e: Exception) {
sendError(e) sendError(e)
return return null
} }
return outputFile
} }
private suspend fun performUpdate(release: Downloadable) { private suspend fun performUpdate(release: Downloadable) {
@@ -213,8 +217,6 @@ class ReleaseManager private constructor(
} catch (_: Exception) { } } catch (_: Exception) { }
} }
downloadLauncherUpdateIfNecessary()
// extraction performed // extraction performed
updatePreferences(release) updatePreferences(release)
_uiState.value = ReleaseManagerState.Finished(true) _uiState.value = ReleaseManagerState.Finished(true)
@@ -237,15 +239,29 @@ class ReleaseManager private constructor(
return originalFileHash != currentFileHash return originalFileHash != currentFileHash
} }
private fun checkLauncherUpdate(launcherUpdate: DownloadableLauncherRelease) { private suspend fun getLatestLauncherUpdate(): DownloadableLauncherRelease? {
if (launcherUpdate.release.tagName != BuildConfig.VERSION_NAME) { return try {
_availableLauncherUpdate.value = launcherUpdate releaseRepository.getLatestLauncherRelease()
} catch (e: Exception) {
sendError(e)
return null
} }
} }
private suspend fun downloadLauncherUpdateIfNecessary() { private suspend fun checkLauncherUpdate() {
val update = _availableLauncherUpdate.value ?: return val launcherRelease = getLatestLauncherUpdate() ?: return
downloadLauncherUpdate(update)
if (launcherRelease.release.tagName != BuildConfig.VERSION_NAME) {
_availableLauncherUpdate.value = launcherRelease
}
}
suspend fun downloadLatestLauncherUpdate(): File? {
val update = _availableLauncherUpdate.value
?: getLatestLauncherUpdate()
?: return null
return downloadLauncherUpdate(update)
} }
private suspend fun checkForNewRelease(isManual: Boolean = false) { private suspend fun checkForNewRelease(isManual: Boolean = false) {
@@ -266,23 +282,12 @@ class ReleaseManager private constructor(
return return
} }
val launcherRelease = try { checkLauncherUpdate()
releaseRepository.getLatestLauncherRelease()
} catch (e: Exception) {
sendError(e)
return
}
if (launcherRelease != null) {
checkLauncherUpdate(launcherRelease)
}
val currentTime = Clock.System.now().toEpochMilliseconds() val currentTime = Clock.System.now().toEpochMilliseconds()
sharedPreferences.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, currentTime) sharedPreferences.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, currentTime)
if (release == null) { if (release == null) {
downloadLauncherUpdateIfNecessary()
_uiState.value = ReleaseManagerState.Finished() _uiState.value = ReleaseManagerState.Finished()
return return
} }
@@ -292,8 +297,6 @@ class ReleaseManager private constructor(
// check if an update is needed // check if an update is needed
if (latestVersion == currentVersion && !fileWasExternallyModified()) { if (latestVersion == currentVersion && !fileWasExternallyModified()) {
downloadLauncherUpdateIfNecessary()
_uiState.value = ReleaseManagerState.Finished() _uiState.value = ReleaseManagerState.Finished()
return return
} }
@@ -302,8 +305,6 @@ class ReleaseManager private constructor(
// check if the file was externally modified // check if the file was externally modified
if (!allowOverwriting && fileWasExternallyModified(true)) { if (!allowOverwriting && fileWasExternallyModified(true)) {
downloadLauncherUpdateIfNecessary()
sendError(UpdateException(UpdateException.Reason.EXTERNAL_FILE_IN_USE)) sendError(UpdateException(UpdateException.Reason.EXTERNAL_FILE_IN_USE))
return return
} }
+1 -1
View File
@@ -67,7 +67,7 @@
<string name="launcher_self_update_failed_title">Update failed</string> <string name="launcher_self_update_failed_title">Update failed</string>
<string name="launcher_self_update_failed">Failed to install launcher update: %1$s.</string> <string name="launcher_self_update_failed">Failed to install launcher update: %1$s.</string>
<string name="launcher_self_update_progress">Updating Geode Launcher…</string> <string name="launcher_self_update_progress">Installing update…</string>
<string name="launcher_starting_game">Starting Geometry Dash…</string> <string name="launcher_starting_game">Starting Geometry Dash…</string>
<string name="launcher_downloading_update">Downloading update…</string> <string name="launcher_downloading_update">Downloading update…</string>