mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-22 08:42:38 +00:00
very basic self updater
This commit is contained in:
8 files changed
+108
-32
No files matched your search
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user