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
+108 -32

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_VIDEO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-feature
android:name="android.hardware.wifi"
@@ -70,6 +70,7 @@ class AltMainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
clearDownloadedApks(this)
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
@@ -483,11 +484,9 @@ fun AltMainScreen(
)
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
if (launcherDownloadUrl != null && launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
if (nextLauncherUpdate != null && launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
StatusIndicator(
icon = {
Icon(
@@ -496,7 +495,7 @@ fun AltMainScreen(
)
},
onClick = {
downloadUrl(context, launcherDownloadUrl)
installLauncherUpdate(context)
},
text = {
Text(
@@ -42,6 +42,7 @@ import com.geode.launcher.utils.GamePackageUtils
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
clearDownloadedApks(this)
super.onCreate(savedInstanceState)
@@ -76,7 +76,7 @@ fun NotificationCardFromType(type: LaunchNotificationType) {
AnimatedNotificationCard(
displayLength = 5000L,
onClick = {
downloadLauncherUpdate(context)
installLauncherUpdate(context)
}
) {
LauncherUpdateContent()
@@ -110,7 +110,6 @@ fun LaunchNotification() {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
// surface is not in use, so this is unfortunately not provided
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
// using manual placements as column layouts seem to mess up the animation
Column(
modifier = Modifier.displayCutoutPadding()
) {
@@ -124,7 +123,7 @@ fun LaunchNotification() {
}
@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 {
MutableTransitionState(false).apply {
targetState = visibilityDelay <= 0
@@ -4,11 +4,11 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
import android.text.format.Formatter
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
@@ -29,39 +29,76 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.updater.ReleaseManager
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.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
}
fun downloadUrl(context: Context, url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
val baseDirectory = LaunchUtils.getBaseDirectory(context)
baseDirectory.listFiles {
// only select apk files
_, name -> name.lowercase().endsWith(".apk")
}?.forEach {
it.delete()
}
}
fun downloadLauncherUpdate(context: Context) {
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
val launcherUrl = nextUpdate?.getLauncherDownload()?.browserDownloadUrl
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)
}
}
if (launcherUrl != null) {
downloadUrl(context, launcherUrl)
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)
try {
val intent = generateInstallIntent(uri)
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show()
}
}
@Composable
fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String) {
fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
val context = LocalContext.current
ElevatedCard(modifier) {
@@ -81,10 +118,10 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String) {
)
TextButton(
onClick = { downloadUrl(context, openTo) },
onClick = { installLauncherUpdate(context) },
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 nextUpdateValue = nextUpdate
if (!releaseViewModel.isInUpdate && nextUpdateValue != null) {
val updateUrl = nextUpdateValue.getLauncherDownload()?.browserDownloadUrl
?: nextUpdateValue.htmlUrl
if (!releaseViewModel.isInUpdate && nextUpdate != null) {
LauncherUpdateIndicator(
modifier = modifier,
openTo = updateUrl
modifier = modifier
)
}
}
@@ -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) {
val releaseAsset = release.getGeodeDownload()
if (releaseAsset == null) {
@@ -170,6 +198,8 @@ class ReleaseManager private constructor(
return
}
downloadLauncherUpdateIfNecessary()
// extraction performed
updatePreferences(release)
_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) {
val release = try {
getLatestRelease()
@@ -220,6 +255,8 @@ class ReleaseManager private constructor(
}
if (release == null) {
downloadLauncherUpdateIfNecessary()
_uiState.value = ReleaseManagerState.Finished()
return
}
@@ -234,12 +271,16 @@ class ReleaseManager private constructor(
// check if an update is needed
if (latestVersion == currentVersion && geodeFile.exists()) {
downloadLauncherUpdateIfNecessary()
_uiState.value = ReleaseManagerState.Finished()
return
}
// check if the file was externally modified
if (!allowOverwriting && fileWasExternallyModified()) {
downloadLauncherUpdateIfNecessary()
sendError(UpdateException(UpdateException.Reason.EXTERNAL_FILE_IN_USE))
return
}
@@ -139,7 +139,8 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
FORCE_HRR,
ENABLE_REDESIGN,
RELEASE_CHANNEL_TAG,
DEVELOPER_MODE
DEVELOPER_MODE,
CLEANUP_APKS
}
private fun defaultValueForBooleanKey(key: Key): Boolean {
@@ -175,6 +176,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.ENABLE_REDESIGN -> "PreferenceEnableRedesign"
Key.RELEASE_CHANNEL_TAG -> "PreferenceReleaseChannelTag"
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_settings">Settings</string>
<string name="launcher_download">Download</string>
<string name="launcher_install">Install</string>
<string name="launcher_title">Geode</string>
<string name="launcher_logo_alt">geode logo</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_update_failed">Failed to check for updates.</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_downloading_update">Downloading update…</string>