Compare commits

..
Author SHA1 Message Date
Chloe 8dd122631c run shared library initialization on separate context 2026-03-13 23:37:50 -07:00
36 changed files with 348 additions and 795 deletions

No files matched your search

+4 -4
View File
@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
- name: Setup Java JDK - name: Setup Java JDK
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 uses: actions/setup-java@v4
with: with:
java-version: 21 java-version: 21
distribution: temurin distribution: temurin
@@ -71,7 +71,7 @@ jobs:
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
- name: Upload release build - name: Upload release build
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@v4
if: ${{ matrix.config.lower == 'release' }} if: ${{ matrix.config.lower == 'release' }}
with: with:
name: geode-launcher-${{ matrix.config.lower }} name: geode-launcher-${{ matrix.config.lower }}
@@ -81,7 +81,7 @@ jobs:
${{ steps.rename_mappings.outputs.path }} ${{ steps.rename_mappings.outputs.path }}
- name: Upload debug build - name: Upload debug build
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@v4
if: ${{ matrix.config.lower == 'debug' }} if: ${{ matrix.config.lower == 'debug' }}
with: with:
name: geode-launcher-${{ matrix.config.lower }} name: geode-launcher-${{ matrix.config.lower }}
+8 -8
View File
@@ -19,8 +19,8 @@ android {
applicationId = "com.geode.launcher" applicationId = "com.geode.launcher"
minSdk = 23 minSdk = 23
targetSdk = 36 targetSdk = 36
versionCode = 29 versionCode = 26
versionName = "1.8.0" versionName = "1.7.3"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
@@ -78,7 +78,7 @@ android {
} }
externalNativeBuild { externalNativeBuild {
cmake { cmake {
version = "3.21.0+" version = "4.0.0+"
path = file("src/main/cpp/CMakeLists.txt") path = file("src/main/cpp/CMakeLists.txt")
} }
} }
@@ -88,18 +88,18 @@ android {
dependencies { dependencies {
implementation (platform("androidx.compose:compose-bom:$composeBOM")) implementation (platform("androidx.compose:compose-bom:$composeBOM"))
implementation ("androidx.core:core-ktx:1.18.0") implementation ("androidx.core:core-ktx:1.17.0")
implementation ("androidx.compose.ui:ui") implementation ("androidx.compose.ui:ui")
implementation ("androidx.compose.material3:material3") implementation ("androidx.compose.material3:material3")
implementation ("androidx.compose.ui:ui-tooling-preview") implementation ("androidx.compose.ui:ui-tooling-preview")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
implementation ("androidx.activity:activity-compose:1.13.0") implementation ("androidx.activity:activity-compose:1.12.4")
implementation ("androidx.activity:activity-ktx:1.13.0") implementation ("androidx.activity:activity-ktx:1.12.4")
implementation ("androidx.appcompat:appcompat:1.7.1") implementation ("androidx.appcompat:appcompat:1.7.1")
implementation ("androidx.documentfile:documentfile:1.1.0") implementation ("androidx.documentfile:documentfile:1.1.0")
implementation ("com.squareup.okio:okio:3.17.0") implementation ("com.squareup.okio:okio:3.16.4")
implementation ("com.squareup.okhttp3:okhttp:5.3.2") implementation ("com.squareup.okhttp3:okhttp:5.3.2")
implementation ("com.squareup.okhttp3:okhttp-coroutines:5.3.2") implementation ("com.squareup.okhttp3:okhttp-coroutines:5.3.2")
@@ -109,7 +109,7 @@ dependencies {
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.2") implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.2")
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2") implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2")
implementation ("androidx.browser:browser:1.10.0") implementation ("androidx.browser:browser:1.9.0")
debugImplementation ("androidx.compose.ui:ui-tooling") debugImplementation ("androidx.compose.ui:ui-tooling")
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5") coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
} }
+2 -22
View File
@@ -103,7 +103,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".preferences.TextViewActivity" android:name=".preferences.GeodeLogViewActivity"
android:exported="false" android:exported="false"
android:theme="@style/Theme.GeodeLauncher"> android:theme="@style/Theme.GeodeLauncher">
</activity> </activity>
@@ -127,7 +127,7 @@
</activity> </activity>
<activity <activity
android:name=".GeometryDashActivity" android:name=".GeometryDashActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard" android:configChanges="orientation|screenSize|smallestScreenSize"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:screenOrientation="landscape" android:screenOrientation="landscape"
@@ -195,26 +195,6 @@
<data android:scheme="geode-launcher" android:host="main" /> <data android:scheme="geode-launcher" android:host="main" />
</intent-filter> </intent-filter>
</activity-alias> </activity-alias>
<activity-alias
android:name=".MainActivitySapphire"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_sapphire"
android:label="@string/application_icon_sapphire"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="main" />
</intent-filter>
</activity-alias>
<activity-alias <activity-alias
android:name=".MainActivityGeode" android:name=".MainActivityGeode"
android:enabled="false" android:enabled="false"
+1 -1
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.21) cmake_minimum_required(VERSION 4.1.2)
project(launcherfix) project(launcherfix)
+4 -5
View File
@@ -15,7 +15,7 @@ public:
}; };
// this is every function that i thought would be relevant // this is every function that i thought would be relevant
constexpr std::array<const std::string_view, 10> remove_symbols{ constexpr std::array<const std::string_view, 12> remove_symbols{
"__gxx_personality_v0", "__gxx_personality_v0",
"__cxa_throw", "__cxa_throw",
"__cxa_rethrow", "__cxa_rethrow",
@@ -25,10 +25,9 @@ constexpr std::array<const std::string_view, 10> remove_symbols{
"__cxa_guard_abort", "__cxa_guard_abort",
"__cxa_guard_acquire", "__cxa_guard_acquire",
"__cxa_guard_release", "__cxa_guard_release",
"__cxa_free_exception" "__cxa_free_exception",
// conflicts with fix within geode on some devices "__emutls_get_address", // added in 2.208
// "__emutls_get_address", // added in 2.208 "__cxa_thread_atexit"
// "__cxa_thread_atexit"
// android 10's libc doesn't have these symbols // android 10's libc doesn't have these symbols
// and the ndk is forbidden from exporting them!! so keep it for old mods... // and the ndk is forbidden from exporting them!! so keep it for old mods...
@@ -24,8 +24,10 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.customRobTop.BaseRobTopActivity import com.customRobTop.BaseRobTopActivity
import com.customRobTop.JniToCpp import com.customRobTop.JniToCpp
import com.geode.launcher.main.FullScreenLoadingIndicator
import com.geode.launcher.main.LaunchNotification import com.geode.launcher.main.LaunchNotification
import com.geode.launcher.main.determineDisplayedCards import com.geode.launcher.main.determineDisplayedCards
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
@@ -35,13 +37,14 @@ import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.GeodeUtils import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.cocos2dx.lib.Cocos2dxGLSurfaceView import org.cocos2dx.lib.Cocos2dxGLSurfaceView
import org.cocos2dx.lib.Cocos2dxHelper import org.cocos2dx.lib.Cocos2dxHelper
import org.cocos2dx.lib.Cocos2dxRenderer import org.cocos2dx.lib.Cocos2dxRenderer
import org.fmod.FMOD import org.fmod.FMOD
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import javax.microedition.khronos.egl.EGL10 import javax.microedition.khronos.egl.EGL10
import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.egl.EGLConfig
@@ -91,97 +94,88 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
return return
} }
try { val rootLayout = setupContext()
createVersionFile() val loadingView = ComposeView(this).apply {
tryLoadGame() setContent {
} catch (e: UnsatisfiedLinkError) { FullScreenLoadingIndicator()
Log.e("GeodeLauncher", "Library linkage failure", e) }
}
// generates helpful information for use in debugging library load failures if (rootLayout != null) {
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0) rootLayout.addView(loadingView)
val abiMismatch = GamePackageUtils.detectAbiMismatch(this, gdPackageInfo, e) } else {
setContentView(loadingView)
}
val is64bit = LaunchUtils.is64bit lifecycleScope.launch {
val errorMessage = when { val error = preloadGame(rootLayout)
abiMismatch && is64bit -> LaunchUtils.LauncherError.LINKER_NEEDS_32BIT if (error != null) {
abiMismatch -> LaunchUtils.LauncherError.LINKER_NEEDS_64BIT returnToMain(error)
e.message?.contains("__gxx_personality_v0") == true ->
LaunchUtils.LauncherError.LINKER_FAILED_STL
else -> LaunchUtils.LauncherError.LINKER_FAILED
} }
returnToMain(errorMessage, e.message, e.stackTraceToString()) rootLayout?.removeView(loadingView)
return
} catch (e: Exception) {
Log.e("GeodeLauncher", "Uncaught error during game load", e)
returnToMain(
LaunchUtils.LauncherError.GENERIC,
e.message ?: "Unknown Exception",
e.stackTraceToString()
)
return
} }
}
suspend fun preloadGame(rootLayout: FrameLayout?): LaunchUtils.FullLauncherError? = try {
createVersionFile()
val loadFailed = tryLoadGame()
val baseView = createView()
if (rootLayout != null) {
rootLayout.addView(baseView)
} else {
setContentView(baseView)
}
mGLSurfaceView?.manualBackEvents = true
setupNotificationView(baseView, loadFailed)
null
} catch (e: UnsatisfiedLinkError) {
Log.e("GeodeLauncher", "Library linkage failure", e)
// generates helpful information for use in debugging library load failures
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
val abiMismatch = GamePackageUtils.detectAbiMismatch(this, gdPackageInfo, e)
val is64bit = LaunchUtils.is64bit
val errorMessage = when {
abiMismatch && is64bit -> LaunchUtils.LauncherError.LINKER_NEEDS_32BIT
abiMismatch -> LaunchUtils.LauncherError.LINKER_NEEDS_64BIT
e.message?.contains("__gxx_personality_v0") == true ->
LaunchUtils.LauncherError.LINKER_FAILED_STL
else -> LaunchUtils.LauncherError.LINKER_FAILED
}
LaunchUtils.FullLauncherError(
errorMessage,
e.message ?: "Unknown exception",
e.stackTraceToString()
)
} catch (e: Exception) {
Log.e("GeodeLauncher", "Uncaught error during game load", e)
LaunchUtils.FullLauncherError(
LaunchUtils.LauncherError.GENERIC,
e.message ?: "Unknown Exception",
e.stackTraceToString()
)
}
private fun setupContext(): FrameLayout? {
Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this)
GeodeUtils.setCapabilityListener(this)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
mGLSurfaceView?.sendKeyBack() mGLSurfaceView?.sendKeyBack()
} }
}) })
mGLSurfaceView?.manualBackEvents = true
}
private fun createVersionFile() {
val versionPath = File(filesDir, "game_version.txt")
val gameVersion = GamePackageUtils.getGameVersionCode(packageManager)
versionPath.writeText("$gameVersion")
}
private fun returnToMain(
error: LaunchUtils.LauncherError? = null,
returnMessage: String? = null,
returnExtendedMessage: String? = null
) {
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)!!.apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
if (error != null && !returnMessage.isNullOrEmpty()) {
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR, error)
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE, returnMessage)
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE, returnExtendedMessage)
}
}
startActivity(launchIntent)
}
private fun tryLoadGame() {
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
setupRedirection(gdPackageInfo)
Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this)
GeodeUtils.setCapabilityListener(this)
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
if (GamePackageUtils.getGameVersionCode(packageManager) >= 39L) {
// this fix requires geode v3, which is 2.206+
// there is a short period in which 2.206 users will still have geode v2, but whatever. ig
LauncherFix.enableExceptionsRenaming()
}
LauncherFix.performPatches()
loadInternalMods()
val baseView = createView()
if (!GeodeUtils.handleSafeArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { if (!GeodeUtils.handleSafeArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
val frameLayoutParams = ViewGroup.LayoutParams( val frameLayoutParams = ViewGroup.LayoutParams(
@@ -200,23 +194,65 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
right = displayInsets.safeInsetRight, right = displayInsets.safeInsetRight,
bottom = displayInsets.safeInsetBottom bottom = displayInsets.safeInsetBottom
) )
} else {
v.updatePadding(
left = 0, top = 0, right = 0, bottom = 0
)
} }
WindowInsets.CONSUMED WindowInsets.CONSUMED
} }
frameLayout.addView(baseView)
setContentView(frameLayout) setContentView(frameLayout)
} else { return frameLayout
setContentView(baseView)
} }
return null
}
private suspend fun createVersionFile() = runCatching {
val gameVersion = GamePackageUtils.getGameVersionCode(packageManager)
val versionPath = File(filesDir, "game_version.txt")
withContext(Dispatchers.IO) {
if (versionPath.exists()) {
versionPath.delete()
}
versionPath.writeText("$gameVersion")
}
}
private fun returnToMain(message: LaunchUtils.FullLauncherError? = null, ) {
val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
if (message != null) {
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR, message.error)
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE, message.returnMessage)
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE, message.extendedMessage)
}
}
startActivity(launchIntent)
}
private suspend fun tryLoadGame(): Boolean {
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
setupRedirection(gdPackageInfo)
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
if (GamePackageUtils.getGameVersionCode(packageManager) >= 39L) {
// this fix requires geode v3, which is 2.206+
// there is a short period in which 2.206 users will still have geode v2, but whatever. ig
LauncherFix.enableExceptionsRenaming()
}
LauncherFix.performPatches()
loadInternalMods()
setupPostLibraryLoad(gdPackageInfo) setupPostLibraryLoad(gdPackageInfo)
val geodeFailed = try { return try {
loadGeodeLibrary() loadGeodeLibrary()
false false
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
@@ -226,8 +262,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
handleGeodeException(e) handleGeodeException(e)
true true
} }
setupNotificationView(baseView, geodeFailed)
} }
private fun setupNotificationView(baseView: FrameLayout, geodeFailed: Boolean) { private fun setupNotificationView(baseView: FrameLayout, geodeFailed: Boolean) {
@@ -287,7 +321,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun tryLoadLibrary(packageInfo: PackageInfo, libraryName: String) { private suspend fun tryLoadLibrary(packageInfo: PackageInfo, libraryName: String) {
val nativeDir = getNativeLibraryDirectory(packageInfo.applicationInfo!!) val nativeDir = getNativeLibraryDirectory(packageInfo.applicationInfo!!)
val libraryPath = if (nativeDir.endsWith('/')) "${nativeDir}lib$libraryName.so" else "$nativeDir/lib$libraryName.so" val libraryPath = if (nativeDir.endsWith('/')) "${nativeDir}lib$libraryName.so" else "$nativeDir/lib$libraryName.so"
@@ -306,7 +340,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadLibraryCopy(libraryName: String, libraryPath: String) { private suspend fun loadLibraryCopy(libraryName: String, libraryPath: String) {
if (libraryPath.contains("!/")) { if (libraryPath.contains("!/")) {
// library in apk can't be loaded directly // library in apk can't be loaded directly
loadLibraryFromAssetsCopy(libraryName) loadLibraryFromAssetsCopy(libraryName)
@@ -316,11 +350,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
val library = File(libraryPath) val library = File(libraryPath)
val libraryCopy = File(cacheDir, "lib$libraryName.so") val libraryCopy = File(cacheDir, "lib$libraryName.so")
libraryCopy.outputStream().use { libraryOutput -> DownloadUtils.copyFile(library, libraryCopy)
library.inputStream().use { inputStream ->
DownloadUtils.copyFile(inputStream, libraryOutput)
}
}
System.load(libraryCopy.path) System.load(libraryCopy.path)
@@ -328,7 +358,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadLibraryFromAssetsCopy(libraryName: String) { private suspend fun loadLibraryFromAssetsCopy(libraryName: String) {
// loads a library loaded in assets // loads a library loaded in assets
// this copies the library to a non-compressed directory // this copies the library to a non-compressed directory
@@ -343,9 +373,11 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
// there doesn't seem to be a way to load a library from a file descriptor // there doesn't seem to be a way to load a library from a file descriptor
val libraryCopy = File(cacheDir, "lib$libraryName.so") val libraryCopy = File(cacheDir, "lib$libraryName.so")
libraryCopy.outputStream().use { libraryOutput -> withContext(Dispatchers.IO) {
libraryFd.createInputStream().use { inputStream -> libraryCopy.outputStream().use { libraryOutput ->
DownloadUtils.copyFile(inputStream, libraryOutput) libraryFd.createInputStream().use { inputStream ->
DownloadUtils.copyFile(inputStream, libraryOutput)
}
} }
} }
@@ -380,7 +412,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadGeodeLibrary() { private suspend fun loadGeodeLibrary() {
// Load Geode if exists // Load Geode if exists
// bundling the object with the application allows for nicer backtraces // bundling the object with the application allows for nicer backtraces
try { try {
@@ -401,22 +433,23 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
// also i get 20 million permission denied errors // also i get 20 million permission denied errors
val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!! val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!!
val copiedPath = File(filesDir.path, "copied") val copiedPath = File(filesDir, "copied")
if (copiedPath.exists()) { if (copiedPath.exists()) {
copiedPath.deleteRecursively() copiedPath.deleteRecursively()
} }
copiedPath.mkdir() copiedPath.mkdir()
val copiedGeodePath = File(copiedPath.path, "Geode.so") val copiedGeodePath = File(copiedPath, "Geode.so")
if (externalGeodePath.exists()) { if (externalGeodePath.exists()) {
DownloadUtils.copyFile( DownloadUtils.copyFile(
FileInputStream(externalGeodePath), externalGeodePath,
FileOutputStream(copiedGeodePath) copiedGeodePath
) )
if (copiedGeodePath.exists()) { if (copiedGeodePath.exists()) {
println("Loading Geode from ${externalGeodePath.name}") println("Loading Geode from ${externalGeodePath.name}")
System.load(copiedGeodePath.path) System.load(copiedGeodePath.path)
return return
} }
@@ -623,10 +656,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
mGLSurfaceView?.enableControllerEvents() mGLSurfaceView?.enableControllerEvents()
true true
} }
GeodeUtils.CAPABILITY_RESIZE_CALLBACK -> {
mGLSurfaceView?.supportsResizeEvents = true
true
}
else -> false else -> false
} }
} }
@@ -7,8 +7,6 @@ import android.text.format.Formatter
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
@@ -72,7 +70,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -85,9 +82,7 @@ import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.Theme.DARK import com.geode.launcher.ui.theme.Theme.DARK
import com.geode.launcher.ui.theme.Theme.LIGHT import com.geode.launcher.ui.theme.Theme.LIGHT
import com.geode.launcher.ui.theme.launcherTitleStyle import com.geode.launcher.ui.theme.launcherTitleStyle
import com.geode.launcher.ui.theme.sapphireTitleStyle
import com.geode.launcher.updater.ReleaseManager import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.utils.ApplicationIcon
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.GamePackageUtils import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
@@ -457,75 +452,35 @@ fun ExtraOptions(onSettings: () -> Unit) {
} }
} }
data class ColorPalette( val lightOuterColorGradient = arrayOf(
val outerColors: List<Pair<Float, Color>>, 0.0f to Color(0xfff5b11d),
val innerColors: List<Pair<Float, Color>>, 0.10f to Color(0xfff0834d),
0.24f to Color(0xffe85f6b),
0.49f to Color(0xffc9659e),
1.0f to Color(0xffb9588f)
) )
data class AnimatedIconSpec( val lightInnerColorGradient = arrayOf(
val lightPalette: ColorPalette, 0.48f to Color(0xfff2ce00),
val darkPalette: ColorPalette, 0.63f to Color(0xfff4b97e),
0.73f to Color(0xfff7b66a),
@param:DrawableRes val innerId: Int, 1.0f to Color(0xffeb8fac)
@param:DrawableRes val outerId: Int,
) )
data class BrandPalette( val darkOuterColorGradient = arrayOf(
@param:DrawableRes val darkLogo: Int, 0.0f to Color(0xfffffdff),
@param:DrawableRes val lightLogo: Int, 0.08f to Color(0xffF2EAF5),
0.12f to Color(0xffEDE5EF),
@param:StringRes val title: Int, 0.31f to Color(0xffCDB5CD),
val titleFont: TextStyle, 0.49f to Color(0xffBA9BBC),
val animatedIcon: AnimatedIconSpec? = null, 1.0f to Color(0xff8D7ACF),
) )
val geodeColorPalette = BrandPalette( val darkInnerColorGradient = arrayOf(
animatedIcon = AnimatedIconSpec( 0.48f to Color(0xffffffff),
lightPalette = ColorPalette( 0.63f to Color(0xfff5e4c2),
outerColors = listOf( 0.73f to Color(0xffe5c7ad),
0.0f to Color(0xfff5b11d), 1.0f to Color(0xffb790a9),
0.10f to Color(0xfff0834d),
0.24f to Color(0xffe85f6b),
0.49f to Color(0xffc9659e),
1.0f to Color(0xffb9588f)
),
innerColors = listOf(
0.48f to Color(0xfff2ce00),
0.63f to Color(0xfff4b97e),
0.73f to Color(0xfff7b66a),
1.0f to Color(0xffeb8fac)
)
),
darkPalette = ColorPalette(
outerColors = listOf(
0.0f to Color(0xfffffdff),
0.08f to Color(0xffF2EAF5),
0.12f to Color(0xffEDE5EF),
0.31f to Color(0xffCDB5CD),
0.49f to Color(0xffBA9BBC),
1.0f to Color(0xff8D7ACF),
),
innerColors = listOf(
0.48f to Color(0xffffffff),
0.63f to Color(0xfff5e4c2),
0.73f to Color(0xffe5c7ad),
1.0f to Color(0xffb790a9),
)
),
innerId = R.drawable.geode_monochrome_inner,
outerId = R.drawable.geode_monochrome_outer,
),
darkLogo = R.drawable.geode_base,
lightLogo = R.drawable.geode_base_light,
title = R.string.launcher_title,
titleFont = launcherTitleStyle
)
val sapphireColorPalette = BrandPalette(
darkLogo = R.drawable.sapphire_full,
lightLogo = R.drawable.sapphire_full,
title = R.string.application_icon_sapphire,
titleFont = sapphireTitleStyle
) )
@Preview(name = "non-animated, light") @Preview(name = "non-animated, light")
@@ -550,49 +505,22 @@ fun GeodeLogoAnimatedPreview() {
CompositionLocalProvider(LocalTheme provides theme) { CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) { GeodeLauncherTheme(theme = theme) {
Surface { Surface {
GeodeLogo(shouldAnimate = true) GeodeLogo(true)
}
}
}
}
@Preview(name = "sapphire, non-animated, light")
@Preview(name = "sapphire, non-animated, dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun SapphireLogoPreview() {
val theme = if (isSystemInDarkTheme()) DARK else LIGHT
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) {
Surface {
GeodeLogo(shouldAnimate = false, basePalette = sapphireColorPalette)
}
}
}
}
@Preview(name = "sapphire, animated, light")
@Preview(name = "sapphire, animated, dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun SapphireLogoAnimatedPreview() {
val theme = if (isSystemInDarkTheme()) DARK else LIGHT
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) {
Surface {
GeodeLogo(shouldAnimate = true, basePalette = sapphireColorPalette)
} }
} }
} }
} }
@Composable @Composable
fun AnimatedLogo(modifier: Modifier = Modifier, spec: AnimatedIconSpec) { fun AnimatedLogo(modifier: Modifier = Modifier) {
val theme = LocalTheme.current val theme = LocalTheme.current
val innerColorPalette = remember(theme) {
if (theme == LIGHT) lightInnerColorGradient else darkInnerColorGradient
}
val colorPalette = if (theme == LIGHT) spec.lightPalette val outerColorPalette = remember(theme) {
else spec.darkPalette if (theme == LIGHT) lightOuterColorGradient else darkOuterColorGradient
}
val innerColorPalette = colorPalette.innerColors
val outerColorPalette = colorPalette.outerColors
val infiniteTransition = rememberInfiniteTransition(label = "infinite transition") val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val offset by infiniteTransition.animateFloat( val offset by infiniteTransition.animateFloat(
@@ -606,14 +534,14 @@ fun AnimatedLogo(modifier: Modifier = Modifier, spec: AnimatedIconSpec) {
) )
val innerBrush = Brush.linearGradient( val innerBrush = Brush.linearGradient(
colorStops = innerColorPalette.toTypedArray(), colorStops = innerColorPalette,
start = Offset(0.0f, 0.0f + (165.06f * offset)), start = Offset(0.0f, 0.0f + (165.06f * offset)),
end = Offset(0.0f, 165.06f + (165.06f * offset)), end = Offset(0.0f, 165.06f + (165.06f * offset)),
tileMode = TileMode.Mirror tileMode = TileMode.Mirror
) )
val outerBrush = Brush.linearGradient( val outerBrush = Brush.linearGradient(
colorStops = outerColorPalette.toTypedArray(), colorStops = outerColorPalette,
start = Offset(0.0f + (84.69f * offset), 0.0f + (165.06f * offset)), start = Offset(0.0f + (84.69f * offset), 0.0f + (165.06f * offset)),
end = Offset(84.69f + (84.69f * offset), 169.61f + (165.06f * offset)), end = Offset(84.69f + (84.69f * offset), 169.61f + (165.06f * offset)),
tileMode = TileMode.Mirror tileMode = TileMode.Mirror
@@ -621,7 +549,7 @@ fun AnimatedLogo(modifier: Modifier = Modifier, spec: AnimatedIconSpec) {
Box(modifier = modifier) { Box(modifier = modifier) {
Icon( Icon(
painter = painterResource(spec.innerId), painter = painterResource(id = R.drawable.geode_monochrome_inner),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
@@ -632,10 +560,9 @@ fun AnimatedLogo(modifier: Modifier = Modifier, spec: AnimatedIconSpec) {
blendMode = BlendMode.SrcIn blendMode = BlendMode.SrcIn
) )
} }
.size(64.dp, 64.dp)
) )
Icon( Icon(
painter = painterResource(spec.outerId), painter = painterResource(id = R.drawable.geode_monochrome_outer),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
@@ -646,50 +573,39 @@ fun AnimatedLogo(modifier: Modifier = Modifier, spec: AnimatedIconSpec) {
blendMode = BlendMode.SrcIn blendMode = BlendMode.SrcIn
) )
} }
.size(64.dp, 64.dp)
) )
} }
} }
@Composable @Composable
fun GeodeLogo(modifier: Modifier = Modifier, shouldAnimate: Boolean = false, basePalette: BrandPalette = geodeColorPalette) { fun GeodeLogo(shouldAnimate: Boolean = false, modifier: Modifier = Modifier) {
FlowRow( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
itemVerticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
) { ) {
val theme = LocalTheme.current val theme = LocalTheme.current
if (basePalette.animatedIcon != null) { Crossfade(
Crossfade( targetState = shouldAnimate,
targetState = shouldAnimate, animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing),
animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing), label="logo fade"
label="logo fade" ) { screen ->
) { screen -> when (screen) {
when (screen) { true -> AnimatedLogo(modifier = Modifier.size(64.dp, 64.dp))
true -> AnimatedLogo(modifier = Modifier.size(64.dp, 64.dp), spec = basePalette.animatedIcon) false -> Image(
false -> Image( painterResource(if (theme == LIGHT)
painterResource(if (theme == LIGHT) R.drawable.geode_base_light else R.drawable.geode_base
basePalette.lightLogo else basePalette.darkLogo ),
), contentDescription = null,
contentDescription = null, modifier = Modifier.size(64.dp, 64.dp)
modifier = Modifier.size(64.dp, 64.dp) )
)
}
} }
} else {
Image(
painterResource(if (theme == LIGHT)
basePalette.lightLogo else basePalette.darkLogo
),
contentDescription = null,
modifier = Modifier.size(84.dp, 84.dp)
)
} }
Text( Text(
stringResource(basePalette.title), stringResource(R.string.launcher_title),
style = basePalette.titleFont, style = launcherTitleStyle,
fontSize = 64.sp, fontSize = 64.sp,
modifier = Modifier modifier = Modifier
.padding(12.dp) .padding(12.dp)
@@ -777,14 +693,7 @@ fun AltMainScreen(
.padding(8.dp) .padding(8.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
val sapphireLogo = remember { GeodeLogo(shouldAnimate = launchUIState.isInProgress())
PreferenceUtils.get(context)
.getString(PreferenceUtils.Key.SELECTED_ICON) == ApplicationIcon.SAPPHIRE.toId()
}
val basePalette = if (sapphireLogo) sapphireColorPalette
else geodeColorPalette
GeodeLogo(shouldAnimate = launchUIState.isInProgress(), basePalette = basePalette)
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -13,8 +13,7 @@ import kotlin.time.Instant
data class CrashDump( data class CrashDump(
val filename: String, val filename: String,
val lastModified: Instant, val lastModified: Instant
val fullPath: String,
) )
class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(crashDirectory) { class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(crashDirectory) {
@@ -42,19 +41,16 @@ class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(c
val children = crashDirectory.listFiles { val children = crashDirectory.listFiles {
// ignore indicator files (including old file) // ignore indicator files (including old file)
_, name -> !LaunchUtils.CRASH_INDICATOR_NAMES.contains(name) _, name -> name != LaunchUtils.CRASH_INDICATOR_NAME && name != "last-pid"
} ?: return emptyList() } ?: return emptyList()
hasIndicator = LaunchUtils.CRASH_INDICATOR_NAMES.any { hasIndicator = File(crashDirectory, LaunchUtils.CRASH_INDICATOR_NAME).exists()
File(crashDirectory, it).exists()
}
return children return children
.map { .map {
CrashDump( CrashDump(
filename = it.name, filename = it.name,
lastModified = Instant.fromEpochMilliseconds(it.lastModified()), lastModified = Instant.fromEpochMilliseconds(it.lastModified())
fullPath = it.path,
) )
} }
.sortedByDescending { it.lastModified } .sortedByDescending { it.lastModified }
@@ -62,12 +58,7 @@ class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(c
fun clearIndicator() { fun clearIndicator() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
LaunchUtils.CRASH_INDICATOR_NAMES.forEach { File(baseDirectory, LaunchUtils.CRASH_INDICATOR_NAME).delete()
val crashFile = File(baseDirectory, it)
if (crashFile.exists()) {
crashFile.delete()
}
}
hasIndicator = false hasIndicator = false
} }
@@ -75,8 +66,7 @@ class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(c
fun createIndicator() { fun createIndicator() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val crashFile = File(baseDirectory, LaunchUtils.CRASH_INDICATOR_NAMES.first()) File(baseDirectory, LaunchUtils.CRASH_INDICATOR_NAME).createNewFile()
crashFile.createNewFile()
hasIndicator = true hasIndicator = true
} }
@@ -14,7 +14,6 @@ data class GeodeLog(
val filename: String, val filename: String,
val lastModified: Instant, val lastModified: Instant,
val fileSize: Long, val fileSize: Long,
val fullPath: String,
) )
class GeodeLogsViewModel(crashDirectory: File) : BaseDirectoryViewModel<GeodeLog>(crashDirectory) { class GeodeLogsViewModel(crashDirectory: File) : BaseDirectoryViewModel<GeodeLog>(crashDirectory) {
@@ -44,8 +43,7 @@ class GeodeLogsViewModel(crashDirectory: File) : BaseDirectoryViewModel<GeodeLog
GeodeLog( GeodeLog(
filename = it.name, filename = it.name,
lastModified = Instant.fromEpochMilliseconds(it.lastModified()), lastModified = Instant.fromEpochMilliseconds(it.lastModified()),
fileSize = it.length(), fileSize = it.length()
fullPath = it.path,
) )
} }
.sortedByDescending { it.lastModified } .sortedByDescending { it.lastModified }
@@ -55,14 +55,11 @@ import com.geode.launcher.preferences.ApplicationLogsActivity
import com.geode.launcher.BuildConfig import com.geode.launcher.BuildConfig
import com.geode.launcher.R import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.preferences.GeodeLogsListingActivity
import com.geode.launcher.preferences.TextViewActivity
import com.geode.launcher.ui.theme.Typography import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.GamePackageUtils import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
data class LoadFailureInfo( data class LoadFailureInfo(
val title: LaunchUtils.LauncherError, val title: LaunchUtils.LauncherError,
@@ -140,28 +137,14 @@ fun ErrorInfoTitle(failureReason: LaunchUtils.LauncherError) {
} }
fun findSomethingForCrash(context: Context): File? {
val lastCrash = LaunchUtils.getLastCrash(context)
if (lastCrash != null) {
return lastCrash
}
val logsDirectory = LaunchUtils.getGeodeLogsDirectory(context)
if (!logsDirectory.exists()) {
return null
}
return logsDirectory.listFiles()?.maxByOrNull { it.lastModified() }
}
fun onShareCrash(context: Context) { fun onShareCrash(context: Context) {
val lastCrash = findSomethingForCrash(context) val lastCrash = LaunchUtils.getLastCrash(context)
if (lastCrash == null) { if (lastCrash == null) {
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
return return
} }
val baseDirectory = LaunchUtils.getBaseDirectory(context, true) val baseDirectory = LaunchUtils.getBaseDirectory(context)
val providerLocation = lastCrash.toRelativeString(baseDirectory) val providerLocation = lastCrash.toRelativeString(baseDirectory)
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}" val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
@@ -180,19 +163,6 @@ fun onShareCrash(context: Context) {
} }
} }
fun openLastCrash(context: Context) {
val lastCrash = findSomethingForCrash(context)
if (lastCrash == null) {
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
return
}
val launchIntent = Intent(context, TextViewActivity::class.java).run {
putExtra(TextViewActivity.EXTRA_LOG_VIEW_FILENAME, lastCrash.path)
}
context.startActivity(launchIntent)
}
fun onShowLogs(context: Context) { fun onShowLogs(context: Context) {
val launchIntent = Intent(context, ApplicationLogsActivity::class.java) val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
context.startActivity(launchIntent) context.startActivity(launchIntent)
@@ -285,34 +255,20 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
Text(stringResource(R.string.launcher_error_copy)) Text(stringResource(R.string.launcher_error_copy))
} }
} else { } else {
val crashFile = remember { findSomethingForCrash(context) } Button(onClick = { onShareCrash(context) }) {
val readableCrash = remember { crashFile?.extension == "log" } Icon(
painterResource(R.drawable.icon_share),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_error_export_crash))
}
if (crashFile != null) { val developerSettingsEnabled = remember {
Button(onClick = { onShareCrash(context) }) { PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.DEVELOPER_MODE)
Icon( }
painterResource(R.drawable.icon_share), if (developerSettingsEnabled) {
contentDescription = null FilledTonalButton(onClick = { onShowLogs(context) }) {
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_error_export_crash))
}
if (readableCrash) {
FilledTonalButton(onClick = { openLastCrash(context) }) {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_error_view_details))
}
}
} else {
Button(onClick = {
val launchIntent = Intent(context, GeodeLogsListingActivity::class.java)
context.startActivity(launchIntent)
}) {
Icon( Icon(
painterResource(R.drawable.icon_description), painterResource(R.drawable.icon_description),
contentDescription = null contentDescription = null
@@ -10,13 +10,16 @@ import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@@ -120,9 +123,10 @@ fun LaunchNotification(cards: List<LaunchNotificationType>) {
val theme = Theme.fromInt(themeOption) val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND) val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) { CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) { GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
// 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) {
Column( Column(
@@ -279,4 +283,27 @@ fun ForwardCompatibilityContent(modifier: Modifier = Modifier) {
}, },
modifier = modifier.width(IntrinsicSize.Max) modifier = modifier.width(IntrinsicSize.Max)
) )
}
@Composable
fun FullScreenLoadingIndicator() {
val theme = Theme.DARK
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = true, dynamicColor = !dynamicColorOption) {
Surface(modifier = Modifier.fillMaxSize()) {
var showProgress by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(1000L)
showProgress = true
}
AnimatedVisibility(showProgress) {
CircularProgressIndicator()
}
}
}
}
} }
@@ -182,6 +182,17 @@ class LaunchViewModel(private val application: Application): ViewModel() {
return return
} }
// last launch crashed, so cancel
if (loadFailure != null) {
// show a different message if this is the first thing users see
val cancelType = if (_uiState.value == LaunchUIState.Initial) {
LaunchCancelReason.LAST_LAUNCH_CRASHED_AUTOMATIC
} else LaunchCancelReason.LAST_LAUNCH_CRASHED
_uiState.tryEmit(LaunchUIState.Cancelled(cancelType))
return
}
if (GamePackageUtils.getGameVersionCode(packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN) { if (GamePackageUtils.getGameVersionCode(packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN) {
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED)) _uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
return return
@@ -194,18 +205,6 @@ class LaunchViewModel(private val application: Application): ViewModel() {
// if forcing immediate launch, then act as if it's manually started (skips timers) // if forcing immediate launch, then act as if it's manually started (skips timers)
val forceImmediate = launchArguments?.forceLaunch == true val forceImmediate = launchArguments?.forceLaunch == true
// last launch crashed, so cancel
if (!forceImmediate && loadFailure != null) {
// show a different message if this is the first thing users see
val cancelType = if (_uiState.value == LaunchUIState.Initial) {
LaunchCancelReason.LAST_LAUNCH_CRASHED_AUTOMATIC
} else LaunchCancelReason.LAST_LAUNCH_CRASHED
_uiState.tryEmit(LaunchUIState.Cancelled(cancelType))
return
}
if (forceImmediate) { if (forceImmediate) {
hasManuallyStarted = true hasManuallyStarted = true
} }
@@ -237,6 +237,7 @@ fun ApplicationIconScreen(
) { ) {
ApplicationIcon.entries.forEach { ApplicationIcon.entries.forEach {
val iconData = IconUtils.getIconDetails(it) val iconData = IconUtils.getIconDetails(it)
IconCard(iconData, selectedIcon == iconData.id) IconCard(iconData, selectedIcon == iconData.id)
} }
@@ -12,7 +12,6 @@ import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.Row
@@ -84,7 +83,7 @@ class CrashLogsActivity : ComponentActivity() {
} }
fun shareCrash(context: Context, filename: String) { fun shareCrash(context: Context, filename: String) {
val baseDirectory = LaunchUtils.getBaseDirectory(context, true) val baseDirectory = LaunchUtils.getBaseDirectory(context)
val crashPath = File(LaunchUtils.getCrashDirectory(context), filename) val crashPath = File(LaunchUtils.getCrashDirectory(context), filename)
if (!crashPath.exists()) { if (!crashPath.exists()) {
@@ -111,20 +110,11 @@ fun shareCrash(context: Context, filename: String) {
@Composable @Composable
fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Modifier = Modifier) { fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Modifier = Modifier) {
val isReadable = crashDump.filename.endsWith(".log")
val context = LocalContext.current
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 1.dp, horizontal = 8.dp) .padding(vertical = 1.dp, horizontal = 8.dp)
.height(64.dp) .height(64.dp)
.then(modifier) .then(modifier)
.clickable(isReadable) {
val launchIntent = Intent(context, TextViewActivity::class.java).run {
putExtra(TextViewActivity.EXTRA_LOG_VIEW_FILENAME, crashDump.fullPath)
}
context.startActivity(launchIntent)
}
.background(MaterialTheme.colorScheme.surfaceContainerLow) .background(MaterialTheme.colorScheme.surfaceContainerLow)
.padding(vertical = 8.dp, horizontal = 8.dp) .padding(vertical = 8.dp, horizontal = 8.dp)
) { ) {
@@ -197,6 +197,20 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
onClick = { onOpenLogs(context) } onClick = { onOpenLogs(context) }
) )
OptionsButton(
title = stringResource(R.string.preferences_view_geode_logs),
icon = {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
},
onClick = {
val launchIntent = Intent(context, GeodeLogsListingActivity::class.java)
context.startActivity(launchIntent)
}
)
OptionsButton( OptionsButton(
title = stringResource(R.string.preferences_copy_external_button), title = stringResource(R.string.preferences_copy_external_button),
description = LaunchUtils.getBaseDirectory(context).path, description = LaunchUtils.getBaseDirectory(context).path,
@@ -1,11 +1,7 @@
package com.geode.launcher.preferences package com.geode.launcher.preferences
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.provider.DocumentsContract
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -16,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
@@ -38,27 +35,23 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
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.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.R import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider import com.geode.launcher.log.GeodeLogsViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.Typography import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.ui.theme.robotoMonoFamily import com.geode.launcher.ui.theme.robotoMonoFamily
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
class TextViewActivity : ComponentActivity() { class GeodeLogViewActivity : ComponentActivity() {
companion object { companion object {
const val EXTRA_LOG_VIEW_FILENAME = "EXTRA_LOG_FILENAME" const val EXTRA_LOG_VIEW_FILENAME = "EXTRA_LOG_FILENAME"
} }
@@ -97,48 +90,19 @@ class TextViewActivity : ComponentActivity() {
} }
} }
fun onShareFile(context: Context, filename: File) {
val baseDirectory = LaunchUtils.getBaseDirectory(context, true)
val providerLocation = filename.toRelativeString(baseDirectory)
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", documentsPath)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
type = "application/octet-stream"
}
try {
context.startActivity(shareIntent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun GeodeLogView( fun GeodeLogView(
onBackPressedDispatcher: OnBackPressedDispatcher?, onBackPressedDispatcher: OnBackPressedDispatcher?,
filename: String, filename: String,
geodeLogsViewModel: GeodeLogsViewModel = viewModel(factory = GeodeLogsViewModel.Factory)
) { ) {
var loadingFile by remember { mutableStateOf(true) } var loadingFile by remember { mutableStateOf(true) }
var description by remember { mutableStateOf(emptyList<String>()) } var description by remember { mutableStateOf(emptyList<String>()) }
val toRead = File(filename)
LaunchedEffect(filename) { LaunchedEffect(filename) {
loadingFile = true loadingFile = true
description = geodeLogsViewModel.getFileText(filename)
withContext(Dispatchers.IO) {
if (!toRead.isFile) {
description = listOf("File does not exist!")
} else {
description = toRead.readLines()
}
}
loadingFile = false loadingFile = false
} }
@@ -159,21 +123,12 @@ fun GeodeLogView(
}, },
title = { title = {
Text( Text(
toRead.name, filename,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
actions = {
val context = LocalContext.current
IconButton(onClick = { onShareFile(context, toRead) }) {
Icon(
painterResource(R.drawable.icon_share),
contentDescription = stringResource(R.string.application_logs_share)
)
}
}
) )
if (loadingFile) { if (loadingFile) {
@@ -80,7 +80,7 @@ class GeodeLogsListingActivity : ComponentActivity() {
} }
fun shareLog(context: Context, filename: String) { fun shareLog(context: Context, filename: String) {
val baseDirectory = LaunchUtils.getBaseDirectory(context, true) val baseDirectory = LaunchUtils.getBaseDirectory(context)
val crashPath = File(LaunchUtils.getGeodeLogsDirectory(context), filename) val crashPath = File(LaunchUtils.getGeodeLogsDirectory(context), filename)
if (!crashPath.exists()) { if (!crashPath.exists()) {
@@ -117,8 +117,8 @@ fun GeodeLogCard(geodeLog: GeodeLog, geodeLogsViewModel: GeodeLogsViewModel, mod
.height(64.dp) .height(64.dp)
.then(modifier) .then(modifier)
.clickable { .clickable {
val launchIntent = Intent(context, TextViewActivity::class.java).run { val launchIntent = Intent(context, GeodeLogViewActivity::class.java).run {
putExtra(TextViewActivity.EXTRA_LOG_VIEW_FILENAME, geodeLog.fullPath) putExtra(GeodeLogViewActivity.EXTRA_LOG_VIEW_FILENAME, geodeLog.filename)
} }
context.startActivity(launchIntent) context.startActivity(launchIntent)
} }
@@ -497,7 +497,7 @@ fun GameplaySettingsGroup() {
SafeModeDialog(onDismiss = { SafeModeDialog(onDismiss = {
showSafeModeDialog = false showSafeModeDialog = false
}) { }) {
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!.apply { val launchIntent = Intent(context, MainActivity::class.java).apply {
// hardcode it, why not // hardcode it, why not
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
data = "geode-launcher://main/launch?safe-mode=true".toUri() data = "geode-launcher://main/launch?safe-mode=true".toUri()
@@ -581,20 +581,6 @@ fun AdvancedSettingsGroup(developerModeEnabled: Boolean, snackbarHostState: Snac
) )
} }
OptionsButton(
title = stringResource(R.string.preferences_view_geode_logs),
icon = {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
},
onClick = {
val launchIntent = Intent(context, GeodeLogsListingActivity::class.java)
context.startActivity(launchIntent)
}
)
OptionsButton( OptionsButton(
title = stringResource(R.string.preferences_view_crashes), title = stringResource(R.string.preferences_view_crashes),
icon = { icon = {
@@ -16,20 +16,9 @@ val quicksandFamily = FontFamily(
Font(R.font.quicksand_semibold, FontWeight.SemiBold) Font(R.font.quicksand_semibold, FontWeight.SemiBold)
) )
val googleSansFamily = FontFamily(
Font(R.font.googlesans_medium, FontWeight.Medium)
)
val launcherTitleStyle = TextStyle( val launcherTitleStyle = TextStyle(
fontFamily = quicksandFamily, fontFamily = quicksandFamily,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
fontSize = 32.sp, fontSize = 32.sp,
letterSpacing = 0.sp letterSpacing = 0.sp
)
val sapphireTitleStyle = TextStyle(
fontFamily = googleSansFamily,
fontWeight = FontWeight.Medium,
fontSize = 32.sp,
letterSpacing = 0.sp
) )
@@ -62,11 +62,6 @@ abstract class Downloadable {
* Release download link for the current platform * Release download link for the current platform
*/ */
abstract fun getDownload(): DownloadableAsset? abstract fun getDownload(): DownloadableAsset?
/**
* Resources download link for current platform.
*/
abstract fun getResourcesDownload(): DownloadableAsset?
} }
class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadable() { class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadable() {
@@ -106,18 +101,6 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
size = download.size.toLong() size = download.size.toLong()
) )
} }
override fun getResourcesDownload(): DownloadableAsset? {
val download = release.assets.find {
it.name == "resources.zip"
} ?: return null
return DownloadableAsset(
url = download.browserDownloadUrl,
filename = download.name,
size = download.size.toLong()
)
}
} }
class DownloadableLauncherRelease(val release: Release) : Downloadable() { class DownloadableLauncherRelease(val release: Release) : Downloadable() {
@@ -143,7 +126,8 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
* 0 | 0 | 1 * 0 | 0 | 1
* surprise! it's an xnor * surprise! it's an xnor
*/ */
asset.name.endsWith("apk") && asset.name.contains("android32") == use32BitPlatform
(asset.name.contains("android32")) == use32BitPlatform
} }
} }
@@ -155,10 +139,6 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
size = download.size.toLong() size = download.size.toLong()
) )
} }
override fun getResourcesDownload(): DownloadableAsset? {
return null
}
} }
class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadable() { class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadable() {
@@ -171,19 +151,11 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab
return version.createdAt.epochSeconds return version.createdAt.epochSeconds
} }
override fun getDownload(): DownloadableAsset { override fun getDownload(): DownloadableAsset? {
val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip" val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
return DownloadableAsset( return DownloadableAsset(
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename", url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename",
filename = filename filename = filename
) )
} }
override fun getResourcesDownload(): DownloadableAsset {
val filename = "resources.zip"
return DownloadableAsset(
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/resources.zip",
filename = filename
)
}
} }
@@ -18,16 +18,12 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okio.HashingSink
import okio.blackholeSink
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.source import okio.source
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException
import java.io.InterruptedIOException import java.io.InterruptedIOException
import kotlin.math.max
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
@@ -98,10 +94,7 @@ class ReleaseManager private constructor(
var dismissedLauncherUpdate = false var dismissedLauncherUpdate = false
var skipStateUpdate = false
private fun sendError(e: Exception) { private fun sendError(e: Exception) {
skipStateUpdate = true
_uiState.value = ReleaseManagerState.Failure(e) _uiState.value = ReleaseManagerState.Failure(e)
// ignore cancellation, it's good actually // ignore cancellation, it's good actually
@@ -173,60 +166,7 @@ class ReleaseManager private constructor(
return Result.success(outputFile) return Result.success(outputFile)
} }
private suspend fun performResourceDownload(resourceAsset: DownloadableAsset, initialSize: Long): Boolean {
val guessSize = if (resourceAsset.size == null) null else initialSize + resourceAsset.size
val guessInitial = if (guessSize == null) 0 else initialSize
_uiState.value = ReleaseManagerState.InDownload(guessInitial, guessSize)
val outputDir = getTempResourcesDirectory()
val outputPath = outputDir.path
val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext)
try {
if (!outputDir.exists()) {
outputDir.mkdirs()
}
DownloadUtils.downloadStream(
httpClient,
resourceAsset.url,
onProgress = { progress, outOf ->
if (!skipStateUpdate)
_uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize)
},
onResponse = { body ->
DownloadUtils.copyZipStreamToDirectory(
body.byteStream(),
outputDir
)
if (finalDir.exists()) {
finalDir.deleteRecursively()
}
if (!outputDir.renameTo(finalDir)) {
println("Failed to rename temporary directory!")
DownloadUtils.copyDirectory(outputDir, finalDir)
}
}
)
} catch (e: Exception) {
sendError(e)
return false
} finally {
val tempPathClone = File(outputPath)
runCatching {
if (tempPathClone.exists()) tempPathClone.deleteRecursively()
}
}
return true
}
private suspend fun performUpdate(release: Downloadable) { private suspend fun performUpdate(release: Downloadable) {
skipStateUpdate = false
val releaseAsset = release.getDownload() val releaseAsset = release.getDownload()
if (releaseAsset == null) { if (releaseAsset == null) {
val noAssetException = Exception("missing Android download") val noAssetException = Exception("missing Android download")
@@ -235,14 +175,8 @@ class ReleaseManager private constructor(
return return
} }
val resourcesAsset = release.getResourcesDownload()
var releaseSize = releaseAsset.size ?: 0L
val resourcesSize = resourcesAsset?.size ?: 0L
// set an initial download size // set an initial download size
val initialSize = if (releaseAsset.size == null && resourcesAsset?.size == null) null else releaseSize + resourcesSize _uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size)
_uiState.value = ReleaseManagerState.InDownload(0, initialSize)
val outputFile = getTempFile() val outputFile = getTempFile()
// clone the file instance as renameTo may move the original file // clone the file instance as renameTo may move the original file
@@ -261,10 +195,7 @@ class ReleaseManager private constructor(
httpClient, httpClient,
releaseAsset.url, releaseAsset.url,
onProgress = { progress, outOf -> onProgress = { progress, outOf ->
if (!skipStateUpdate) _uiState.value = ReleaseManagerState.InDownload(progress, outOf)
_uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize)
releaseSize = max(outOf, releaseSize)
}, },
onResponse = { body -> onResponse = { body ->
DownloadUtils.extractFileFromZipStream( DownloadUtils.extractFileFromZipStream(
@@ -298,12 +229,6 @@ class ReleaseManager private constructor(
} }
} }
if (resourcesAsset != null) {
if (!performResourceDownload(resourcesAsset, releaseSize)) {
return
}
}
// extraction performed // extraction performed
updatePreferences(release) updatePreferences(release)
@@ -324,9 +249,10 @@ class ReleaseManager private constructor(
val sharedPreferences = PreferenceUtils.get(applicationContext) val sharedPreferences = PreferenceUtils.get(applicationContext)
val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED) val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED)
?: return true ?: return false
val currentFileHash = computeFileHash(geodeFile) val currentFileHash = computeFileHash(geodeFile)
return originalFileHash != currentFileHash return originalFileHash != currentFileHash
} }
@@ -458,14 +384,8 @@ class ReleaseManager private constructor(
sharedPreferences.setString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED, fileHash) sharedPreferences.setString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED, fileHash)
} }
private fun computeFileHash(file: File): String { private fun computeFileHash(file: File): String =
val sink = HashingSink.md5(blackholeSink()) file.source().buffer().use { it.readByteString().md5().hex() }
file.source().buffer().use {
it.readAll(sink)
}
return sink.hash.hex()
}
private fun getGeodeOutputPath(): File { private fun getGeodeOutputPath(): File {
val geodeName = LaunchUtils.geodeFilename val geodeName = LaunchUtils.geodeFilename
@@ -474,13 +394,6 @@ class ReleaseManager private constructor(
return File(geodeDirectory, geodeName) return File(geodeDirectory, geodeName)
} }
private fun createRandomString(): String {
val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return buildString(8) {
repeat(8) { append(alphabet.random()) }
}
}
private fun getTempFile(): File { private fun getTempFile(): File {
val geodeName = LaunchUtils.geodeFilename val geodeName = LaunchUtils.geodeFilename
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext) val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
@@ -488,7 +401,11 @@ class ReleaseManager private constructor(
// warning!! while File::createTempFile may look tempting, a certain brand of phones has a messed up implementation of it // warning!! while File::createTempFile may look tempting, a certain brand of phones has a messed up implementation of it
// so we're making a temp file manually (as long as it doesn't collide with the geode download, it's okay) // so we're making a temp file manually (as long as it doesn't collide with the geode download, it's okay)
val suffix = createRandomString() val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9')
val suffix = buildString(8) {
repeat(8) { append(alphabet.random()) }
}
val tmpName = "tmp-$suffix.$geodeName" val tmpName = "tmp-$suffix.$geodeName"
val tempFile = File(geodeDirectory, tmpName) val tempFile = File(geodeDirectory, tmpName)
@@ -496,19 +413,6 @@ class ReleaseManager private constructor(
return tempFile return tempFile
} }
private fun getTempResourcesDirectory(): File {
val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext)
val suffix = createRandomString()
val tempDirName = "tmp-$suffix-geode.loader"
val parentDir = finalDir.parentFile
return if (parentDir != null) {
File(finalDir.parentFile, tempDirName)
} else {
File(tempDirName)
}
}
/** /**
* Cancels the current update job. * Cancels the current update job.
*/ */
@@ -17,7 +17,7 @@ object Constants {
const val SUPPORTED_VERSION_CODE_MIN = 37L const val SUPPORTED_VERSION_CODE_MIN = 37L
// anything below this version code shows a warning on the main menu // anything below this version code shows a warning on the main menu
const val SUPPORTED_VERSION_CODE_MIN_WARNING = 41L const val SUPPORTED_VERSION_CODE_MIN_WARNING = 40L
// anything above this version code will show a warning when starting the game // anything above this version code will show a warning when starting the game
const val SUPPORTED_VERSION_CODE = 41L const val SUPPORTED_VERSION_CODE = 41L
@@ -72,35 +72,6 @@ object DownloadUtils {
} }
} }
suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible {
ZipInputStream(inputStream).use { zipStream ->
if (output.exists()) output.deleteRecursively()
output.mkdirs()
val canonicalOutput = output.canonicalPath
var entry = zipStream.nextEntry
while (entry != null) {
val destination = File(output, entry.name)
if (!destination.canonicalPath.startsWith(canonicalOutput)) {
throw IOException("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
}
}
}
/** /**
* Extracts a file named by zipPath from inputStream and copies it to outputStream * Extracts a file named by zipPath from inputStream and copies it to outputStream
* *
@@ -160,13 +131,6 @@ object DownloadUtils {
}} }}
} }
} }
suspend fun copyDirectory(sourceFile: File, outputFile: File) = withContext(Dispatchers.IO) {
sourceFile.copyRecursively(
outputFile,
overwrite = true
)
}
} }
// based on <https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java> // based on <https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java>
@@ -557,7 +557,6 @@ object GeodeUtils {
const val CAPABILITY_EXTENDED_INPUT = "extended_input" const val CAPABILITY_EXTENDED_INPUT = "extended_input"
const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs" const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs"
const val CAPABILITY_RICH_INPUT = "internal_callbacks_v2" const val CAPABILITY_RICH_INPUT = "internal_callbacks_v2"
const val CAPABILITY_RESIZE_CALLBACK = "resize_callbacks"
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null) private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
@@ -15,10 +15,6 @@ import androidx.compose.ui.res.painterResource
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.geode.launcher.R import com.geode.launcher.R
import kotlinx.datetime.Month
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import kotlin.time.Clock
// https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486 // https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486
@Composable @Composable
@@ -47,26 +43,23 @@ enum class ApplicationIcon {
DEFAULT, DEFAULT,
GEODE, GEODE,
PRIDE, PRIDE,
TRANS, TRANS;
SAPPHIRE;
companion object { companion object {
fun fromId(id: String) = when (id) { fun fromId(id: String) = when (id) {
"default" -> DEFAULT "default" -> ApplicationIcon.DEFAULT
"geode" -> GEODE "geode" -> ApplicationIcon.GEODE
"pride" -> PRIDE "pride" -> ApplicationIcon.PRIDE
"trans" -> TRANS "trans" -> ApplicationIcon.TRANS
"sapphire" -> SAPPHIRE else -> ApplicationIcon.DEFAULT
else -> DEFAULT
} }
} }
fun toId() = when (this) { fun toId() = when (this) {
PRIDE -> "pride" ApplicationIcon.PRIDE -> "pride"
GEODE -> "geode" ApplicationIcon.GEODE -> "geode"
DEFAULT -> "default" ApplicationIcon.DEFAULT -> "default"
TRANS -> "trans" ApplicationIcon.TRANS -> "trans"
SAPPHIRE -> "sapphire"
} }
} }
@@ -103,11 +96,5 @@ object IconUtils {
iconId = R.mipmap.ic_launcher_trans, iconId = R.mipmap.ic_launcher_trans,
nameId = R.string.application_icon_trans nameId = R.string.application_icon_trans
) )
ApplicationIcon.SAPPHIRE -> ApplicationIconDetails(
id = id.toId(),
component = "MainActivitySapphire",
iconId = R.mipmap.ic_launcher_sapphire,
nameId = R.string.application_icon_sapphire,
)
} }
} }
@@ -73,7 +73,7 @@ object LaunchUtils {
return File(getBaseDirectory(context), "save") return File(getBaseDirectory(context), "save")
} }
val CRASH_INDICATOR_NAMES = setOf("lastSessionDidCrash", "last-pid", "last-crashed") const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
fun getCrashDirectory(context: Context): File { fun getCrashDirectory(context: Context): File {
val base = getBaseDirectory(context) val base = getBaseDirectory(context)
@@ -85,11 +85,6 @@ object LaunchUtils {
return File(base, "game/geode/logs/") return File(base, "game/geode/logs/")
} }
fun getGeodeResourcesDirectory(context: Context): File {
val base = getBaseDirectory(context)
return File(base, "game/geode/resources/geode.loader/")
}
fun getLastCrash(context: Context): File? { fun getLastCrash(context: Context): File? {
val crashDirectory = getCrashDirectory(context) val crashDirectory = getCrashDirectory(context)
if (!crashDirectory.exists()) { if (!crashDirectory.exists()) {
@@ -98,7 +93,7 @@ object LaunchUtils {
val children = crashDirectory.listFiles { val children = crashDirectory.listFiles {
// ignore indicator files (including old file) // ignore indicator files (including old file)
_, name -> !CRASH_INDICATOR_NAMES.contains(name) _, name -> name != CRASH_INDICATOR_NAME && name != "last-pid"
} }
return children?.maxByOrNull { it.lastModified() } return children?.maxByOrNull { it.lastModified() }
@@ -106,9 +101,9 @@ object LaunchUtils {
fun lastSessionCrashed(context: Context): Boolean { fun lastSessionCrashed(context: Context): Boolean {
val base = getCrashDirectory(context) val base = getCrashDirectory(context)
return CRASH_INDICATOR_NAMES.any { val crashIndicatorFile = File(base, CRASH_INDICATOR_NAME)
File(base, it).exists()
} return crashIndicatorFile.exists()
} }
enum class LauncherError : Serializable { enum class LauncherError : Serializable {
@@ -124,6 +119,12 @@ object LaunchUtils {
} }
} }
data class FullLauncherError(
val error: LauncherError,
val returnMessage: String,
val extendedMessage: String? = null,
) : Serializable
const val LAUNCHER_KEY_RETURN_ERROR = "return_error" const val LAUNCHER_KEY_RETURN_ERROR = "return_error"
const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message" const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message"
const val LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE = "return_extended_message" const val LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE = "return_extended_message"
@@ -63,7 +63,6 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
var manualBackEvents = false var manualBackEvents = false
var controllerEventsEnabled = false var controllerEventsEnabled = false
private set private set
var supportsResizeEvents = false
var cocos2dxEditText: AppCompatEditText? = null var cocos2dxEditText: AppCompatEditText? = null
set(value) { set(value) {
@@ -289,12 +288,6 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
if (!isInEditMode) { if (!isInEditMode) {
cocos2dxRenderer.setScreenWidthAndHeight(newSurfaceWidth, newSurfaceHeight) cocos2dxRenderer.setScreenWidthAndHeight(newSurfaceWidth, newSurfaceHeight)
} }
if (supportsResizeEvents) {
queueEvent {
GeodeUtils.resizeSurface(newSurfaceWidth, newSurfaceHeight)
}
}
} }
private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean { private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
@@ -1,36 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M0,0h1024v1024h-1024z"
android:fillColor="#070066"/>
<path
android:pathData="M512.5,521.1L915.2,638.7L1082,922.6L915.2,1206.4L512.5,1324L109.8,1206.4L-57,922.6L109.8,638.7L512.5,521.1Z"
android:fillColor="#6694C8"/>
<path
android:pathData="M915.2,638.7L1082,922.6V638.7H915.2Z"
android:fillColor="#4268B8"/>
<path
android:pathData="M109.8,638.7L512.5,521.1L109.8,397.2V638.7Z"
android:fillColor="#2D5EC7"/>
<path
android:pathData="M-57,922.6L109.8,638.7H-57V922.6Z"
android:fillColor="#4268B8"/>
<path
android:pathData="M512.5,521.1L915.2,638.7V397.2L512.5,521.1Z"
android:fillColor="#2D5EC7"/>
<path
android:pathData="M-57,638.7H109.8V397.2L-57,638.7Z"
android:fillColor="#2248AA"/>
<path
android:pathData="M109.8,397.2L512.5,521.1V194L109.8,397.2Z"
android:fillColor="#020F9D"/>
<path
android:pathData="M512.5,521.1L915.2,397.2L512.5,194V521.1Z"
android:fillColor="#020F9D"/>
<path
android:pathData="M915.2,638.7H1082L915.2,397.2V638.7Z"
android:fillColor="#2248AA"/>
</vector>
@@ -1,27 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="643"
android:viewportHeight="766">
<group android:scaleX="0.41038585"
android:scaleY="0.4888889"
android:translateX="191.0163"
android:translateY="195.75555">
<path
android:pathData="M299.94,33.78C313.01,25.85 329.41,25.85 342.48,33.78L370.9,51.03L268.17,116.49L268.23,116.51L92.68,223.61C84.64,228.51 79.74,237.25 79.74,246.66V509.84C79.74,519.26 84.64,527.99 92.68,532.89L306.22,663.17C314.85,668.44 325.71,668.44 334.34,663.17L547.89,532.89C555.92,527.99 560.82,519.26 560.82,509.85V246.66C560.82,246.2 560.81,245.74 560.78,245.28V166.26L599.75,189.9C612,197.34 619.48,210.63 619.48,224.96V540.81C619.48,555.14 612,568.42 599.75,575.86L342.48,731.98C329.41,739.91 313.01,739.91 299.94,731.98L42.66,575.86C30.41,568.42 22.93,555.14 22.93,540.81V224.96C22.93,210.63 30.41,197.34 42.66,189.9L299.94,33.78Z"
android:fillColor="#000000"
android:fillAlpha="0.15"/>
<path
android:pathData="M311.75,173.17C318.78,168.88 327.62,168.88 334.65,173.17L481.88,262.87C488.43,266.86 492.43,273.98 492.43,281.66V463.16C492.43,470.84 488.43,477.96 481.88,481.95L334.65,571.65C327.62,575.94 318.78,575.94 311.75,571.65L164.52,481.95C157.97,477.96 153.97,470.84 153.97,463.16V414.95L208.24,448.54C208.52,448.75 208.8,448.95 209.1,449.13L315.66,515.36C319.22,517.57 323.72,517.57 327.28,515.36L433.84,449.13C437.07,447.13 439.03,443.59 439.03,439.79V305.9C439.03,302.09 437.07,298.56 433.84,296.55L327.28,230.33C323.72,228.12 319.22,228.12 315.66,230.33L209.1,296.55C208.62,296.85 208.17,297.18 207.75,297.55L153.97,331.28V281.66C153.97,273.98 157.97,266.86 164.52,262.87L311.75,173.17Z"
android:fillColor="#000000"
android:fillAlpha="0.15"
android:fillType="evenOdd"/>
<path
android:pathData="M299.94,5.95C313.01,-1.98 329.41,-1.98 342.48,5.95L374.73,25.52L264.09,96.01L264.15,96.03L74.11,211.97C66.07,216.88 61.17,225.61 61.17,235.02V520.77C61.17,530.18 66.07,538.92 74.11,543.82L306.15,685.38C314.78,690.65 325.64,690.65 334.27,685.38L566.31,543.82C574.34,538.92 579.25,530.18 579.25,520.77V235.02C579.25,234.53 579.23,234.05 579.2,233.56V149.6L622.68,175.99C634.93,183.42 642.41,196.71 642.41,211.04V554.72C642.41,569.05 634.93,582.34 622.68,589.78L342.48,759.82C329.41,767.75 313.01,767.75 299.94,759.82L19.73,589.78C7.48,582.34 0,569.05 0,554.72V211.04C0,196.71 7.48,183.42 19.73,175.99L299.94,5.95Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M311.75,142.59C318.78,138.31 327.62,138.31 334.65,142.59L507.79,248.08C514.34,252.08 518.34,259.2 518.34,266.87V479.95C518.34,487.62 514.34,494.74 507.79,498.74L334.65,604.23C327.62,608.51 318.78,608.51 311.75,604.23L138.61,498.74C132.06,494.74 128.06,487.62 128.06,479.95V422.46L189.93,460.76C190.22,460.98 190.52,461.19 190.84,461.39L315.4,538.8C318.95,541.01 323.46,541.01 327.01,538.8L451.58,461.39C454.8,459.38 456.77,455.85 456.77,452.04V295.77C456.77,291.97 454.8,288.44 451.58,286.43L327.01,209.02C323.46,206.81 318.95,206.81 315.4,209.02L190.84,286.43C190.32,286.76 189.83,287.12 189.37,287.52L128.06,325.98V266.87C128.06,259.2 132.06,252.08 138.61,248.08L311.75,142.59Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</group>
</vector>
@@ -1,18 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="643"
android:viewportHeight="766">
<group android:scaleX="0.41038585"
android:scaleY="0.4888889"
android:translateX="191.0163"
android:translateY="195.75555">
<path
android:pathData="M299.94,5.95C313.01,-1.98 329.41,-1.98 342.48,5.95L374.73,25.52L264.09,96.01L264.15,96.03L74.11,211.97C66.07,216.88 61.17,225.61 61.17,235.02V520.77C61.17,530.18 66.07,538.92 74.11,543.82L306.15,685.38C314.78,690.65 325.64,690.65 334.27,685.38L566.31,543.82C574.34,538.92 579.25,530.18 579.25,520.77V235.02C579.25,234.53 579.23,234.05 579.2,233.56V149.6L622.68,175.99C634.93,183.42 642.41,196.71 642.41,211.04V554.72C642.41,569.05 634.93,582.34 622.68,589.78L342.48,759.82C329.41,767.75 313.01,767.75 299.94,759.82L19.73,589.78C7.48,582.34 0,569.05 0,554.72V211.04C0,196.71 7.48,183.42 19.73,175.99L299.94,5.95Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M311.75,142.59C318.78,138.31 327.62,138.31 334.65,142.59L507.79,248.08C514.34,252.08 518.34,259.2 518.34,266.87V479.95C518.34,487.62 514.34,494.74 507.79,498.74L334.65,604.23C327.62,608.51 318.78,608.51 311.75,604.23L138.61,498.74C132.06,494.74 128.06,487.62 128.06,479.95V422.46L189.93,460.76C190.22,460.98 190.52,461.19 190.84,461.39L315.4,538.8C318.95,541.01 323.46,541.01 327.01,538.8L451.58,461.39C454.8,459.38 456.77,455.85 456.77,452.04V295.77C456.77,291.97 454.8,288.44 451.58,286.43L327.01,209.02C323.46,206.81 318.95,206.81 315.4,209.02L190.84,286.43C190.32,286.76 189.83,287.12 189.37,287.52L128.06,325.98V266.87C128.06,259.2 132.06,252.08 138.61,248.08L311.75,142.59Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</group>
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_sapphire_background"/>
<foreground android:drawable="@drawable/ic_launcher_sapphire_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_sapphire_monochrome" />
</adaptive-icon>
+2 -4
View File
@@ -53,7 +53,6 @@
<string name="launcher_error_view_logs">View logs</string> <string name="launcher_error_view_logs">View logs</string>
<string name="launcher_error_copy_error_alt">Copy</string> <string name="launcher_error_copy_error_alt">Copy</string>
<string name="launcher_error_copy">Copy details</string> <string name="launcher_error_copy">Copy details</string>
<string name="launcher_error_view_details">View details</string>
<string name="launcher_error_export_crash">Share crash</string> <string name="launcher_error_export_crash">Share crash</string>
<string name="launcher_error_export_missing">No crash dump was found!</string> <string name="launcher_error_export_missing">No crash dump was found!</string>
<string name="launcher_error_device_info">Android %2$s, %1$s (%5$s)\nGD %4$s, Geode %3$s + v%6$s</string> <string name="launcher_error_device_info">Android %2$s, %1$s (%5$s)\nGD %4$s, Geode %3$s + v%6$s</string>
@@ -119,10 +118,10 @@
<string name="load_failed_crashed_description">A crash was reported during the last session. Try to:</string> <string name="load_failed_crashed_description">A crash was reported during the last session. Try to:</string>
<string name="load_failed_recommendation_reinstall">Reinstall Geometry Dash.</string> <string name="load_failed_recommendation_reinstall">Reinstall Geometry Dash.</string>
<string name="load_failed_recommendation_switch_32bit_abi">Install the 32-bit version of the launcher.</string> <string name="load_failed_recommendation_switch_32bit_abi">Install the 32-bit version of the launcher.</string>
<string name="load_failed_recommendation_switch_64bit_abi">Install the non-32-bit version of the launcher.</string> <string name="load_failed_recommendation_switch_64bit_abi">Install the non 32-bit version of the launcher.</string>
<string name="load_failed_recommendation_update">Verify that the latest versions of the launcher and Geometry Dash are installed.</string> <string name="load_failed_recommendation_update">Verify that the latest versions of the launcher and Geometry Dash are installed.</string>
<string name="load_failed_recommendation_report">Report this issue to the developers.</string> <string name="load_failed_recommendation_report">Report this issue to the developers.</string>
<string name="load_failed_recommendation_safe_mode">Use the safe mode button in Settings.</string> <string name="load_failed_recommendation_safe_mode">Enable the safe mode button in Settings.</string>
<string name="load_failed_recommendation_dev_stl">Devs: If this is a locally built Geode version, please use the c++_shared ANDROID_STL!</string> <string name="load_failed_recommendation_dev_stl">Devs: If this is a locally built Geode version, please use the c++_shared ANDROID_STL!</string>
<string name="message_box_cancel">Cancel</string> <string name="message_box_cancel">Cancel</string>
@@ -336,7 +335,6 @@
<string name="application_icon_geode">Geode</string> <string name="application_icon_geode">Geode</string>
<string name="application_icon_pride">Pride</string> <string name="application_icon_pride">Pride</string>
<string name="application_icon_trans">Transgender</string> <string name="application_icon_trans">Transgender</string>
<string name="application_icon_sapphire">Sapphire</string>
<string name="application_icon_confirm">Change icon to %1$s?</string> <string name="application_icon_confirm">Change icon to %1$s?</string>
<string name="application_icon_confirm_description">Geode Launcher will close itself to change the application icon.</string> <string name="application_icon_confirm_description">Geode Launcher will close itself to change the application icon.</string>
+5 -5
View File
@@ -1,13 +1,13 @@
buildscript { buildscript {
val composeBOM by extra("2026.03.01") val composeBOM by extra("2026.02.00")
} }
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "9.1.0" apply false id("com.android.application") version "9.0.1" apply false
id("com.android.library") version "9.1.0" apply false id("com.android.library") version "9.0.1" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.20" apply false id("org.jetbrains.kotlin.plugin.serialization") version "2.3.10" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
} }
tasks.register<Delete>("clean") { tasks.register<Delete>("clean") {
+2 -2
View File
@@ -1,7 +1,7 @@
#Sun Apr 05 23:29:16 MST 2026 #Wed Jan 28 04:12:33 MST 2026
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME