forked from geode-sdk/android-launcher
perform launcher download during update
This commit is contained in:
3 files changed
+101
-86
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,8 +163,7 @@ 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 ->
|
session.openWrite("package", 0, length).use { sessionStream ->
|
||||||
apkStream.copyTo(sessionStream)
|
apkStream.copyTo(sessionStream)
|
||||||
session.fsync(sessionStream)
|
session.fsync(sessionStream)
|
||||||
@@ -176,30 +178,24 @@ object LauncherUpdater {
|
|||||||
)
|
)
|
||||||
|
|
||||||
session.commit(pi.intentSender)
|
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)
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
if (isCancelling) {
|
||||||
|
Text(stringResource(R.string.release_fetch_cancelling))
|
||||||
|
} else if (updateState == LauncherUpdater.LauncherUpdateState.Downloading) {
|
||||||
|
Text(stringResource(R.string.launcher_downloading_update))
|
||||||
|
} else {
|
||||||
|
Text(stringResource(R.string.launcher_self_update_progress))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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 = {
|
||||||
|
cancellingUpdate = true
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
updateJob?.cancelAndJoin()
|
||||||
|
|
||||||
beginUpdate = false
|
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,12 +365,21 @@ 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) {
|
||||||
|
cancellingUpdate = true
|
||||||
|
coroutineScope.launch {
|
||||||
|
updateJob?.cancelAndJoin()
|
||||||
|
updateJob = null
|
||||||
|
|
||||||
beginUpdate = false
|
beginUpdate = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ElevatedCard(modifier) {
|
ElevatedCard(modifier) {
|
||||||
Column(
|
Column(
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user