forked from geode-sdk/android-launcher
very basic self updater
This commit is contained in:
8 files changed
+108
-32
No files matched your search
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
fun downloadUrl(context: Context, url: String) {
|
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||||
try {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
baseDirectory.listFiles {
|
||||||
context.startActivity(intent)
|
// only select apk files
|
||||||
} catch (e: ActivityNotFoundException) {
|
_, name -> name.lowercase().endsWith(".apk")
|
||||||
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
|
}?.forEach {
|
||||||
|
it.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadLauncherUpdate(context: Context) {
|
fun generateInstallIntent(uri: Uri): Intent {
|
||||||
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
|
// maybe one day i'll rewrite this to use packageinstaller. not today
|
||||||
val launcherUrl = nextUpdate?.getLauncherDownload()?.browserDownloadUrl
|
return Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
|
||||||
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (launcherUrl != null) {
|
fun installLauncherUpdate(context: Context) {
|
||||||
downloadUrl(context, launcherUrl)
|
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 {
|
} 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user