From 269e85493b0cbd0956e55fe51f8ff25fb77c880a Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 31 Jul 2026 18:13:29 -0700 Subject: [PATCH 1/8] add gradle task to download geode binaries --- app/build.gradle.kts | 102 ++++++++++++++++++ .../geode/launcher/main/ErrorComponents.kt | 1 + .../launcher/preferences/SettingsActivity.kt | 8 +- .../com/geode/launcher/utils/LaunchUtils.kt | 3 + 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c5b171f3..d9fc32e2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,6 @@ +import java.net.URI +import java.util.zip.ZipInputStream + val composeBOM: String by rootProject.extra kotlin { @@ -12,6 +15,94 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") } +val currentGeodeVersion = "5.8.2" + +abstract class DownloadAndUnzipGeodeTask : DefaultTask() { + @get:Input + abstract val geodeVersion: Property + + @get:Input + abstract val platform: Property + + @get:OutputFile + abstract val targetFile: RegularFileProperty + + @TaskAction + fun execute() { + val downloadGeodeVersion = geodeVersion.get() + val downloadPlatform = platform.get() + val url = "https://github.com/geode-sdk/geode/releases/download/v$downloadGeodeVersion/geode-v$downloadGeodeVersion-$downloadPlatform.zip" + val destinationFile = targetFile.get().asFile + + val expectedFile = "Geode.$downloadPlatform.so" + + logger.info("Downloading Geode v$downloadGeodeVersion for platform $downloadPlatform") + + ZipInputStream(URI.create(url).toURL().openStream()).use { zip -> + var entry = zip.nextEntry + while (entry != null) { + if (entry.name == expectedFile) { + destinationFile.outputStream().use { + zip.copyTo(it) + } + return@execute + } + + entry = zip.nextEntry + } + + throw GradleException("Could not find Geode release.") + } + } +} + +val downloadGeode32Task = tasks.register("downloadGeode32Binary") { + description = "Downloads the 32bit Geode release archive" + inputs.property("geodeVersion", currentGeodeVersion) + + geodeVersion = currentGeodeVersion + platform = "android32" + targetFile = temporaryDir.resolve("libgeode.so") +} + +val downloadGeode64Task = tasks.register("downloadGeode64Binary") { + description = "Downloads the 64bit Geode release archive" + inputs.property("geodeVersion", currentGeodeVersion) + + geodeVersion = currentGeodeVersion + platform = "android64" + targetFile = temporaryDir.resolve("libgeode.so") +} + +abstract class MergeGeodeFilesTask : DefaultTask() { + @get:InputFile + abstract val binary32: RegularFileProperty + + @get:InputFile + abstract val binary64: RegularFileProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun execute() { + val out = outputDir.get().asFile + out.deleteRecursively() + out.mkdirs() + + binary32.get().asFile.copyTo(outputDir.file("armeabi-v7a/libgeode.so").get().asFile) + binary64.get().asFile.copyTo(outputDir.file("arm64-v8a/libgeode.so").get().asFile) + } +} + +val fullDownloadTask = tasks.register("downloadGeodeFull") { + description = "Downloads both Geode releases" + + binary32 = downloadGeode32Task.flatMap { it.targetFile } + binary64 = downloadGeode64Task.flatMap { it.targetFile } + outputDir = layout.buildDirectory.dir("geode-prebuilt") +} + android { compileSdk = 36 @@ -56,12 +147,14 @@ android { dimension = "distribution" isDefault = true buildConfigField("boolean", "GOOGLE_PLAY_BUILD", "false") + buildConfigField("String", "PREBUNDLED_GEODE", "null") } create("googlePlay") { dimension = "distribution" applicationId = "com.geode.launcher.play" buildConfigField("boolean", "GOOGLE_PLAY_BUILD", "true") + buildConfigField("String", "PREBUNDLED_GEODE", "\"$currentGeodeVersion\"") } } @@ -102,6 +195,15 @@ android { ndkVersion = "29.0.14206865" } +androidComponents { + onVariants(selector().withFlavor("distribution" to "googlePlay")) { variant -> + variant.sources.jniLibs?.addGeneratedSourceDirectory( + fullDownloadTask, + MergeGeodeFilesTask::outputDir + ) + } +} + dependencies { implementation (platform("androidx.compose:compose-bom:$composeBOM")) implementation ("androidx.core:core-ktx:1.18.0") diff --git a/app/src/main/java/com/geode/launcher/main/ErrorComponents.kt b/app/src/main/java/com/geode/launcher/main/ErrorComponents.kt index 2572fbac..d6a0ed77 100644 --- a/app/src/main/java/com/geode/launcher/main/ErrorComponents.kt +++ b/app/src/main/java/com/geode/launcher/main/ErrorComponents.kt @@ -203,6 +203,7 @@ fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) { val context = LocalContext.current val currentRelease = remember { PreferenceUtils.get(context).getString(PreferenceUtils.Key.CURRENT_VERSION_TAG) + ?: BuildConfig.PREBUNDLED_GEODE } val gameVersion = remember { diff --git a/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt b/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt index c9f59d4d..824fe896 100644 --- a/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt +++ b/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt @@ -665,7 +665,13 @@ fun AboutSettingsGroup(developerModeEnabled: Boolean) { stringResource(R.string.preference_loader_version_name), stringResource( R.string.preference_loader_version_description, - currentRelease ?: "unknown" + currentRelease + ?: "${BuildConfig.PREBUNDLED_GEODE}*" + .takeIf { + @Suppress("SENSELESS_COMPARISON") + BuildConfig.PREBUNDLED_GEODE != null + } + ?: "unknown" ), displayInline = true ) diff --git a/app/src/main/java/com/geode/launcher/utils/LaunchUtils.kt b/app/src/main/java/com/geode/launcher/utils/LaunchUtils.kt index 3671fff3..41fbcfa9 100644 --- a/app/src/main/java/com/geode/launcher/utils/LaunchUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/LaunchUtils.kt @@ -42,6 +42,9 @@ object LaunchUtils { } fun isGeodeInstalled(context: Context): Boolean { + @Suppress("SENSELESS_COMPARISON") + if (BuildConfig.PREBUNDLED_GEODE != null) return true + return getInstalledGeodePath(context) != null } From ce25afc111ea19e2740fa86d23184be791085cca Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 31 Jul 2026 18:17:32 -0700 Subject: [PATCH 2/8] disable automatic update on play build --- .../geode/launcher/main/LaunchViewModel.kt | 27 +++++++----- .../preferences/DeveloperSettingsActivity.kt | 43 ++++++++++--------- .../launcher/preferences/SettingsActivity.kt | 5 ++- .../geode/launcher/updater/ReleaseManager.kt | 9 ++++ .../launcher/updater/ReleaseViewModel.kt | 5 +++ 5 files changed, 58 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/geode/launcher/main/LaunchViewModel.kt b/app/src/main/java/com/geode/launcher/main/LaunchViewModel.kt index 4b43531b..6f26ea73 100644 --- a/app/src/main/java/com/geode/launcher/main/LaunchViewModel.kt +++ b/app/src/main/java/com/geode/launcher/main/LaunchViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory +import com.geode.launcher.BuildConfig import com.geode.launcher.updater.ReleaseManager import com.geode.launcher.utils.Constants import com.geode.launcher.utils.GamePackageUtils @@ -248,25 +249,31 @@ class LaunchViewModel(private val application: Application): ViewModel() { initReadyTimer() } - val releaseManager = ReleaseManager.get(application) + if (!BuildConfig.GOOGLE_PLAY_BUILD) { + val releaseManager = ReleaseManager.get(application) - val hasGeode = LaunchUtils.isGeodeInstalled(application) - val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY) + val hasGeode = LaunchUtils.isGeodeInstalled(application) + val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY) - if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) { - releaseManager.checkForUpdates(false) - } else { - releaseManager.afterUseCachedUpdate() - } + if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) { + releaseManager.checkForUpdates(false) + } else { + releaseManager.afterUseCachedUpdate() + } - if (isRestart) { - releaseManager.dismissedLauncherUpdate = true + if (isRestart) { + releaseManager.dismissedLauncherUpdate = true + } } preReadyCheck() } fun retryUpdate() { + if (BuildConfig.GOOGLE_PLAY_BUILD) { + return + } + ReleaseManager.get(application).checkForUpdates(true) if (ReleaseManager.get(application).isInUpdate) { restoreUpdateState = _uiState.value diff --git a/app/src/main/java/com/geode/launcher/preferences/DeveloperSettingsActivity.kt b/app/src/main/java/com/geode/launcher/preferences/DeveloperSettingsActivity.kt index 65fb2cb7..b10130c9 100644 --- a/app/src/main/java/com/geode/launcher/preferences/DeveloperSettingsActivity.kt +++ b/app/src/main/java/com/geode/launcher/preferences/DeveloperSettingsActivity.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.geode.launcher.BuildConfig import com.geode.launcher.R import com.geode.launcher.preferences.components.OptionsButton import com.geode.launcher.preferences.components.OptionsGroup @@ -204,28 +205,30 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) { ) } - OptionsGroup(title = stringResource(R.string.preference_category_updater)) { - val context = LocalContext.current + if (!BuildConfig.GOOGLE_PLAY_BUILD) { + OptionsGroup(title = stringResource(R.string.preference_category_updater)) { + val context = LocalContext.current - SettingsSelectCard( - title = stringResource(R.string.preference_release_channel_tag_name), - dialogTitle = stringResource(R.string.preference_release_channel_select), - preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL_TAG, - options = linkedMapOf( - 0 to stringResource(R.string.preference_release_channel_stable), - 1 to stringResource(R.string.preference_release_channel_beta), - 2 to stringResource(R.string.preference_release_channel_nightly), - ), - extraSelectBehavior = { - PreferenceUtils.get(context) - .setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, 0L) - } - ) + SettingsSelectCard( + title = stringResource(R.string.preference_release_channel_tag_name), + dialogTitle = stringResource(R.string.preference_release_channel_select), + preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL_TAG, + options = linkedMapOf( + 0 to stringResource(R.string.preference_release_channel_stable), + 1 to stringResource(R.string.preference_release_channel_beta), + 2 to stringResource(R.string.preference_release_channel_nightly), + ), + extraSelectBehavior = { + PreferenceUtils.get(context) + .setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, 0L) + } + ) - SettingsCard( - title = stringResource(R.string.preference_disable_update_cache), - preferenceKey = PreferenceUtils.Key.DISABLE_UPDATE_CACHE - ) + SettingsCard( + title = stringResource(R.string.preference_disable_update_cache), + preferenceKey = PreferenceUtils.Key.DISABLE_UPDATE_CACHE + ) + } } OptionsGroup(title = stringResource(R.string.preference_category_testing)) { diff --git a/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt b/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt index 824fe896..1d8fc463 100644 --- a/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt +++ b/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt @@ -70,7 +70,6 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.geode.launcher.BuildConfig -import com.geode.launcher.MainActivity import com.geode.launcher.R import com.geode.launcher.UserDirectoryProvider import com.geode.launcher.preferences.components.BackupButton @@ -510,6 +509,10 @@ fun GameplaySettingsGroup() { @Composable fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) { + if (BuildConfig.GOOGLE_PLAY_BUILD) { + return + } + val updateStatus by releaseViewModel.uiState.collectAsState() OptionsGroup(stringResource(R.string.preference_category_updater)) { 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 572959ad..63f2fec2 100644 --- a/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt +++ b/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt @@ -388,6 +388,10 @@ class ReleaseManager private constructor( } fun shouldUseCache(): Boolean { + if (BuildConfig.GOOGLE_PLAY_BUILD) { + return true + } + val sharedPreferences = PreferenceUtils.get(applicationContext) if (sharedPreferences.getBoolean(PreferenceUtils.Key.DISABLE_UPDATE_CACHE)) { return false @@ -406,6 +410,11 @@ class ReleaseManager private constructor( } private suspend fun checkForNewRelease(isManual: Boolean = false) { + if (BuildConfig.GOOGLE_PLAY_BUILD) { + _uiState.value = ReleaseManagerState.Finished() + return + } + val sharedPreferences = PreferenceUtils.get(applicationContext) if (!isManual && shouldUseCache()) { diff --git a/app/src/main/java/com/geode/launcher/updater/ReleaseViewModel.kt b/app/src/main/java/com/geode/launcher/updater/ReleaseViewModel.kt index 450d0b88..2a3b463b 100644 --- a/app/src/main/java/com/geode/launcher/updater/ReleaseViewModel.kt +++ b/app/src/main/java/com/geode/launcher/updater/ReleaseViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.AP import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory +import com.geode.launcher.BuildConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -89,6 +90,10 @@ class ReleaseViewModel(private val application: Application): ViewModel() { } fun runReleaseCheck(isManual: Boolean = false) { + if (BuildConfig.GOOGLE_PLAY_BUILD) { + return + } + hasPerformedCheck = true viewModelScope.launch(Dispatchers.IO) { From 34e2cb913eb79801dc11b78dc45538af290938ff Mon Sep 17 00:00:00 2001 From: Chloe Date: Sun, 6 Sep 2026 03:32:47 -0700 Subject: [PATCH 3/8] get download url from release api --- app/build.gradle.kts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d9fc32e2..db346ee1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,4 @@ +import groovy.json.JsonSlurper import java.net.URI import java.util.zip.ZipInputStream @@ -15,7 +16,7 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") } -val currentGeodeVersion = "5.8.2" +val currentGeodeVersion = "5.10.1" abstract class DownloadAndUnzipGeodeTask : DefaultTask() { @get:Input @@ -27,16 +28,33 @@ abstract class DownloadAndUnzipGeodeTask : DefaultTask() { @get:OutputFile abstract val targetFile: RegularFileProperty + private fun getGeodeDownload(): String? { + val downloadGeodeVersion = geodeVersion.get() + + val downloadPlatform = platform.get() + + val releaseUrl = "https://api.geode-sdk.org/v1/loader/versions/$downloadGeodeVersion" + URI.create(releaseUrl).toURL().openStream().use { response -> + val json = JsonSlurper().parse(response) as Map<*, *> + + val downloads = json["downloads"] as? Map<*, *> + val platformInfo = downloads?.get(downloadPlatform) as? Map<*, *> + return platformInfo?.get("url") as? String + } + } + @TaskAction fun execute() { + val releaseUrl = getGeodeDownload() + val downloadGeodeVersion = geodeVersion.get() val downloadPlatform = platform.get() - val url = "https://github.com/geode-sdk/geode/releases/download/v$downloadGeodeVersion/geode-v$downloadGeodeVersion-$downloadPlatform.zip" + val url = releaseUrl ?: "https://github.com/geode-sdk/geode/releases/download/v$downloadGeodeVersion/geode-v$downloadGeodeVersion-$downloadPlatform.zip" val destinationFile = targetFile.get().asFile val expectedFile = "Geode.$downloadPlatform.so" - logger.info("Downloading Geode v$downloadGeodeVersion for platform $downloadPlatform") + logger.info("Downloading Geode v$downloadGeodeVersion for platform $downloadPlatform from $releaseUrl") ZipInputStream(URI.create(url).toURL().openStream()).use { zip -> var entry = zip.nextEntry From 9dd8f54407a3436761c84fc55a13985e440d8458 Mon Sep 17 00:00:00 2001 From: Chloe Date: Sun, 6 Sep 2026 04:59:38 -0700 Subject: [PATCH 4/8] fix incorrect priorities between external library and prebundled --- .../geode/launcher/GeometryDashActivity.kt | 89 +++++++++++-------- .../java/com/geode/launcher/MainActivity.kt | 10 +++ .../geode/launcher/main/ErrorComponents.kt | 7 +- .../launcher/preferences/SettingsActivity.kt | 11 +-- .../geode/launcher/utils/PreferenceUtils.kt | 2 + 5 files changed, 73 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt index 15b507c4..4e72cee7 100644 --- a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt +++ b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt @@ -379,54 +379,69 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL return "$sourceDir!/lib/$architecture/" } - @SuppressLint("UnsafeDynamicallyLoadedCode") private fun loadGeodeLibrary() { - // Load Geode if exists - // bundling the object with the application allows for nicer backtraces - try { - // put libgeode.so in jniLibs/armeabi-v7a to get this - System.loadLibrary("geode") - return - } catch (e: UnsatisfiedLinkError) { - // but users may prefer it stored with data - val geodeFilename = LaunchUtils.geodeFilename - val geodePath = File(filesDir.path, "launcher/$geodeFilename") - if (geodePath.exists()) { - System.load(geodePath.path) + @Suppress("SENSELESS_COMPARISON") + if (BuildConfig.PREBUNDLED_GEODE != null) { + // attempt to load an externally provided Geode first + if (!loadExternalGeode()) { + System.loadLibrary("geode") + } + } else { + // Load Geode if exists + // bundling the object with the application allows for nicer backtraces + try { + // put libgeode.so in jniLibs/armeabi-v7a to get this + System.loadLibrary("geode") return + } catch (e: UnsatisfiedLinkError) { + if (!loadExternalGeode()) { + throw e + } } + } + } - // you know zmx i have 0 clue what this does so im - // just gonna like copy the binary from external - // also i get 20 million permission denied errors - val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!! + @SuppressLint("UnsafeDynamicallyLoadedCode") + private fun loadExternalGeode(): Boolean { + // but users may prefer it stored with data + val geodeFilename = LaunchUtils.geodeFilename + val geodePath = File(filesDir.path, "launcher/$geodeFilename") + if (geodePath.exists()) { + System.load(geodePath.path) + return true + } - val copiedPath = File(filesDir.path, "copied") - if (copiedPath.exists()) { - copiedPath.deleteRecursively() - } - copiedPath.mkdir() + // you know zmx i have 0 clue what this does so im + // just gonna like copy the binary from external + // also i get 20 million permission denied errors + val externalGeodePath = LaunchUtils.getInstalledGeodePath(this) + ?: return false - val copiedGeodePath = File(copiedPath.path, "Geode.so") + val copiedPath = File(filesDir.path, "copied") + if (copiedPath.exists()) { + copiedPath.deleteRecursively() + } + copiedPath.mkdir() - if (externalGeodePath.exists()) { - DownloadUtils.copyFile( - FileInputStream(externalGeodePath), - FileOutputStream(copiedGeodePath) - ) + val copiedGeodePath = File(copiedPath.path, "Geode.so") - if (copiedGeodePath.exists()) { - // android 17 fix - copiedGeodePath.setWritable(false) + if (externalGeodePath.exists()) { + DownloadUtils.copyFile( + FileInputStream(externalGeodePath), + FileOutputStream(copiedGeodePath) + ) - println("Loading Geode from ${externalGeodePath.name}") - System.load(copiedGeodePath.path) - return - } - } + if (copiedGeodePath.exists()) { + // android 17 fix + copiedGeodePath.setWritable(false) - throw e + println("Loading Geode from ${externalGeodePath.name}") + System.load(copiedGeodePath.path) + return true + } } + + return false } private fun createView(): FrameLayout { diff --git a/app/src/main/java/com/geode/launcher/MainActivity.kt b/app/src/main/java/com/geode/launcher/MainActivity.kt index 536e4c44..cc5759cf 100644 --- a/app/src/main/java/com/geode/launcher/MainActivity.kt +++ b/app/src/main/java/com/geode/launcher/MainActivity.kt @@ -111,6 +111,16 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() clearDownloadedApks(this) + // remove old Geode for the like 4 people that downloaded the Play Store build + @Suppress("SENSELESS_COMPARISON") + if (BuildConfig.PREBUNDLED_GEODE != null) { + val preferences = PreferenceUtils.get(this) + if (!preferences.getBoolean(PreferenceUtils.Key.PREBUNDLED_GEODE_MIGRATION_PERFORMED)) { + LaunchUtils.getInstalledGeodePath(this)?.delete() + preferences.setBoolean(PreferenceUtils.Key.PREBUNDLED_GEODE_MIGRATION_PERFORMED, true) + } + } + val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE) val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE) diff --git a/app/src/main/java/com/geode/launcher/main/ErrorComponents.kt b/app/src/main/java/com/geode/launcher/main/ErrorComponents.kt index d6a0ed77..19d899a3 100644 --- a/app/src/main/java/com/geode/launcher/main/ErrorComponents.kt +++ b/app/src/main/java/com/geode/launcher/main/ErrorComponents.kt @@ -202,8 +202,11 @@ fun onShowLogs(context: Context) { fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) { val context = LocalContext.current val currentRelease = remember { - PreferenceUtils.get(context).getString(PreferenceUtils.Key.CURRENT_VERSION_TAG) - ?: BuildConfig.PREBUNDLED_GEODE + @Suppress("SENSELESS_COMPARISON") + if (BuildConfig.PREBUNDLED_GEODE != null) + "${BuildConfig.PREBUNDLED_GEODE}*" + else + PreferenceUtils.get(context).getString(PreferenceUtils.Key.CURRENT_VERSION_TAG) } val gameVersion = remember { diff --git a/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt b/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt index 1d8fc463..f1cde78b 100644 --- a/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt +++ b/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt @@ -664,17 +664,14 @@ fun AboutSettingsGroup(developerModeEnabled: Boolean) { ) } + @Suppress("SENSELESS_COMPARISON") OptionsLabel( stringResource(R.string.preference_loader_version_name), stringResource( R.string.preference_loader_version_description, - currentRelease - ?: "${BuildConfig.PREBUNDLED_GEODE}*" - .takeIf { - @Suppress("SENSELESS_COMPARISON") - BuildConfig.PREBUNDLED_GEODE != null - } - ?: "unknown" + if (BuildConfig.PREBUNDLED_GEODE != null) + "${BuildConfig.PREBUNDLED_GEODE}*" + else (currentRelease ?: "unknown") ), displayInline = true ) 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 20f94b4c..b8b34f61 100644 --- a/app/src/main/java/com/geode/launcher/utils/PreferenceUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/PreferenceUtils.kt @@ -149,6 +149,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) { LAST_UPDATE_CHECK_TIME, LAST_LAUNCHER_UPDATE, DISABLE_UPDATE_CACHE, + PREBUNDLED_GEODE_MIGRATION_PERFORMED, } private fun defaultValueForBooleanKey(key: Key): Boolean { @@ -199,6 +200,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) { Key.LAST_UPDATE_CHECK_TIME -> "PreferenceLastUpdateCheckTime" Key.LAST_LAUNCHER_UPDATE -> "PreferenceLastLauncherUpdate" Key.DISABLE_UPDATE_CACHE -> "PreferenceDisableUpdateCache" + Key.PREBUNDLED_GEODE_MIGRATION_PERFORMED -> "PreferencePrebundledGeodeMigrationPerformed" // Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel" // Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate" // Key.FORCE_HRR -> "PreferenceForceHighRefreshRate" From ef5889a8d0cb1c4d6039eac925d92b309c8417a3 Mon Sep 17 00:00:00 2001 From: Chloe Date: Mon, 7 Sep 2026 04:08:20 -0700 Subject: [PATCH 5/8] bundle geode resources this also fixes the release api fetch as i never tested it until now, oops --- app/build.gradle.kts | 161 ++++++++++++++++-- .../geode/launcher/GeometryDashActivity.kt | 47 +++++ 2 files changed, 191 insertions(+), 17 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ac4ef76d..e2a2aa42 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,7 @@ import groovy.json.JsonSlurper +import java.io.IOException import java.net.URI +import java.security.MessageDigest import java.util.zip.ZipInputStream kotlin { @@ -16,45 +18,98 @@ plugins { val currentGeodeVersion = "5.10.1" -abstract class DownloadAndUnzipGeodeTask : DefaultTask() { +abstract class DownloadVersionMetadataTask : DefaultTask() { @get:Input abstract val geodeVersion: Property - @get:Input - abstract val platform: Property - @get:OutputFile abstract val targetFile: RegularFileProperty - private fun getGeodeDownload(): String? { + @TaskAction + fun execute() { val downloadGeodeVersion = geodeVersion.get() - - val downloadPlatform = platform.get() + val destinationFile = targetFile.get().asFile val releaseUrl = "https://api.geode-sdk.org/v1/loader/versions/$downloadGeodeVersion" URI.create(releaseUrl).toURL().openStream().use { response -> - val json = JsonSlurper().parse(response) as Map<*, *> + destinationFile.outputStream().use { destination -> + response.copyTo(destination) + } + } + } +} + +abstract class DownloadGeodeFileTask : DefaultTask() { + @get:Input + abstract val platform: Property + + @get:InputFile + abstract val metadata: RegularFileProperty + + private fun downloadAndVerifyFile(filename: String, url: String, hash: String?): File { + logger.info("Downloading $filename from $url") + + val tempFile = File(temporaryDir, filename) + + URI.create(url).toURL().openStream().use { response -> + tempFile.outputStream().use { output -> + response.copyTo(output) + } + } + + if (hash != null) { + val downloadedHash = MessageDigest.getInstance("SHA-256") + .digest(tempFile.readBytes()) + .toHexString() - val downloads = json["downloads"] as? Map<*, *> - val platformInfo = downloads?.get(downloadPlatform) as? Map<*, *> - return platformInfo?.get("url") as? String + if (downloadedHash != hash) { + throw GradleException("Hash check failed for file $filename, expected $hash but found $downloadedHash.") + } } + + return tempFile } + fun downloadZip(fallbackUrl: String?): File { + val downloadPlatform = platform.get() + + val metadataFile = metadata.get().asFile + val json = JsonSlurper().parse(metadataFile) as Map<*, *> + + val payload = json["payload"] as? Map<*, *> + + val downloads = payload?.get("downloads") as? Map<*, *> + val platformInfo = downloads?.get(downloadPlatform) as? Map<*, *> + + val downloadUrl = platformInfo?.get("url") as? String ?: fallbackUrl + if (downloadUrl == null) { + throw GradleException("Failed to fetch download url for platform $platform.") + } + + val expectedHash = platformInfo?.get("hash") as? String + + return downloadAndVerifyFile("$downloadPlatform.zip", downloadUrl, expectedHash) + } +} + +abstract class DownloadAndUnzipGeodeTask : DownloadGeodeFileTask() { + @get:Input + abstract val geodeVersion: Property + + @get:OutputFile + abstract val targetFile: RegularFileProperty + @TaskAction fun execute() { - val releaseUrl = getGeodeDownload() - val downloadGeodeVersion = geodeVersion.get() val downloadPlatform = platform.get() - val url = releaseUrl ?: "https://github.com/geode-sdk/geode/releases/download/v$downloadGeodeVersion/geode-v$downloadGeodeVersion-$downloadPlatform.zip" val destinationFile = targetFile.get().asFile - val expectedFile = "Geode.$downloadPlatform.so" + val tempOutput = this.downloadZip("https://github.com/geode-sdk/geode/releases/download/v$downloadGeodeVersion/geode-v$downloadGeodeVersion-$downloadPlatform.zip") - logger.info("Downloading Geode v$downloadGeodeVersion for platform $downloadPlatform from $releaseUrl") + val expectedFile = "Geode.$downloadPlatform.so" - ZipInputStream(URI.create(url).toURL().openStream()).use { zip -> + ZipInputStream(tempOutput.inputStream()).use { zip -> var entry = zip.nextEntry while (entry != null) { if (entry.name == expectedFile) { @@ -72,12 +127,70 @@ abstract class DownloadAndUnzipGeodeTask : DefaultTask() { } } +abstract class DownloadAndUnzipGeodeResourcesTask : DownloadGeodeFileTask() { + init { + platform.set("resources") + } + + @get:Input + abstract val geodeVersion: Property + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun execute() { + val downloadGeodeVersion = geodeVersion.get() + + val tempOutput = this.downloadZip("https://github.com/geode-sdk/geode/releases/download/v$downloadGeodeVersion/resources.zip") + + val baseOutput = outputDir.get().asFile + if (baseOutput.exists()) baseOutput.deleteRecursively() + + val output = File(baseOutput, "geode.loader") + output.mkdirs() + + val canonicalOutput = output.canonicalPath + + ZipInputStream(tempOutput.inputStream()).use { zipStream -> + var entry = zipStream.nextEntry + while (entry != null) { + val destination = File(output, entry.name) + if (!destination.canonicalPath.startsWith(canonicalOutput)) { + throw GradleException("attempted copy to outside of output directory: ${entry.name}") + } + + if (destination.isDirectory) { + destination.mkdirs() + } else { + destination.parentFile?.mkdirs() + + destination.outputStream().use { destinationStream -> + zipStream.copyTo(destinationStream) + } + } + + entry = zipStream.nextEntry + } + } + } +} + +val fetchGeodeMetadataTask = tasks.register("fetchGeodeMetadata") { + description = "Fetches and saves a Geode version's metadata" + inputs.property("geodeVersion", currentGeodeVersion) + + geodeVersion = currentGeodeVersion + targetFile = temporaryDir.resolve("metadata.json") +} + val downloadGeode32Task = tasks.register("downloadGeode32Binary") { description = "Downloads the 32bit Geode release archive" inputs.property("geodeVersion", currentGeodeVersion) geodeVersion = currentGeodeVersion platform = "android32" + metadata = fetchGeodeMetadataTask.flatMap { it.targetFile } targetFile = temporaryDir.resolve("libgeode.so") } @@ -87,9 +200,19 @@ val downloadGeode64Task = tasks.register("downloadGeo geodeVersion = currentGeodeVersion platform = "android64" + metadata = fetchGeodeMetadataTask.flatMap { it.targetFile } targetFile = temporaryDir.resolve("libgeode.so") } +val downloadGeodeResourcesTask = tasks.register("downloadGeodeResources") { + description = "Downloads Geode resources" + inputs.property("geodeVersion", currentGeodeVersion) + + geodeVersion = currentGeodeVersion + metadata = fetchGeodeMetadataTask.flatMap { it.targetFile } + outputDir = layout.buildDirectory.dir("geode-resources") +} + abstract class MergeGeodeFilesTask : DefaultTask() { @get:InputFile abstract val binary32: RegularFileProperty @@ -217,6 +340,10 @@ androidComponents { fullDownloadTask, MergeGeodeFilesTask::outputDir ) + variant.sources.assets?.addGeneratedSourceDirectory( + downloadGeodeResourcesTask, + DownloadAndUnzipGeodeResourcesTask::outputDir + ) } } diff --git a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt index 4e72cee7..6376326e 100644 --- a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt +++ b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt @@ -384,6 +384,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL if (BuildConfig.PREBUNDLED_GEODE != null) { // attempt to load an externally provided Geode first if (!loadExternalGeode()) { + copyGeodeResources() System.loadLibrary("geode") } } else { @@ -681,6 +682,52 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL } } + private fun copyGeodeResources() { + @Suppress("SENSELESS_COMPARISON") + if (BuildConfig.PREBUNDLED_GEODE == null) { + return + } + + val resourcesDirectory = LaunchUtils.getGeodeResourcesDirectory(this) + resourcesDirectory.mkdirs() + + val pathBase = "geode.loader" + val versionFilename = "version" + + val bundledVersion = try { + assets.open("$pathBase/$versionFilename").bufferedReader().use { + it.readText() + }.trimEnd() + } catch (_: IOException) { + return + } + + val versionFile = File(resourcesDirectory, versionFilename) + val resourcesVersion = if (versionFile.exists()) { + versionFile.readText().trimEnd() + } else null + + if (bundledVersion == resourcesVersion) { + println("copyGeodeResources: found $resourcesVersion, expected $bundledVersion. No work needs to be done!") + return + } + + println("copyGeodeResources: found $resourcesVersion, expected $bundledVersion. Copying assets") + + val fileListing = try { + assets.list(pathBase) + } catch (_: IOException) { + return + } + + fileListing?.forEach { fileName -> + val resourceOutput = File(resourcesDirectory, fileName) + + val resource = assets.open("$pathBase/$fileName") + DownloadUtils.copyFile(resource, resourceOutput.outputStream()) + } + } + class EGLConfigChooser : GLSurfaceView.EGLConfigChooser { // this comes from EGL14, but is unavailable on EGL10 // also EGL14 is incompatible with EGL10. so whatever From 0437ac0df378d3fc2489a1530ae47b75cbb24de3 Mon Sep 17 00:00:00 2001 From: Chloe Date: Mon, 7 Sep 2026 04:15:16 -0700 Subject: [PATCH 6/8] add message for loading from files dir --- app/src/main/java/com/geode/launcher/GeometryDashActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt index 6376326e..d64bb710 100644 --- a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt +++ b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt @@ -408,6 +408,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL val geodeFilename = LaunchUtils.geodeFilename val geodePath = File(filesDir.path, "launcher/$geodeFilename") if (geodePath.exists()) { + println("Loading Geode from ${geodePath.path}") System.load(geodePath.path) return true } From bba7e7d80a6e2b0e61fe29e8d4c13d5befc4fe2c Mon Sep 17 00:00:00 2001 From: Chloe Date: Mon, 7 Sep 2026 09:35:52 -0700 Subject: [PATCH 7/8] raise minimum gd version with bundled geode --- app/src/main/java/com/geode/launcher/MainActivity.kt | 7 ++++--- .../main/java/com/geode/launcher/main/LaunchComponents.kt | 4 +++- .../main/java/com/geode/launcher/main/LaunchViewModel.kt | 4 +++- .../main/java/com/geode/launcher/utils/GamePackageUtils.kt | 4 ++++ app/src/main/res/values/strings.xml | 4 ++-- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/geode/launcher/MainActivity.kt b/app/src/main/java/com/geode/launcher/MainActivity.kt index 4d5857f4..f0dfaa31 100644 --- a/app/src/main/java/com/geode/launcher/MainActivity.kt +++ b/app/src/main/java/com/geode/launcher/MainActivity.kt @@ -217,7 +217,7 @@ fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): Lau ) LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo( title = stringResource(R.string.launcher_cancelled_error), - details = stringResource(R.string.launcher_cancelled_outdated) + details = stringResource(R.string.launcher_cancelled_outdated, GamePackageUtils.getSupportedUnifiedVersion()) ) LaunchViewModel.LaunchCancelReason.AUTOMATIC, LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo( @@ -838,10 +838,11 @@ fun AltMainScreen( ) // only show launcher update in a case where a user won't see it ingame - if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) { + val currentUIState = launchUIState + if (currentUIState is LaunchViewModel.LaunchUIState.Cancelled) { val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) } - if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) { + if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING && currentUIState.reason != LaunchViewModel.LaunchCancelReason.GAME_OUTDATED) { UnsupportedVersionWarning() } diff --git a/app/src/main/java/com/geode/launcher/main/LaunchComponents.kt b/app/src/main/java/com/geode/launcher/main/LaunchComponents.kt index c2b60784..97e90137 100644 --- a/app/src/main/java/com/geode/launcher/main/LaunchComponents.kt +++ b/app/src/main/java/com/geode/launcher/main/LaunchComponents.kt @@ -187,6 +187,8 @@ fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, modifier: Modi @Composable fun UnsupportedVersionWarning(modifier: Modifier = Modifier) { + val gameVersion = GamePackageUtils.getSupportedUnifiedVersion() + InlineDialog( headline = { Icon( @@ -202,7 +204,7 @@ fun UnsupportedVersionWarning(modifier: Modifier = Modifier) { }, body = { Text( - stringResource(R.string.launcher_game_update_required), + stringResource(R.string.launcher_game_update_required, gameVersion), ) }, modifier = modifier.padding(8.dp) diff --git a/app/src/main/java/com/geode/launcher/main/LaunchViewModel.kt b/app/src/main/java/com/geode/launcher/main/LaunchViewModel.kt index 6f26ea73..8afdfd78 100644 --- a/app/src/main/java/com/geode/launcher/main/LaunchViewModel.kt +++ b/app/src/main/java/com/geode/launcher/main/LaunchViewModel.kt @@ -183,7 +183,9 @@ class LaunchViewModel(private val application: Application): ViewModel() { return } - if (GamePackageUtils.getGameVersionCode(packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN) { + // play store build only comes with the one build of geode + val minVersionCode = if (BuildConfig.GOOGLE_PLAY_BUILD) Constants.SUPPORTED_VERSION_CODE else Constants.SUPPORTED_VERSION_CODE_MIN + if (GamePackageUtils.getGameVersionCode(packageManager) < minVersionCode) { _uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED)) return } diff --git a/app/src/main/java/com/geode/launcher/utils/GamePackageUtils.kt b/app/src/main/java/com/geode/launcher/utils/GamePackageUtils.kt index a69929f3..eb6ca8ff 100644 --- a/app/src/main/java/com/geode/launcher/utils/GamePackageUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/GamePackageUtils.kt @@ -63,6 +63,10 @@ object GamePackageUtils { return gameVersionMap[versionCode] ?: getGameVersionString(packageManager) } + fun getSupportedUnifiedVersion(): String { + return gameVersionMap[Constants.SUPPORTED_VERSION_CODE] ?: Constants.SUPPORTED_VERSION_STRING + } + fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean { val abi = LaunchUtils.applicationArchitecture val isExtracted = diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ac3ea6e..7f9b4589 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,7 +35,7 @@ A Geometry Dash update is required. - The currently installed version of Geometry Dash is no longer supported. Please update Geometry Dash to continue receiving Geode updates. + The currently installed version of Geometry Dash is no longer supported. Please update Geometry Dash to version %1$s to continue receiving Geode updates. A new launcher update is available! Dismiss @@ -82,7 +82,7 @@ Launch paused due to a crash. Geode crashed. Geode cannot be used. - A Geometry Dash update is required. + Geometry Dash version %1$s is required. Restart Resume Retry From a93d6a05f6cadbde7f40e05bf53d9d9bfb399748 Mon Sep 17 00:00:00 2001 From: Chloe Date: Mon, 7 Sep 2026 14:11:21 -0700 Subject: [PATCH 8/8] clear old resources prior to copying --- .../main/java/com/geode/launcher/GeometryDashActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt index d64bb710..8c13687e 100644 --- a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt +++ b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt @@ -690,7 +690,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL } val resourcesDirectory = LaunchUtils.getGeodeResourcesDirectory(this) - resourcesDirectory.mkdirs() val pathBase = "geode.loader" val versionFilename = "version" @@ -713,6 +712,11 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL return } + if (resourcesDirectory.exists()) { + resourcesDirectory.deleteRecursively() + } + resourcesDirectory.mkdirs() + println("copyGeodeResources: found $resourcesVersion, expected $bundledVersion. Copying assets") val fileListing = try {