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…