very basic self updater

This commit is contained in:
qimiko committed 2024-06-21 06:18:32 -07:00
1 parent 375f771e8a
commit 759ee4afd7
8 files changed
+107 -31

No files matched your search

+1
View File
@@ -20,6 +20,7 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-feature <uses-feature
android:name="android.hardware.wifi" android:name="android.hardware.wifi"
@@ -70,6 +70,7 @@ class AltMainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
clearDownloadedApks(this)
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager) val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
val geodeInstalled = LaunchUtils.isGeodeInstalled(this) val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
@@ -483,11 +484,9 @@ fun AltMainScreen(
) )
val nextLauncherUpdate by launchViewModel.nextLauncherUpdate.collectAsState() val nextLauncherUpdate by launchViewModel.nextLauncherUpdate.collectAsState()
val launcherDownloadUrl =
nextLauncherUpdate?.getLauncherDownload()?.browserDownloadUrl
// only show launcher update in a case where a user won't see it ingame // only show launcher update in a case where a user won't see it ingame
if (launcherDownloadUrl != null && launchUIState is LaunchViewModel.LaunchUIState.Cancelled) { if (nextLauncherUpdate != null && launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
StatusIndicator( StatusIndicator(
icon = { icon = {
Icon( Icon(
@@ -496,7 +495,7 @@ fun AltMainScreen(
) )
}, },
onClick = { onClick = {
downloadUrl(context, launcherDownloadUrl) installLauncherUpdate(context)
}, },
text = { text = {
Text( Text(
@@ -42,6 +42,7 @@ import com.geode.launcher.utils.GamePackageUtils
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
clearDownloadedApks(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -76,7 +76,7 @@ fun NotificationCardFromType(type: LaunchNotificationType) {
AnimatedNotificationCard( AnimatedNotificationCard(
displayLength = 5000L, displayLength = 5000L,
onClick = { onClick = {
downloadLauncherUpdate(context) installLauncherUpdate(context)
} }
) { ) {
LauncherUpdateContent() LauncherUpdateContent()
@@ -110,7 +110,6 @@ fun LaunchNotification() {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) { GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
// surface is not in use, so this is unfortunately not provided // surface is not in use, so this is unfortunately not provided
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
// using manual placements as column layouts seem to mess up the animation
Column( Column(
modifier = Modifier.displayCutoutPadding() modifier = Modifier.displayCutoutPadding()
) { ) {
@@ -124,7 +123,7 @@ fun LaunchNotification() {
} }
@Composable @Composable
fun AnimatedNotificationCard(visibilityDelay: Long = 0L, displayLength: Long = 3000L, onClick: (() -> Unit)? = null, modifier: Modifier = Modifier, contents: @Composable () -> Unit) { fun AnimatedNotificationCard(modifier: Modifier = Modifier, visibilityDelay: Long = 0L, displayLength: Long = 3000L, onClick: (() -> Unit)? = null, contents: @Composable () -> Unit) {
val state = remember { val state = remember {
MutableTransitionState(false).apply { MutableTransitionState(false).apply {
targetState = visibilityDelay <= 0 targetState = visibilityDelay <= 0
@@ -4,11 +4,11 @@ import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract
import android.text.format.Formatter import android.text.format.Formatter
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -29,39 +29,76 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.geode.launcher.R import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.updater.ReleaseManager import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.updater.ReleaseViewModel import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import java.io.File
import java.net.ConnectException import java.net.ConnectException
import java.net.UnknownHostException import java.net.UnknownHostException
fun clearDownloadedApks(context: Context) {
// technically we should be using the activity results but it was too inconsistent for my liking
val performCleanup = PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.CLEANUP_APKS)
if (!performCleanup) {
return
}
val baseDirectory = LaunchUtils.getBaseDirectory(context)
baseDirectory.listFiles {
// only select apk files
_, name -> name.lowercase().endsWith(".apk")
}?.forEach {
it.delete()
}
}
fun generateInstallIntent(uri: Uri): Intent {
// maybe one day i'll rewrite this to use packageinstaller. not today
return Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
fun installLauncherUpdate(context: Context) {
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
val launcherDownload = nextUpdate?.getLauncherDownload()
if (launcherDownload != null) {
val outputFile = launcherDownload.name
val baseDirectory = LaunchUtils.getBaseDirectory(context)
val outputPathFile = File(baseDirectory, outputFile)
if (!outputPathFile.exists()) {
return
}
val outputPath = "${UserDirectoryProvider.ROOT}${outputFile}"
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", outputPath)
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.CLEANUP_APKS, true)
fun downloadUrl(context: Context, url: String) {
try { try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val intent = generateInstallIntent(uri)
context.startActivity(intent) context.startActivity(intent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
} }
}
fun downloadLauncherUpdate(context: Context) {
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
val launcherUrl = nextUpdate?.getLauncherDownload()?.browserDownloadUrl
if (launcherUrl != null) {
downloadUrl(context, launcherUrl)
} else { } else {
Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show()
} }
} }
@Composable @Composable
fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String) { fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
val context = LocalContext.current val context = LocalContext.current
ElevatedCard(modifier) { ElevatedCard(modifier) {
@@ -81,10 +118,10 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String) {
) )
TextButton( TextButton(
onClick = { downloadUrl(context, openTo) }, onClick = { installLauncherUpdate(context) },
modifier = Modifier.align(Alignment.End) modifier = Modifier.align(Alignment.End)
) { ) {
Text(stringResource(R.string.launcher_download)) Text(stringResource(R.string.launcher_install))
} }
} }
} }
@@ -248,15 +285,10 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
} }
val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState() val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState()
val nextUpdateValue = nextUpdate
if (!releaseViewModel.isInUpdate && nextUpdateValue != null) {
val updateUrl = nextUpdateValue.getLauncherDownload()?.browserDownloadUrl
?: nextUpdateValue.htmlUrl
if (!releaseViewModel.isInUpdate && nextUpdate != null) {
LauncherUpdateIndicator( LauncherUpdateIndicator(
modifier = modifier, modifier = modifier
openTo = updateUrl
) )
} }
} }
@@ -131,6 +131,34 @@ class ReleaseManager private constructor(
} }
} }
private suspend fun downloadLauncherUpdate(release: Release) {
val download = release.getLauncherDownload() ?: return
val outputDirectory = LaunchUtils.getBaseDirectory(applicationContext)
val outputFile = File(outputDirectory, download.name)
if (outputFile.exists()) {
// only download the apk once
return
}
_uiState.value = ReleaseManagerState.InDownload(0, download.size.toLong())
try {
val fileStream = DownloadUtils.downloadStream(
httpClient,
download.browserDownloadUrl
) { progress, outOf ->
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
}
fileStream.copyTo(outputFile.outputStream())
} catch (e: Exception) {
sendError(e)
return
}
}
private suspend fun performUpdate(release: Release) { private suspend fun performUpdate(release: Release) {
val releaseAsset = release.getGeodeDownload() val releaseAsset = release.getGeodeDownload()
if (releaseAsset == null) { if (releaseAsset == null) {
@@ -170,6 +198,8 @@ class ReleaseManager private constructor(
return return
} }
downloadLauncherUpdateIfNecessary()
// extraction performed // extraction performed
updatePreferences(release) updatePreferences(release)
_uiState.value = ReleaseManagerState.Finished(true) _uiState.value = ReleaseManagerState.Finished(true)
@@ -200,6 +230,11 @@ class ReleaseManager private constructor(
} }
} }
private suspend fun downloadLauncherUpdateIfNecessary() {
val update = _availableLauncherUpdate.value ?: return
downloadLauncherUpdate(update)
}
private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) { private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) {
val release = try { val release = try {
getLatestRelease() getLatestRelease()
@@ -220,6 +255,8 @@ class ReleaseManager private constructor(
} }
if (release == null) { if (release == null) {
downloadLauncherUpdateIfNecessary()
_uiState.value = ReleaseManagerState.Finished() _uiState.value = ReleaseManagerState.Finished()
return return
} }
@@ -234,12 +271,16 @@ class ReleaseManager private constructor(
// check if an update is needed // check if an update is needed
if (latestVersion == currentVersion && geodeFile.exists()) { if (latestVersion == currentVersion && geodeFile.exists()) {
downloadLauncherUpdateIfNecessary()
_uiState.value = ReleaseManagerState.Finished() _uiState.value = ReleaseManagerState.Finished()
return return
} }
// check if the file was externally modified // check if the file was externally modified
if (!allowOverwriting && fileWasExternallyModified()) { if (!allowOverwriting && fileWasExternallyModified()) {
downloadLauncherUpdateIfNecessary()
sendError(UpdateException(UpdateException.Reason.EXTERNAL_FILE_IN_USE)) sendError(UpdateException(UpdateException.Reason.EXTERNAL_FILE_IN_USE))
return return
} }
@@ -139,7 +139,8 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
FORCE_HRR, FORCE_HRR,
ENABLE_REDESIGN, ENABLE_REDESIGN,
RELEASE_CHANNEL_TAG, RELEASE_CHANNEL_TAG,
DEVELOPER_MODE DEVELOPER_MODE,
CLEANUP_APKS
} }
private fun defaultValueForBooleanKey(key: Key): Boolean { private fun defaultValueForBooleanKey(key: Key): Boolean {
@@ -175,6 +176,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.ENABLE_REDESIGN -> "PreferenceEnableRedesign" Key.ENABLE_REDESIGN -> "PreferenceEnableRedesign"
Key.RELEASE_CHANNEL_TAG -> "PreferenceReleaseChannelTag" Key.RELEASE_CHANNEL_TAG -> "PreferenceReleaseChannelTag"
Key.DEVELOPER_MODE -> "PreferenceDeveloperMode" Key.DEVELOPER_MODE -> "PreferenceDeveloperMode"
Key.CLEANUP_APKS -> "PreferenceCleanupPackages"
} }
} }
+2 -1
View File
@@ -7,6 +7,7 @@
<string name="launcher_launch">Launch</string> <string name="launcher_launch">Launch</string>
<string name="launcher_settings">Settings</string> <string name="launcher_settings">Settings</string>
<string name="launcher_download">Download</string> <string name="launcher_download">Download</string>
<string name="launcher_install">Install</string>
<string name="launcher_title">Geode</string> <string name="launcher_title">Geode</string>
<string name="launcher_logo_alt">geode logo</string> <string name="launcher_logo_alt">geode logo</string>
<string name="launcher_launch_icon_alt">play icon</string> <string name="launcher_launch_icon_alt">play icon</string>
@@ -50,7 +51,7 @@
<string name="launcher_notification_settings">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_failed">Failed to check for updates.</string>
<string name="launcher_notification_update_success">Geode updated!</string> <string name="launcher_notification_update_success">Geode updated!</string>
<string name="launcher_notification_update_cta">Tap to download.</string> <string name="launcher_notification_update_cta">Tap to install.</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>