From 759ee4afd799d778ddc24edd6deed10ce220dd1a Mon Sep 17 00:00:00 2001
From: qimiko <25387744+qimiko@users.noreply.github.com>
Date: Fri, 21 Jun 2024 06:18:32 -0700
Subject: [PATCH] very basic self updater
---
app/src/main/AndroidManifest.xml | 1 +
.../com/geode/launcher/AltMainActivity.kt | 7 +-
.../java/com/geode/launcher/MainActivity.kt | 1 +
.../geode/launcher/main/LaunchNotification.kt | 5 +-
.../geode/launcher/main/UpdateComponents.kt | 76 +++++++++++++------
.../geode/launcher/updater/ReleaseManager.kt | 41 ++++++++++
.../geode/launcher/utils/PreferenceUtils.kt | 4 +-
app/src/main/res/values/strings.xml | 3 +-
8 files changed, 107 insertions(+), 31 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 122636e..8e0612a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,6 +20,7 @@
+
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
diff --git a/app/src/main/java/com/geode/launcher/main/UpdateComponents.kt b/app/src/main/java/com/geode/launcher/main/UpdateComponents.kt
index 9ac8112..b45a002 100644
--- a/app/src/main/java/com/geode/launcher/main/UpdateComponents.kt
+++ b/app/src/main/java/com/geode/launcher/main/UpdateComponents.kt
@@ -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
+ }
+
+ val baseDirectory = LaunchUtils.getBaseDirectory(context)
-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()
+ baseDirectory.listFiles {
+ // only select apk files
+ _, name -> name.lowercase().endsWith(".apk")
+ }?.forEach {
+ it.delete()
}
}
-fun downloadLauncherUpdate(context: Context) {
+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 launcherUrl = nextUpdate?.getLauncherDownload()?.browserDownloadUrl
+ val launcherDownload = nextUpdate?.getLauncherDownload()
+
+ if (launcherDownload != null) {
+ val outputFile = launcherDownload.name
+ val baseDirectory = LaunchUtils.getBaseDirectory(context)
- if (launcherUrl != null) {
- downloadUrl(context, launcherUrl)
+ 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
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt b/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt
index d2f31ae..486cdf9 100644
--- a/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt
+++ b/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt
@@ -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
}
diff --git a/app/src/main/java/com/geode/launcher/utils/PreferenceUtils.kt b/app/src/main/java/com/geode/launcher/utils/PreferenceUtils.kt
index e33e6c3..43bc36d 100644
--- a/app/src/main/java/com/geode/launcher/utils/PreferenceUtils.kt
+++ b/app/src/main/java/com/geode/launcher/utils/PreferenceUtils.kt
@@ -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"
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 63ea097..b6d78ba 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -7,6 +7,7 @@
Launch
Settings
Download
+ Install
Geode
geode logo
play icon
@@ -50,7 +51,7 @@
Launcher Settings
Failed to check for updates.
Geode updated!
- Tap to download.
+ Tap to install.
Starting Geometry Dash…
Downloading update…