Compare commits

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

No files matched your search

+12 -59
View File
@@ -12,23 +12,17 @@ jobs:
config: config:
- name: Release - name: Release
lower: release lower: release
flavor: standard
- name: Debug - name: Debug
lower: debug lower: debug
flavor: standard
- name: Release
lower: release
flavor: googlePlay
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
@@ -40,19 +34,9 @@ jobs:
- name: Fix gradle permissions - name: Fix gradle permissions
run: chmod +x ./gradlew run: chmod +x ./gradlew
- name: Build project (standard) - name: Build project
if: ${{ matrix.config.flavor == 'standard' }}
run: | run: |
./gradlew app:assembleStandard${{ matrix.config.name }} \ ./gradlew app:assemble${{ matrix.config.name }} \
-Pandroid.injected.signing.store.file="$HOME/release.keystore" \
-Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \
-Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \
-Pandroid.injected.signing.key.password="${{secrets.KEY_PASSWORD}}"
- name: Build project (Google Play)
if: ${{ matrix.config.flavor == 'googlePlay' }}
run: |
./gradlew app:bundleGooglePlayRelease -PdisableAbiSplits \
-Pandroid.injected.signing.store.file="$HOME/release.keystore" \ -Pandroid.injected.signing.store.file="$HOME/release.keystore" \
-Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \ -Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \
-Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \ -Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \
@@ -60,57 +44,35 @@ jobs:
- name: Rename universal apk - name: Rename universal apk
id: rename_apk_universal id: rename_apk_universal
if: ${{ matrix.config.flavor == 'standard' }}
run: | run: |
SHORT_SHA=${GITHUB_SHA::7} SHORT_SHA=${GITHUB_SHA::7}
APK_PATH=${{github.workspace}}/app/build/outputs/apk/standard/${{ matrix.config.lower }}/app-standard-universal-${{ matrix.config.lower }}.apk APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-universal-${{ matrix.config.lower }}.apk
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.apk OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.apk
mv $APK_PATH $OUT_PATH mv $APK_PATH $OUT_PATH
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
- name: Rename apk - name: Rename apk
id: rename_apk id: rename_apk
if: ${{ matrix.config.flavor == 'standard' }}
run: | run: |
SHORT_SHA=${GITHUB_SHA::7} SHORT_SHA=${GITHUB_SHA::7}
APK_PATH=${{github.workspace}}/app/build/outputs/apk/standard/${{ matrix.config.lower }}/app-standard-armeabi-v7a-${{ matrix.config.lower }}.apk APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-armeabi-v7a-${{ matrix.config.lower }}.apk
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-android32-$SHORT_SHA.apk OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-android32-$SHORT_SHA.apk
mv $APK_PATH $OUT_PATH mv $APK_PATH $OUT_PATH
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
- name: Rename mappings - name: Rename mappings
id: rename_mappings id: rename_mappings
if: ${{ matrix.config.flavor == 'standard' && matrix.config.lower == 'release' }} if: ${{ matrix.config.lower == 'release' }}
run: | run: |
SHORT_SHA=${GITHUB_SHA::7} SHORT_SHA=${GITHUB_SHA::7}
MAPPING_PATH=${{github.workspace}}/app/build/outputs/mapping/standardRelease/mapping.txt MAPPING_PATH=${{github.workspace}}/app/build/outputs/mapping/${{ matrix.config.lower }}/mapping.txt
OUT_PATH=${{github.workspace}}/mapping-${{ matrix.config.lower }}-$SHORT_SHA.txt
mv $MAPPING_PATH $OUT_PATH
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
- name: Rename AAB
id: rename_aab
if: ${{ matrix.config.flavor == 'googlePlay' }}
run: |
SHORT_SHA=${GITHUB_SHA::7}
AAB_PATH=${{github.workspace}}/app/build/outputs/bundle/googlePlayRelease/app-googlePlay-release.aab
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.aab
mv $AAB_PATH $OUT_PATH
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
- name: Rename AAB mappings
id: rename_aab_mappings
if: ${{ matrix.config.flavor == 'googlePlay' }}
run: |
SHORT_SHA=${GITHUB_SHA::7}
MAPPING_PATH=${{github.workspace}}/app/build/outputs/mapping/googlePlayRelease/mapping.txt
OUT_PATH=${{github.workspace}}/mapping-${{ matrix.config.lower }}-$SHORT_SHA.txt OUT_PATH=${{github.workspace}}/mapping-${{ matrix.config.lower }}-$SHORT_SHA.txt
mv $MAPPING_PATH $OUT_PATH mv $MAPPING_PATH $OUT_PATH
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.flavor == 'standard' && matrix.config.lower == 'release' }} if: ${{ matrix.config.lower == 'release' }}
with: with:
name: geode-launcher-${{ matrix.config.lower }} name: geode-launcher-${{ matrix.config.lower }}
path: | path: |
@@ -119,19 +81,10 @@ 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.flavor == 'standard' && matrix.config.lower == 'debug' }} if: ${{ matrix.config.lower == 'debug' }}
with: with:
name: geode-launcher-${{ matrix.config.lower }} name: geode-launcher-${{ matrix.config.lower }}
path: | path: |
${{ steps.rename_apk_universal.outputs.path }} ${{ steps.rename_apk_universal.outputs.path }}
${{ steps.rename_apk.outputs.path }} ${{ steps.rename_apk.outputs.path }}
- name: Upload Google Play build
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: ${{ matrix.config.flavor == 'googlePlay' }}
with:
name: geode-launcher-google-play
path: |
${{ steps.rename_aab.outputs.path }}
${{ steps.rename_aab_mappings.outputs.path }}
+26 -286
View File
@@ -1,7 +1,4 @@
import groovy.json.JsonSlurper val composeBOM: String by rootProject.extra
import java.net.URI
import java.security.MessageDigest
import java.util.zip.ZipInputStream
kotlin { kotlin {
compilerOptions { compilerOptions {
@@ -15,241 +12,15 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose") id("org.jetbrains.kotlin.plugin.compose")
} }
val currentGeodeVersion = "5.11.2"
abstract class DownloadVersionMetadataTask : DefaultTask() {
@get:Input
abstract val geodeVersion: Property<String>
@get:OutputFile
abstract val targetFile: RegularFileProperty
@TaskAction
fun execute() {
val downloadGeodeVersion = geodeVersion.get()
val destinationFile = targetFile.get().asFile
val releaseUrl = "https://api.geode-sdk.org/v1/loader/versions/$downloadGeodeVersion"
URI.create(releaseUrl).toURL().openStream().use { response ->
destinationFile.outputStream().use { destination ->
response.copyTo(destination)
}
}
}
}
abstract class DownloadGeodeFileTask : DefaultTask() {
@get:Input
abstract val platform: Property<String>
@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()
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<String>
@get:OutputFile
abstract val targetFile: RegularFileProperty
@TaskAction
fun execute() {
val downloadGeodeVersion = geodeVersion.get()
val downloadPlatform = platform.get()
val destinationFile = targetFile.get().asFile
val tempOutput = this.downloadZip("https://github.com/geode-sdk/geode/releases/download/v$downloadGeodeVersion/geode-v$downloadGeodeVersion-$downloadPlatform.zip")
val expectedFile = "Geode.$downloadPlatform.so"
ZipInputStream(tempOutput.inputStream()).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.")
}
}
}
abstract class DownloadAndUnzipGeodeResourcesTask : DownloadGeodeFileTask() {
init {
platform.set("resources")
}
@get:Input
abstract val geodeVersion: Property<String>
@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<DownloadVersionMetadataTask>("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<DownloadAndUnzipGeodeTask>("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")
}
val downloadGeode64Task = tasks.register<DownloadAndUnzipGeodeTask>("downloadGeode64Binary") {
description = "Downloads the 64bit Geode release archive"
inputs.property("geodeVersion", currentGeodeVersion)
geodeVersion = currentGeodeVersion
platform = "android64"
metadata = fetchGeodeMetadataTask.flatMap { it.targetFile }
targetFile = temporaryDir.resolve("libgeode.so")
}
val downloadGeodeResourcesTask = tasks.register<DownloadAndUnzipGeodeResourcesTask>("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
@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<MergeGeodeFilesTask>("downloadGeodeFull") {
description = "Downloads both Geode releases"
binary32 = downloadGeode32Task.flatMap { it.targetFile }
binary64 = downloadGeode64Task.flatMap { it.targetFile }
outputDir = layout.buildDirectory.dir("geode-prebuilt")
}
android { android {
compileSdk = 37 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "com.geode.launcher" applicationId = "com.geode.launcher"
minSdk = 23 minSdk = 23
targetSdk = 37 targetSdk = 36
versionCode = 31 versionCode = 26
versionName = "1.9.0" versionName = "1.7.3"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
@@ -268,7 +39,7 @@ android {
splits { splits {
abi { abi {
isEnable = !project.hasProperty("disableAbiSplits") isEnable = true
reset() reset()
//noinspection ChromeOsAbiSupport. i'm sorry! //noinspection ChromeOsAbiSupport. i'm sorry!
@@ -278,24 +49,6 @@ android {
} }
} }
flavorDimensions += "distribution"
productFlavors {
create("standard") {
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\"")
}
}
buildTypes { buildTypes {
release { release {
isMinifyEnabled = true isMinifyEnabled = true
@@ -311,8 +64,8 @@ android {
// enables a polyfill for java Instant on api levels < 26 (used for updater) // enables a polyfill for java Instant on api levels < 26 (used for updater)
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_1_8
} }
buildFeatures { buildFeatures {
compose = true compose = true
@@ -325,51 +78,38 @@ 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")
} }
} }
namespace = "com.geode.launcher" namespace = "com.geode.launcher"
ndkVersion = "30.0.16248370" ndkVersion = "29.0.14206865"
}
androidComponents {
onVariants(selector().withFlavor("distribution" to "googlePlay")) { variant ->
variant.sources.jniLibs?.addGeneratedSourceDirectory(
fullDownloadTask,
MergeGeodeFilesTask::outputDir
)
variant.sources.assets?.addGeneratedSourceDirectory(
downloadGeodeResourcesTask,
DownloadAndUnzipGeodeResourcesTask::outputDir
)
}
} }
dependencies { dependencies {
implementation (platform("androidx.compose:compose-bom:2026.09.00")) implementation (platform("androidx.compose:compose-bom:$composeBOM"))
implementation ("androidx.core:core-ktx:1.19.1") 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.11.0") implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.11.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.8.0") 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.18.2") implementation ("com.squareup.okio:okio:3.16.4")
implementation ("com.squareup.okhttp3:okhttp:5.5.0") implementation ("com.squareup.okhttp3:okhttp:5.3.2")
implementation ("com.squareup.okhttp3:okhttp-coroutines:5.5.0") implementation ("com.squareup.okhttp3:okhttp-coroutines:5.3.2")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.11.0") implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.10.0")
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0") implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.46.0") implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.2")
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.46.0") 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")
} }
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
tools:node="remove" />
</manifest>
+6 -44
View File
@@ -17,6 +17,8 @@
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
@@ -101,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>
@@ -125,7 +127,7 @@
</activity> </activity>
<activity <activity
android:name=".GeometryDashActivity" android:name=".GeometryDashActivity"
android:configChanges="fontScale|layoutDirection|density|smallestScreenSize|screenSize|uiMode|screenLayout|orientation|navigation|keyboardHidden|keyboard|locale" android:configChanges="orientation|screenSize|smallestScreenSize"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:screenOrientation="landscape" android:screenOrientation="landscape"
@@ -193,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"
@@ -233,30 +215,10 @@
<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=".MainActivityGreen"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_green"
android:label="@string/app_name"
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>
<provider <provider
android:name=".UserDirectoryProvider" android:name=".UserDirectoryProvider"
android:authorities="${applicationId}.user" android:authorities="com.geode.launcher.user"
android:exported="true" android:exported="true"
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS"> android:permission="android.permission.MANAGE_DOCUMENTS">
@@ -266,7 +228,7 @@
</provider> </provider>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="com.geode.launcher.fileprovider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
+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...
@@ -2,106 +2,33 @@ package com.customRobTop
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.*
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.provider.Settings import android.provider.Settings
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri import androidx.core.net.toUri
import com.customRobTop.contracts.ImportFileActivityContract
import com.customRobTop.contracts.ShareFileActivityContract
import com.geode.launcher.R import com.geode.launcher.R
import com.geode.launcher.utils.DownloadUtils
import com.geode.launcher.utils.LaunchUtils
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.OutputStream
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.UUID import java.util.*
@Suppress("unused", "UNUSED_PARAMETER") @Suppress("unused", "UNUSED_PARAMETER")
object BaseRobTopActivity { object BaseRobTopActivity {
const val KEYBOARD_INPUT_MULTILINE = 1
const val KEYBOARD_INPUT_NUMERIC = 4
const val KEYBOARD_INPUT_PASSWORD = 2
const val LEVEL_EXTENSION: String = ".gdlvl"
private var isLoaded = false private var isLoaded = false
var blockBackButton = false var blockBackButton = false
private var keyboardActive = false private var keyboardActive = false
var curKeyboardInputFlags = 0
var isPaused = false var isPaused = false
lateinit var me: WeakReference<Activity> lateinit var me: WeakReference<Activity>
private var shouldResumeSound = true private var shouldResumeSound = true
private var pendingExportPaths = emptyList<String>() fun setCurrentActivity(currentActivity: Activity) {
private lateinit var importFileActivityContract: ActivityResultLauncher<Unit>
private lateinit var shareFileActivityContract: ActivityResultLauncher<String>
private lateinit var shareFolderActivityContract: ActivityResultLauncher<Uri?>
fun setCurrentActivity(currentActivity: AppCompatActivity) {
me = WeakReference(currentActivity) me = WeakReference(currentActivity)
importFileActivityContract = currentActivity.registerForActivityResult(ImportFileActivityContract()) { uris ->
val filePaths = copyPickedFiles(uris)
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
JniToCpp.filesPicked(filePaths.toTypedArray())
}
}
shareFileActivityContract = currentActivity.registerForActivityResult(ShareFileActivityContract()) { uri ->
val currentlyPending = pendingExportPaths
pendingExportPaths = emptyList()
val exportPath = currentlyPending.firstOrNull()
if (exportPath == null || uri == null) {
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
JniToCpp.fileShareFinished(false)
}
return@registerForActivityResult
}
val result = saveToDocument(exportPath, uri)
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
JniToCpp.fileShareFinished(result)
}
}
shareFolderActivityContract = currentActivity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
val currentlyPending = pendingExportPaths
pendingExportPaths = emptyList()
if (uri == null) {
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
JniToCpp.fileShareFinished(false)
}
return@registerForActivityResult
}
val result = saveToFolder(currentlyPending, uri) > 0
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
JniToCpp.fileShareFinished(result)
}
}
} }
@SuppressLint("HardwareIds") @SuppressLint("HardwareIds")
@@ -160,18 +87,6 @@ object BaseRobTopActivity {
} }
} }
@JvmStatic
fun getClipboardString(): String {
me.get()?.run {
val manager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = manager.primaryClip
if (clip != null && clip.itemCount > 0) {
return clip.getItemAt(0).coerceToText(this).toString()
}
}
return ""
}
@JvmStatic @JvmStatic
fun sendMail(subject: String, body: String, to: String) { fun sendMail(subject: String, body: String, to: String) {
me.get()?.runOnUiThread { me.get()?.runOnUiThread {
@@ -349,164 +264,4 @@ object BaseRobTopActivity {
level = DeprecationLevel.HIDDEN level = DeprecationLevel.HIDDEN
) )
fun logEvent(event: String) {} fun logEvent(event: String) {}
@JvmStatic
fun setKeyboardInputFlags(flags: Int) {
curKeyboardInputFlags = flags
}
@JvmStatic
fun pickImportFiles() {
try {
importFileActivityContract.launch(Unit)
} catch (_: ActivityNotFoundException) {
me.get()?.apply {
runOnUiThread {
Toast.makeText(
this,
R.string.no_activity_found,
Toast.LENGTH_SHORT,
).show()
}
}
}
}
private fun copyPickedFiles(uris: List<Uri>): List<String> {
val me = me.get() ?: return emptyList()
val importsDir = File(LaunchUtils.getSaveDirectory(me), "imports")
importsDir.mkdirs()
return uris.mapIndexedNotNull { index, uri ->
val displayName = displayName(uri)
?: "import_$index$LEVEL_EXTENSION"
if (!displayName.endsWith(LEVEL_EXTENSION)) {
return@mapIndexedNotNull null
}
var outputFile = File(importsDir, displayName)
var counter = 2
while (outputFile.exists()) {
outputFile = File(importsDir, "${counter}_$displayName")
counter++
}
try {
me.contentResolver.openInputStream(uri)?.use { input ->
outputFile.outputStream().use { output ->
DownloadUtils.copyFile(input, output)
}
outputFile.absolutePath
}
} catch (_: Exception) {
outputFile.delete()
null
}
}
}
private fun displayName(uri: Uri): String? {
me.get()?.contentResolver?.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursorQuery ->
if (cursorQuery.moveToFirst()) {
val displayName = cursorQuery.getString(0)
if (displayName.isNotEmpty()) {
return File(displayName).name
}
}
}
return null
}
@JvmStatic
fun shareFiles(files: Array<String>) {
pendingExportPaths = files.toList()
try {
if (files.size == 1) {
shareFileActivityContract.launch(files.first())
return
} else if (files.size > 1) {
shareFolderActivityContract.launch(null)
return
}
} catch (_: ActivityNotFoundException) {
me.get()?.apply {
runOnUiThread {
Toast.makeText(
this,
R.string.no_activity_found,
Toast.LENGTH_SHORT,
).show()
}
}
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
JniToCpp.fileShareFinished(false)
}
}
}
private fun saveToFolder(exportPaths: List<String>, treeUri: Uri): Int {
val uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri))
val me = me.get() ?: return 0
return exportPaths.count { path ->
val filename = File(path).name
val documentUri = DocumentsContract.createDocument(me.contentResolver, uri, "application/octet-stream", filename)
?: return@count false
saveToDocument(path, documentUri)
}
}
private fun saveToDocument(exportPath: String, uri: Uri): Boolean {
try {
FileInputStream(exportPath).use { inputStream ->
openOutput(uri).use { outputStream ->
if (outputStream != null) {
DownloadUtils.copyFile(inputStream, outputStream)
}
}
}
ensureLevelExtension(uri)
return true
} catch (_: IOException) {
return false
}
}
private fun openOutput(uri: Uri): OutputStream? {
val me = me.get() ?: return null
return try {
me.contentResolver.openOutputStream(uri, "wt")
} catch (_: IOException) {
me.contentResolver.openOutputStream(uri)
}
}
private fun ensureLevelExtension(uri: Uri) {
val displayName = displayName(uri)
if (displayName == null || displayName.endsWith(LEVEL_EXTENSION)) {
return
}
val me = me.get() ?: return
val extensionIdx = displayName.lastIndexOf(".gdlvl.")
val substring = if (extensionIdx < 0) {
displayName + LEVEL_EXTENSION
} else {
displayName.substring(0, extensionIdx + 6)
}
try {
DocumentsContract.renameDocument(me.contentResolver, uri, substring)
} catch (_: IOException) {
}
}
} }
@@ -55,16 +55,4 @@ object JniToCpp {
@JvmStatic @JvmStatic
external fun videoAdShowed() external fun videoAdShowed()
@JvmStatic
external fun filesPicked(files: Array<String>)
@JvmStatic
external fun fileShareFinished(success: Boolean)
@JvmStatic
external fun textInputCaretToEnd()
@JvmStatic
external fun textInputDone()
} }
@@ -1,40 +0,0 @@
package com.customRobTop.contracts
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
class ImportFileActivityContract : ActivityResultContract<Unit, List<Uri>>() {
override fun createIntent(
context: Context,
input: Unit
): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
}
override fun parseResult(
resultCode: Int,
intent: Intent?
): List<Uri> {
if (resultCode != Activity.RESULT_OK || intent == null) {
return emptyList()
}
val clipData = intent.clipData
return if (clipData != null) {
List(clipData.itemCount) { i -> clipData.getItemAt(i).uri }
} else {
val intentData = intent.data
if (intentData != null)
listOf(intentData)
else
emptyList()
}
}
}
@@ -1,40 +0,0 @@
package com.customRobTop.contracts
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.activity.result.contract.ActivityResultContract
import java.io.File
class ShareFileActivityContract : ActivityResultContract<String, Uri?>() {
override fun createIntent(
context: Context,
input: String
): Intent {
return Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/octet-stream"
putExtra(Intent.EXTRA_TITLE, File(input).name)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
putExtra(DocumentsContract.EXTRA_INITIAL_URI,
DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Download")
)
}
}
}
override fun parseResult(
resultCode: Int,
intent: Intent?
): Uri? {
if (resultCode != Activity.RESULT_OK || intent == null) {
return null
}
return intent.data
}
}
@@ -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,101 +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)
val gameVersionCode = GamePackageUtils.getGameVersionCode(packageManager)
if (gameVersionCode >= 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 (gameVersionCode >= 42L) {
mGLSurfaceView?.sendNewCocosEvents = true
}
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(
@@ -204,33 +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
)
} }
Cocos2dxGLSurfaceView.onKeyboardInsets(insets)
WindowInsets.CONSUMED WindowInsets.CONSUMED
} }
frameLayout.addView(baseView)
setContentView(frameLayout) setContentView(frameLayout)
} else { return frameLayout
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { }
baseView.setOnApplyWindowInsetsListener { view, insets ->
Cocos2dxGLSurfaceView.onKeyboardInsets(insets) return null
return@setOnApplyWindowInsetsListener view.onApplyWindowInsets(insets) }
}
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()
} }
setContentView(baseView) 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) {
@@ -240,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) {
@@ -301,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"
@@ -320,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)
@@ -330,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)
@@ -342,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
@@ -357,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)
}
} }
} }
@@ -393,73 +411,54 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
return "$sourceDir!/lib/$architecture/" return "$sourceDir!/lib/$architecture/"
} }
private fun loadGeodeLibrary() { @SuppressLint("UnsafeDynamicallyLoadedCode")
@Suppress("SENSELESS_COMPARISON") private suspend fun loadGeodeLibrary() {
if (BuildConfig.PREBUNDLED_GEODE != null) { // Load Geode if exists
// attempt to load an externally provided Geode first // bundling the object with the application allows for nicer backtraces
if (!loadExternalGeode()) { try {
copyGeodeResources() // put libgeode.so in jniLibs/armeabi-v7a to get this
System.loadLibrary("geode") System.loadLibrary("geode")
} return
} else { } catch (e: UnsatisfiedLinkError) {
// Load Geode if exists // but users may prefer it stored with data
// bundling the object with the application allows for nicer backtraces val geodeFilename = LaunchUtils.geodeFilename
try { val geodePath = File(filesDir.path, "launcher/$geodeFilename")
// put libgeode.so in jniLibs/armeabi-v7a to get this if (geodePath.exists()) {
System.loadLibrary("geode") System.load(geodePath.path)
return 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)!!
val copiedPath = File(filesDir, "copied")
if (copiedPath.exists()) {
copiedPath.deleteRecursively()
}
copiedPath.mkdir()
val copiedGeodePath = File(copiedPath, "Geode.so")
if (externalGeodePath.exists()) {
DownloadUtils.copyFile(
externalGeodePath,
copiedGeodePath
)
if (copiedGeodePath.exists()) {
println("Loading Geode from ${externalGeodePath.name}")
System.load(copiedGeodePath.path)
return
} }
} }
throw e
} }
} }
@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()) {
println("Loading Geode from ${geodePath.path}")
System.load(geodePath.path)
return true
}
// 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 copiedPath = File(filesDir.path, "copied")
if (copiedPath.exists()) {
copiedPath.deleteRecursively()
}
copiedPath.mkdir()
val copiedGeodePath = File(copiedPath.path, "Geode.so")
if (externalGeodePath.exists()) {
DownloadUtils.copyFile(
FileInputStream(externalGeodePath),
FileOutputStream(copiedGeodePath)
)
if (copiedGeodePath.exists()) {
// android 17 fix
copiedGeodePath.setWritable(false)
println("Loading Geode from ${externalGeodePath.name}")
System.load(copiedGeodePath.path)
return true
}
}
return false
}
private fun createView(): FrameLayout { private fun createView(): FrameLayout {
val frameLayoutParams = ViewGroup.LayoutParams( val frameLayoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@@ -511,10 +510,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
glSurfaceView.cocos2dxEditText = editText glSurfaceView.cocos2dxEditText = editText
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
glSurfaceView.defaultFocusHighlightEnabled = false
}
return frameLayout return frameLayout
} }
@@ -661,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
} }
} }
@@ -701,56 +692,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
} }
private fun copyGeodeResources() {
@Suppress("SENSELESS_COMPARISON")
if (BuildConfig.PREBUNDLED_GEODE == null) {
return
}
val resourcesDirectory = LaunchUtils.getGeodeResourcesDirectory(this)
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
}
if (resourcesDirectory.exists()) {
resourcesDirectory.deleteRecursively()
}
resourcesDirectory.mkdirs()
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 { class EGLConfigChooser : GLSurfaceView.EGLConfigChooser {
// this comes from EGL14, but is unavailable on EGL10 // this comes from EGL14, but is unavailable on EGL10
// also EGL14 is incompatible with EGL10. so whatever // also EGL14 is incompatible with EGL10. so whatever
@@ -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
@@ -104,25 +99,13 @@ class MainActivity : ComponentActivity() {
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager) val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
val geodeInstalled = LaunchUtils.isGeodeInstalled(this) val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
val restartImmediately = intent.getBooleanExtra("restarted", false) if (gdInstalled && geodeInstalled && intent.getBooleanExtra("restarted", false)) {
if (gdInstalled && geodeInstalled && restartImmediately) { onLaunch(this)
val withSafeMode = intent.getBooleanExtra("safe-mode", false)
onLaunch(this, withSafeMode)
} }
enableEdgeToEdge() enableEdgeToEdge()
clearDownloadedApks(this) 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 returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE) val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
@@ -219,7 +202,7 @@ fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): Lau
) )
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo( LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_error), title = stringResource(R.string.launcher_cancelled_error),
details = stringResource(R.string.launcher_cancelled_outdated, GamePackageUtils.getSupportedUnifiedVersion()) details = stringResource(R.string.launcher_cancelled_outdated)
) )
LaunchViewModel.LaunchCancelReason.AUTOMATIC, LaunchViewModel.LaunchCancelReason.AUTOMATIC,
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo( LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
@@ -268,7 +251,7 @@ fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Bool
) )
} }
is LaunchViewModel.LaunchUIState.Cancelled -> { is LaunchViewModel.LaunchUIState.Cancelled -> {
mapCancelReasonToInfo(state.reason) return mapCancelReasonToInfo(state.reason)
} }
} }
} }
@@ -469,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")
@@ -562,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(
@@ -618,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
@@ -633,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)
@@ -644,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)
@@ -658,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)
@@ -728,9 +632,6 @@ fun GeodeUpdateIndicator(snackbarHostState: SnackbarHostState, onRetry: () -> Un
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource( ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
R.string.release_fetch_external_in_use_short R.string.release_fetch_external_in_use_short
) )
ReleaseManager.UpdateException.Reason.HASH_VALIDATION_FAILED -> stringResource(
R.string.release_fetch_hash_validation_failed_short
)
else -> stringResource(R.string.release_fetch_generic_short) else -> stringResource(R.string.release_fetch_generic_short)
} }
} }
@@ -792,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),
@@ -840,11 +734,10 @@ fun AltMainScreen(
) )
// only show launcher update in a case where a user won't see it ingame // only show launcher update in a case where a user won't see it ingame
val currentUIState = launchUIState if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
if (currentUIState is LaunchViewModel.LaunchUIState.Cancelled) {
val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) } val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) }
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING && currentUIState.reason != LaunchViewModel.LaunchCancelReason.GAME_OUTDATED) { if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
UnsupportedVersionWarning() UnsupportedVersionWarning()
} }
@@ -10,10 +10,7 @@ import android.provider.DocumentsContract
import android.provider.DocumentsProvider import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.geode.launcher.utils.ApplicationIcon
import com.geode.launcher.utils.IconUtils
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import java.io.File import java.io.File
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf( private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
@@ -63,17 +60,8 @@ class UserDirectoryProvider : DocumentsProvider() {
DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
DocumentsContract.Root.FLAG_SUPPORTS_SEARCH DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
) )
add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.app_name))
val selectedIconKey = PreferenceUtils.get(context).getString(PreferenceUtils.Key.SELECTED_ICON) add(DocumentsContract.Root.COLUMN_ICON, R.drawable.geode_logo)
?: "geode"
val selectedIcon = ApplicationIcon.fromId(selectedIconKey)
val iconDetails = IconUtils.getIconDetails(selectedIcon)
val nameId = if (selectedIcon == ApplicationIcon.SAPPHIRE) R.string.application_icon_sapphire else R.string.app_name
val logoId = if (selectedIcon == ApplicationIcon.GEODE) R.drawable.geode_logo else iconDetails.iconId
add(DocumentsContract.Root.COLUMN_TITLE, context.getString(nameId))
add(DocumentsContract.Root.COLUMN_ICON, logoId)
} }
return result return result
@@ -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)
@@ -202,11 +172,7 @@ fun onShowLogs(context: Context) {
fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) { fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
val context = LocalContext.current val context = LocalContext.current
val currentRelease = remember { val currentRelease = remember {
@Suppress("SENSELESS_COMPARISON") PreferenceUtils.get(context).getString(PreferenceUtils.Key.CURRENT_VERSION_TAG)
if (BuildConfig.PREBUNDLED_GEODE != null)
"${BuildConfig.PREBUNDLED_GEODE}*"
else
PreferenceUtils.get(context).getString(PreferenceUtils.Key.CURRENT_VERSION_TAG)
} }
val gameVersion = remember { val gameVersion = remember {
@@ -289,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
@@ -36,7 +36,6 @@ import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlin.time.Duration.Companion.milliseconds
@Composable @Composable
@@ -143,7 +142,7 @@ fun onSettings(context: Context) {
} }
@Composable @Composable
fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, content: @Composable RowScope.() -> Unit) { fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, enabled: Boolean = true, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit) {
// compose apis don't provide a good way of adding long press to a button // compose apis don't provide a good way of adding long press to a button
var isLongPress by remember { mutableStateOf(false) } var isLongPress by remember { mutableStateOf(false) }
@@ -155,11 +154,11 @@ fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, modifier: Modi
interactionSource.interactions.collectLatest { interaction -> interactionSource.interactions.collectLatest { interaction ->
when (interaction) { when (interaction) {
is PressInteraction.Press -> { is PressInteraction.Press -> {
delay(viewConfiguration.longPressTimeoutMillis.milliseconds) delay(viewConfiguration.longPressTimeoutMillis)
// perform a second delay to make the action more obvious // perform a second delay to make the action more obvious
haptics.performHapticFeedback(HapticFeedbackType.LongPress) haptics.performHapticFeedback(HapticFeedbackType.LongPress)
delay(viewConfiguration.longPressTimeoutMillis.milliseconds) delay(viewConfiguration.longPressTimeoutMillis)
haptics.performHapticFeedback(HapticFeedbackType.LongPress) haptics.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -187,8 +186,6 @@ fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, modifier: Modi
@Composable @Composable
fun UnsupportedVersionWarning(modifier: Modifier = Modifier) { fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
val gameVersion = GamePackageUtils.getSupportedUnifiedVersion()
InlineDialog( InlineDialog(
headline = { headline = {
Icon( Icon(
@@ -204,7 +201,7 @@ fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
}, },
body = { body = {
Text( Text(
stringResource(R.string.launcher_game_update_required, gameVersion), stringResource(R.string.launcher_game_update_required),
) )
}, },
modifier = modifier.padding(8.dp) modifier = modifier.padding(8.dp)
@@ -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
@@ -39,8 +42,6 @@ import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.GeodeUtils import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
enum class LaunchNotificationType { enum class LaunchNotificationType {
GEODE_UPDATED, LAUNCHER_UPDATE_AVAILABLE, UPDATE_FAILED, UNSUPPORTED_VERSION, FAILED_LOAD; GEODE_UPDATED, LAUNCHER_UPDATE_AVAILABLE, UPDATE_FAILED, UNSUPPORTED_VERSION, FAILED_LOAD;
@@ -89,7 +90,7 @@ fun NotificationCardFromType(type: LaunchNotificationType) {
} }
AnimatedNotificationCard( AnimatedNotificationCard(
displayLength = 5.seconds, displayLength = 5000L,
onClick = { onClick = {
showInfoDialog = true showInfoDialog = true
} }
@@ -122,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(
@@ -142,15 +144,15 @@ fun LaunchNotification(cards: List<LaunchNotificationType>) {
} }
@Composable @Composable
fun AnimatedNotificationCard(modifier: Modifier = Modifier, visibilityDelay: Duration = Duration.ZERO, displayLength: Duration = 3.seconds, onClick: (() -> Unit)? = null, contents: @Composable () -> Unit) { fun AnimatedNotificationCard(modifier: Modifier = Modifier, visibilityDelay: Long = 0L, displayLength: Long = 3000L, onClick: (() -> Unit)? = null, contents: @Composable () -> Unit) {
val state = remember { val state = remember {
MutableTransitionState(false).apply { MutableTransitionState(false).apply {
targetState = visibilityDelay <= Duration.ZERO targetState = visibilityDelay <= 0
} }
} }
LaunchedEffect(true) { LaunchedEffect(true) {
if (visibilityDelay > Duration.ZERO) { if (visibilityDelay > 0) {
delay(visibilityDelay) delay(visibilityDelay)
state.targetState = true state.targetState = true
} }
@@ -282,3 +284,26 @@ 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()
}
}
}
}
}
@@ -7,7 +7,6 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.geode.launcher.BuildConfig
import com.geode.launcher.updater.ReleaseManager import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.GamePackageUtils import com.geode.launcher.utils.GamePackageUtils
@@ -183,9 +182,18 @@ class LaunchViewModel(private val application: Application): ViewModel() {
return return
} }
// play store build only comes with the one build of geode // last launch crashed, so cancel
val minVersionCode = if (BuildConfig.GOOGLE_PLAY_BUILD) Constants.SUPPORTED_VERSION_CODE else Constants.SUPPORTED_VERSION_CODE_MIN if (loadFailure != null) {
if (GamePackageUtils.getGameVersionCode(packageManager) < minVersionCode) { // 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) {
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED)) _uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
return return
} }
@@ -197,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
} }
@@ -251,31 +247,25 @@ class LaunchViewModel(private val application: Application): ViewModel() {
initReadyTimer() initReadyTimer()
} }
if (!BuildConfig.GOOGLE_PLAY_BUILD) { val releaseManager = ReleaseManager.get(application)
val releaseManager = ReleaseManager.get(application)
val hasGeode = LaunchUtils.isGeodeInstalled(application) val hasGeode = LaunchUtils.isGeodeInstalled(application)
val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY) val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) { if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) {
releaseManager.checkForUpdates(false) releaseManager.checkForUpdates(false)
} else { } else {
releaseManager.afterUseCachedUpdate() releaseManager.afterUseCachedUpdate()
} }
if (isRestart) { if (isRestart) {
releaseManager.dismissedLauncherUpdate = true releaseManager.dismissedLauncherUpdate = true
}
} }
preReadyCheck() preReadyCheck()
} }
fun retryUpdate() { fun retryUpdate() {
if (BuildConfig.GOOGLE_PLAY_BUILD) {
return
}
ReleaseManager.get(application).checkForUpdates(true) ReleaseManager.get(application).checkForUpdates(true)
if (ReleaseManager.get(application).isInUpdate) { if (ReleaseManager.get(application).isInUpdate) {
restoreUpdateState = _uiState.value restoreUpdateState = _uiState.value
@@ -4,6 +4,7 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -408,8 +409,7 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
} }
} }
// there's like a single pixel of the shadow that gets clipped without this... ElevatedCard(modifier) {
ElevatedCard(modifier = modifier.padding(bottom = 1.dp)) {
Column( Column(
// buttons add enough padding already, lower to compensate // buttons add enough padding already, lower to compensate
modifier = Modifier.padding( modifier = Modifier.padding(
@@ -129,7 +129,7 @@ fun onSelectIcon(context: Context, selectedIcon: ApplicationIconDetails) {
val iconData = IconUtils.getIconDetails(it) val iconData = IconUtils.getIconDetails(it)
packageManager.setComponentEnabledSetting( packageManager.setComponentEnabledSetting(
ComponentName(context, "${context.packageName.removeSuffix(".play")}.${iconData.component}"), ComponentName(context, "${context.packageName}.${iconData.component}"),
if (iconData.id == selectedIcon.id) if (iconData.id == selectedIcon.id)
PackageManager.COMPONENT_ENABLED_STATE_ENABLED PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
@@ -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)
) { ) {
@@ -40,7 +40,6 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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 com.geode.launcher.BuildConfig
import com.geode.launcher.R import com.geode.launcher.R
import com.geode.launcher.preferences.components.OptionsButton import com.geode.launcher.preferences.components.OptionsButton
import com.geode.launcher.preferences.components.OptionsGroup import com.geode.launcher.preferences.components.OptionsGroup
@@ -198,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,
@@ -205,30 +218,28 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
) )
} }
if (!BuildConfig.GOOGLE_PLAY_BUILD) { OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
OptionsGroup(title = stringResource(R.string.preference_category_updater)) { val context = LocalContext.current
val context = LocalContext.current
SettingsSelectCard( SettingsSelectCard(
title = stringResource(R.string.preference_release_channel_tag_name), title = stringResource(R.string.preference_release_channel_tag_name),
dialogTitle = stringResource(R.string.preference_release_channel_select), dialogTitle = stringResource(R.string.preference_release_channel_select),
preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL_TAG, preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL_TAG,
options = linkedMapOf( options = linkedMapOf(
0 to stringResource(R.string.preference_release_channel_stable), 0 to stringResource(R.string.preference_release_channel_stable),
1 to stringResource(R.string.preference_release_channel_beta), 1 to stringResource(R.string.preference_release_channel_beta),
2 to stringResource(R.string.preference_release_channel_nightly), 2 to stringResource(R.string.preference_release_channel_nightly),
), ),
extraSelectBehavior = { extraSelectBehavior = {
PreferenceUtils.get(context) PreferenceUtils.get(context)
.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, 0L) .setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, 0L)
} }
) )
SettingsCard( SettingsCard(
title = stringResource(R.string.preference_disable_update_cache), title = stringResource(R.string.preference_disable_update_cache),
preferenceKey = PreferenceUtils.Key.DISABLE_UPDATE_CACHE preferenceKey = PreferenceUtils.Key.DISABLE_UPDATE_CACHE
) )
}
} }
OptionsGroup(title = stringResource(R.string.preference_category_testing)) { OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
@@ -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)
} }
@@ -70,6 +70,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.BuildConfig import com.geode.launcher.BuildConfig
import com.geode.launcher.MainActivity
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.components.BackupButton import com.geode.launcher.preferences.components.BackupButton
@@ -496,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()
@@ -509,10 +510,6 @@ fun GameplaySettingsGroup() {
@Composable @Composable
fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) { fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) {
if (BuildConfig.GOOGLE_PLAY_BUILD) {
return
}
val updateStatus by releaseViewModel.uiState.collectAsState() val updateStatus by releaseViewModel.uiState.collectAsState()
OptionsGroup(stringResource(R.string.preference_category_updater)) { OptionsGroup(stringResource(R.string.preference_category_updater)) {
@@ -584,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 = {
@@ -664,14 +647,11 @@ fun AboutSettingsGroup(developerModeEnabled: Boolean) {
) )
} }
@Suppress("SENSELESS_COMPARISON")
OptionsLabel( OptionsLabel(
stringResource(R.string.preference_loader_version_name), stringResource(R.string.preference_loader_version_name),
stringResource( stringResource(
R.string.preference_loader_version_description, R.string.preference_loader_version_description,
if (BuildConfig.PREBUNDLED_GEODE != null) currentRelease ?: "unknown"
"${BuildConfig.PREBUNDLED_GEODE}*"
else (currentRelease ?: "unknown")
), ),
displayInline = true displayInline = true
) )
@@ -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
)
@@ -3,9 +3,10 @@ package com.geode.launcher.updater
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import kotlin.time.Instant import kotlin.time.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.time.ExperimentalTime
@Serializable @Serializable
data class Asset( data class Asset @OptIn(ExperimentalTime::class) constructor(
val url: String, val url: String,
val id: Int, val id: Int,
val name: String, val name: String,
@@ -13,11 +14,10 @@ data class Asset(
val createdAt: Instant, val createdAt: Instant,
val updatedAt: Instant, val updatedAt: Instant,
val browserDownloadUrl: String, val browserDownloadUrl: String,
val digest: String?,
) )
@Serializable @Serializable
data class Release( data class Release @OptIn(ExperimentalTime::class) constructor(
val url: String, val url: String,
val id: Int, val id: Int,
val targetCommitish: String, val targetCommitish: String,
@@ -31,29 +31,12 @@ data class Release(
) )
@Serializable @Serializable
data class LoaderPlatformDownload( data class LoaderVersion @OptIn(ExperimentalTime::class) constructor(
val url: String,
val hash: String?,
)
@Serializable
data class LoaderDownload(
val win: LoaderPlatformDownload,
val mac: LoaderPlatformDownload,
val android32: LoaderPlatformDownload,
val android64: LoaderPlatformDownload,
val ios: LoaderPlatformDownload,
val resources: LoaderPlatformDownload,
)
@Serializable
data class LoaderVersion(
val tag: String, val tag: String,
val version: String, val version: String,
val createdAt: Instant, val createdAt: Instant,
val commitHash: String, val commitHash: String,
val prerelease: Boolean, val prerelease: Boolean
val downloads: LoaderDownload,
) )
@Serializable @Serializable
@@ -62,25 +45,7 @@ data class LoaderPayload<T>(
val error: String val error: String
) )
data class DownloadableAsset( data class DownloadableAsset(val url: String, val filename: String, val size: Long? = null)
val url: String,
val filename: String,
val size: Long? = null,
val hash: String? = null,
)
private fun mapDownload(download: Asset): DownloadableAsset {
val hash = if (download.digest?.startsWith("sha256:") == true)
download.digest.removePrefix("sha256:")
else null
return DownloadableAsset(
url = download.browserDownloadUrl,
filename = download.name,
size = download.size.toLong(),
hash = hash,
)
}
abstract class Downloadable { abstract class Downloadable {
/** /**
@@ -97,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() {
@@ -118,6 +78,7 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
return release.tagName return release.tagName
} }
@OptIn(ExperimentalTime::class)
override fun getDescriptor(): Long { override fun getDescriptor(): Long {
return release.createdAt.epochSeconds return release.createdAt.epochSeconds
} }
@@ -134,15 +95,11 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
override fun getDownload(): DownloadableAsset? { override fun getDownload(): DownloadableAsset? {
val download = getGitHubDownload() ?: return null val download = getGitHubDownload() ?: return null
return mapDownload(download) return DownloadableAsset(
} url = download.browserDownloadUrl,
filename = download.name,
override fun getResourcesDownload(): DownloadableAsset? { size = download.size.toLong()
val download = release.assets.find { )
it.name == "resources.zip"
} ?: return null
return mapDownload(download)
} }
} }
@@ -151,6 +108,7 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
return release.tagName return release.tagName
} }
@OptIn(ExperimentalTime::class)
override fun getDescriptor(): Long { override fun getDescriptor(): Long {
return release.createdAt.epochSeconds return release.createdAt.epochSeconds
} }
@@ -168,17 +126,18 @@ 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
} }
} }
override fun getDownload(): DownloadableAsset? { override fun getDownload(): DownloadableAsset? {
val download = getGitHubDownload() ?: return null val download = getGitHubDownload() ?: return null
return mapDownload(download) return DownloadableAsset(
} url = download.browserDownloadUrl,
filename = download.name,
override fun getResourcesDownload(): DownloadableAsset? { size = download.size.toLong()
return null )
} }
} }
@@ -187,27 +146,16 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab
return version.tag return version.tag
} }
@OptIn(ExperimentalTime::class)
override fun getDescriptor(): Long { override fun getDescriptor(): Long {
return version.createdAt.epochSeconds return version.createdAt.epochSeconds
} }
override fun getDownload(): DownloadableAsset { override fun getDownload(): DownloadableAsset? {
val data = if (LaunchUtils.is64bit) version.downloads.android64 else version.downloads.android32
val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip" val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
return DownloadableAsset( return DownloadableAsset(
url = data.url, url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename",
filename = filename, filename = filename
hash = data.hash,
)
}
override fun getResourcesDownload(): DownloadableAsset {
val data = version.downloads.resources
val filename = "resources.zip"
return DownloadableAsset(
url = data.url,
filename = filename,
hash = data.hash,
) )
} }
} }
@@ -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.InterruptedIOException import java.io.InterruptedIOException
import java.io.IOException
import kotlin.math.max
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
@@ -75,8 +71,7 @@ class ReleaseManager private constructor(
class UpdateException(reason: Reason? = null, cause: Throwable? = null) : Exception(reason?.name, cause) { class UpdateException(reason: Reason? = null, cause: Throwable? = null) : Exception(reason?.name, cause) {
enum class Reason { enum class Reason {
EXTERNAL_FILE_IN_USE, EXTERNAL_FILE_IN_USE
HASH_VALIDATION_FAILED
} }
var reason: Reason? = reason var reason: Reason? = reason
@@ -99,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
@@ -156,17 +148,17 @@ class ReleaseManager private constructor(
val outputFile = File(outputDirectory, download.filename) val outputFile = File(outputDirectory, download.filename)
try { try {
val hash = DownloadUtils.downloadFile( DownloadUtils.downloadStream(
httpClient, httpClient,
download.url, download.url,
outputFile onResponse = { body ->
body.source().use { source ->
outputFile.sink().buffer().use { sink ->
sink.writeAll(source)
}
}
}
) )
if (download.hash != null && download.hash != hash) {
Log.w("ReleaseManager", "downloadLauncherUpdate failed: found $hash but expected ${download.hash}")
outputFile.delete()
throw UpdateException(UpdateException.Reason.HASH_VALIDATION_FAILED)
}
} catch (e: Exception) { } catch (e: Exception) {
return Result.failure(e) return Result.failure(e)
} }
@@ -174,70 +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)
val downloadFile = getTempFile("resources.zip")
try {
if (!outputDir.exists()) {
outputDir.mkdirs()
}
val hash = DownloadUtils.downloadFile(
httpClient,
resourceAsset.url,
downloadFile,
onProgress = { progress, outOf ->
if (!skipStateUpdate)
_uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize)
},
)
if (resourceAsset.hash != null && resourceAsset.hash != hash) {
Log.w("ReleaseManager", "performResourceDownload failed: found $hash but expected ${resourceAsset.hash}")
throw UpdateException(UpdateException.Reason.HASH_VALIDATION_FAILED)
}
downloadFile.inputStream().use { zip ->
DownloadUtils.copyZipStreamToDirectory(
zip,
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)
try {
if (downloadFile.exists()) downloadFile.delete()
if (tempPathClone.exists()) tempPathClone.deleteRecursively()
} catch (_: IOException) {}
}
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")
@@ -246,23 +175,15 @@ 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(LaunchUtils.geodeFilename) 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
val tempFilePath = outputFile.path val tempFilePath = outputFile.path
val geodeFile = getGeodeOutputPath() val geodeFile = getGeodeOutputPath()
val downloadFile = getTempFile("geode.zip")
try { try {
// tempFile should be in same path as geodeFile // tempFile should be in same path as geodeFile
val geodeParent = geodeFile.parentFile val geodeParent = geodeFile.parentFile
@@ -270,58 +191,41 @@ class ReleaseManager private constructor(
geodeParent.mkdirs() geodeParent.mkdirs()
} }
val hash = DownloadUtils.downloadFile( DownloadUtils.downloadStream(
httpClient, httpClient,
releaseAsset.url, releaseAsset.url,
downloadFile,
onProgress = { progress, outOf -> onProgress = { progress, outOf ->
if (!skipStateUpdate) _uiState.value = ReleaseManagerState.InDownload(progress, outOf)
_uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize) },
onResponse = { body ->
DownloadUtils.extractFileFromZipStream(
body.byteStream(),
outputFile,
geodeFile.name
)
releaseSize = max(outOf, releaseSize) // work around a permission issue from adb push
if (geodeFile.exists()) {
geodeFile.delete()
}
val renameSuccessful = runCatching {
outputFile.renameTo(geodeFile)
}.getOrDefault(false)
if (!renameSuccessful) {
// attempt a manual copy if rename fails for whatever reason
DownloadUtils.copyFile(outputFile, geodeFile)
}
} }
) )
if (releaseAsset.hash != null && releaseAsset.hash != hash) {
Log.w("ReleaseManager", "performUpdate failed: found $hash but expected ${releaseAsset.hash}")
throw UpdateException(UpdateException.Reason.HASH_VALIDATION_FAILED)
}
downloadFile.inputStream().use { zip ->
DownloadUtils.extractFileFromZipStream(
zip,
outputFile,
geodeFile.name
)
}
// work around a permission issue from adb push
if (geodeFile.exists()) {
geodeFile.delete()
}
val renameSuccessful = runCatching {
outputFile.renameTo(geodeFile)
}.getOrDefault(false)
if (!renameSuccessful) {
// attempt a manual copy if rename fails for whatever reason
DownloadUtils.copyFile(outputFile, geodeFile)
}
} catch (e: Exception) { } catch (e: Exception) {
sendError(e) sendError(e)
return return
} finally { } finally {
val tempFileClone = File(tempFilePath) val tempFileClone = File(tempFilePath)
try { runCatching {
if (downloadFile.exists()) downloadFile.delete()
if (tempFileClone.exists()) tempFileClone.delete() if (tempFileClone.exists()) tempFileClone.delete()
} catch (_: IOException) {}
}
if (resourcesAsset != null) {
if (!performResourceDownload(resourcesAsset, releaseSize)) {
return
} }
} }
@@ -345,18 +249,14 @@ 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
} }
private suspend fun getLatestLauncherUpdate(): DownloadableLauncherRelease? { private suspend fun getLatestLauncherUpdate(): DownloadableLauncherRelease? {
if (BuildConfig.GOOGLE_PLAY_BUILD) {
// don't check for updates on Google Play builds
return null
}
return try { return try {
releaseRepository.getLatestLauncherRelease() releaseRepository.getLatestLauncherRelease()
} catch (e: Exception) { } catch (e: Exception) {
@@ -366,8 +266,6 @@ class ReleaseManager private constructor(
} }
suspend fun checkLauncherUpdate() { suspend fun checkLauncherUpdate() {
if (BuildConfig.GOOGLE_PLAY_BUILD) return
val launcherRelease = getLatestLauncherUpdate() ?: return val launcherRelease = getLatestLauncherUpdate() ?: return
// unconditionally track it, we're not using it for checks and it might be good to have // unconditionally track it, we're not using it for checks and it might be good to have
@@ -384,8 +282,6 @@ class ReleaseManager private constructor(
} }
fun checkCachedLauncherUpdate() { fun checkCachedLauncherUpdate() {
if (BuildConfig.GOOGLE_PLAY_BUILD) return
val lastUpdate = PreferenceUtils.get(applicationContext) val lastUpdate = PreferenceUtils.get(applicationContext)
.getString(PreferenceUtils.Key.LAST_LAUNCHER_UPDATE) ?: return .getString(PreferenceUtils.Key.LAST_LAUNCHER_UPDATE) ?: return
@@ -395,12 +291,6 @@ class ReleaseManager private constructor(
} }
suspend fun downloadLatestLauncherUpdate(): Result<File> { suspend fun downloadLatestLauncherUpdate(): Result<File> {
if (BuildConfig.GOOGLE_PLAY_BUILD) {
return Result.failure(UnsupportedOperationException(
"Launcher updates are not supported on Google Play builds"
))
}
val update = _availableLauncherUpdateDetails.value val update = _availableLauncherUpdateDetails.value
?: getLatestLauncherUpdate() ?: getLatestLauncherUpdate()
?: return Result.failure(FileNotFoundException()) ?: return Result.failure(FileNotFoundException())
@@ -409,10 +299,6 @@ class ReleaseManager private constructor(
} }
fun shouldUseCache(): Boolean { fun shouldUseCache(): Boolean {
if (BuildConfig.GOOGLE_PLAY_BUILD) {
return true
}
val sharedPreferences = PreferenceUtils.get(applicationContext) val sharedPreferences = PreferenceUtils.get(applicationContext)
if (sharedPreferences.getBoolean(PreferenceUtils.Key.DISABLE_UPDATE_CACHE)) { if (sharedPreferences.getBoolean(PreferenceUtils.Key.DISABLE_UPDATE_CACHE)) {
return false return false
@@ -431,11 +317,6 @@ class ReleaseManager private constructor(
} }
private suspend fun checkForNewRelease(isManual: Boolean = false) { private suspend fun checkForNewRelease(isManual: Boolean = false) {
if (BuildConfig.GOOGLE_PLAY_BUILD) {
_uiState.value = ReleaseManagerState.Finished()
return
}
val sharedPreferences = PreferenceUtils.get(applicationContext) val sharedPreferences = PreferenceUtils.get(applicationContext)
if (!isManual && shouldUseCache()) { if (!isManual && shouldUseCache()) {
@@ -503,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
@@ -519,40 +394,25 @@ class ReleaseManager private constructor(
return File(geodeDirectory, geodeName) return File(geodeDirectory, geodeName)
} }
private fun createRandomString(): String { private fun getTempFile(): File {
val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9') val geodeName = LaunchUtils.geodeFilename
return buildString(8) {
repeat(8) { append(alphabet.random()) }
}
}
private fun getTempFile(filename: String): File {
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext) val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
// 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 tmpName = "tmp-$suffix.$filename" val suffix = buildString(8) {
repeat(8) { append(alphabet.random()) }
}
val tmpName = "tmp-$suffix.$geodeName"
val tempFile = File(geodeDirectory, tmpName) val tempFile = File(geodeDirectory, tmpName)
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.
*/ */
@@ -7,7 +7,6 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.AP
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.geode.launcher.BuildConfig
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -90,10 +89,6 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
} }
fun runReleaseCheck(isManual: Boolean = false) { fun runReleaseCheck(isManual: Boolean = false) {
if (BuildConfig.GOOGLE_PLAY_BUILD) {
return
}
hasPerformedCheck = true hasPerformedCheck = true
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -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
@@ -17,12 +17,10 @@ import okio.BufferedSource
import okio.FileSystem import okio.FileSystem
import okio.ForwardingSink import okio.ForwardingSink
import okio.ForwardingSource import okio.ForwardingSource
import okio.HashingSink
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import okio.Sink import okio.Sink
import okio.Source import okio.Source
import okio.buffer import okio.buffer
import okio.sink
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@@ -34,31 +32,12 @@ import java.util.zip.ZipInputStream
typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit
object DownloadUtils { object DownloadUtils {
suspend fun downloadFile(
httpClient: OkHttpClient,
url: String,
outputFile: File,
onProgress: ProgressCallback? = null,
): String = downloadStream(
httpClient,
url,
onProgress = onProgress
).use { body ->
val hashingSink = HashingSink.sha256(outputFile.sink().buffer())
hashingSink.use { sink ->
body.source().use { source ->
source.readAll(sink)
}
}
hashingSink.hash.hex()
}
suspend fun downloadStream( suspend fun downloadStream(
httpClient: OkHttpClient, httpClient: OkHttpClient,
url: String, url: String,
onProgress: ProgressCallback? = null, onProgress: ProgressCallback? = null,
): ResponseBody { onResponse: suspend (ResponseBody) -> Unit,
) {
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.build() .build()
@@ -85,42 +64,10 @@ object DownloadUtils {
val progressClient = progressClientBuilder.build() val progressClient = progressClientBuilder.build()
val call = progressClient.newCall(request) val call = progressClient.newCall(request)
val response = call.executeAsync() call.executeAsync().use { response ->
when (response.code) {
val responseCode = response.code 200 -> onResponse(response.body)
if (responseCode != 200) { else -> throw IOException("unexpected response ${response.code}")
response.close()
throw IOException("unexpected response ${response.code}")
}
return response.body
}
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
} }
} }
} }
@@ -184,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>
@@ -8,7 +8,6 @@ import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import java.io.File import java.io.File
import androidx.core.net.toUri
class FileUtils { class FileUtils {
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android // copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
@@ -87,7 +86,7 @@ class FileUtils {
val file = File(id) val file = File(id)
if (file.exists()) return id if (file.exists()) return id
} }
val contentUri = ContentUris.withAppendedId("content://downloads/public_downloads".toUri(), java.lang.Long.valueOf(id)) val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null) return getDataColumn(context, contentUri, null, null)
} }
@@ -53,7 +53,6 @@ object GamePackageUtils {
39L to "2.206", 39L to "2.206",
40L to "2.2074", // ok 40L to "2.2074", // ok
41L to "2.2081", 41L to "2.2081",
42L to "2.209",
) )
/** /**
@@ -64,10 +63,6 @@ object GamePackageUtils {
return gameVersionMap[versionCode] ?: getGameVersionString(packageManager) 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 { fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean {
val abi = LaunchUtils.applicationArchitecture val abi = LaunchUtils.applicationArchitecture
val isExtracted = val isExtracted =
@@ -32,6 +32,7 @@ import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
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.activityresult.GeodeOpenFileActivityResult import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
@@ -191,16 +192,10 @@ object GeodeUtils {
@JvmStatic @JvmStatic
fun restartGame() { fun restartGame() {
restartGame(false)
}
@JvmStatic
fun restartGame(safeMode: Boolean) {
activity.get()?.run { activity.get()?.run {
packageManager.getLaunchIntentForPackage(packageName)?.also { packageManager.getLaunchIntentForPackage(packageName)?.also {
val mainIntent = Intent.makeRestartActivityTask(it.component) val mainIntent = Intent.makeRestartActivityTask(it.component)
mainIntent.putExtra("restarted", true) mainIntent.putExtra("restarted", true)
mainIntent.putExtra("safe-mode", safeMode)
startActivity(mainIntent) startActivity(mainIntent)
exitProcess(0) exitProcess(0)
} }
@@ -220,15 +215,13 @@ object GeodeUtils {
val relativePath = pathFile.relativeTo(baseDirectory) val relativePath = pathFile.relativeTo(baseDirectory)
val uri = DocumentsContract.buildDocumentUri( val uri = DocumentsContract.buildDocumentUri(
"com.android.externalstorage.documents", "com.android.externalstorage.documents",
if (BuildConfig.GOOGLE_PLAY_BUILD) { "primary:Android/media/${context.packageName}/$relativePath"
"primary:Android/data/${context.packageName}/$relativePath"
} else {
"primary:Android/media/${context.packageName}/$relativePath"
}
) )
Intent(Intent.ACTION_VIEW).apply { Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR) setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
} else { } else {
Intent(Intent.ACTION_OPEN_DOCUMENT).apply { Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
@@ -416,7 +409,7 @@ object GeodeUtils {
} }
fun isGeodeUri(uri: Uri): Boolean { fun isGeodeUri(uri: Uri): Boolean {
return "${BuildConfig.APPLICATION_ID}.user" == uri.authority return "com.geode.launcher.user" == uri.authority
} }
private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal" private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal"
@@ -435,7 +428,7 @@ object GeodeUtils {
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) )
permissions.all { return permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
} }
} }
@@ -564,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)
@@ -800,7 +792,7 @@ object GeodeUtils {
external fun internalKeyEvent(timestamp: Long, deviceId: Int, eventSource: Int, keyCode: Int, modifiers: Int, isDown: Boolean, repeatCount: Int) external fun internalKeyEvent(timestamp: Long, deviceId: Int, eventSource: Int, keyCode: Int, modifiers: Int, isDown: Boolean, repeatCount: Int)
external fun internalTouchEvent(timestamp: Long, deviceId: Int, eventSource: Int, eventType: Int, ids: IntArray, xs: FloatArray, ys: FloatArray) external fun internalTouchEvent(timestamp: Long, deviceId: Int, eventSource: Int, eventType: Int, ids: IntArray, xs: FloatArray, ys: FloatArray)
external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float, mouseX: Float, mouseY: Float) external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float)
external fun internalJoystickEvent(timestamp: Long, deviceId: Int, eventSource: Int, leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray) external fun internalJoystickEvent(timestamp: Long, deviceId: Int, eventSource: Int, leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray)
external fun inputDeviceAdded(deviceId: Int) external fun inputDeviceAdded(deviceId: Int)
@@ -43,29 +43,23 @@ enum class ApplicationIcon {
DEFAULT, DEFAULT,
GEODE, GEODE,
PRIDE, PRIDE,
TRANS, TRANS;
GREEN,
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
"green" -> GREEN
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"
GREEN -> "green"
} }
} }
@@ -102,17 +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,
)
ApplicationIcon.GREEN -> ApplicationIconDetails(
id = id.toId(),
component = "MainActivityGreen",
iconId = R.mipmap.ic_launcher_green,
nameId = R.string.application_icon_green,
)
} }
} }
@@ -2,7 +2,6 @@ package com.geode.launcher.utils
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import com.geode.launcher.BuildConfig
import java.io.File import java.io.File
import java.io.Serializable import java.io.Serializable
@@ -42,9 +41,6 @@ object LaunchUtils {
} }
fun isGeodeInstalled(context: Context): Boolean { fun isGeodeInstalled(context: Context): Boolean {
@Suppress("SENSELESS_COMPARISON")
if (BuildConfig.PREBUNDLED_GEODE != null) return true
return getInstalledGeodePath(context) != null return getInstalledGeodePath(context) != null
} }
@@ -52,14 +48,10 @@ object LaunchUtils {
* Returns the directory that Geode/the game should base itself off of. * Returns the directory that Geode/the game should base itself off of.
*/ */
fun getBaseDirectory(context: Context, ignoreProfile: Boolean = false): File { fun getBaseDirectory(context: Context, ignoreProfile: Boolean = false): File {
val dir = if (BuildConfig.GOOGLE_PLAY_BUILD) { // deprecated, but seems to be the best choice of directory (i forced mat to test it)
context.getExternalFilesDir(null)!!.parentFile!! // also, is getting the first item the correct choice here?? what do they mean
} else { @Suppress("DEPRECATION")
// deprecated, but seems to be the best choice of directory (i forced mat to test it) val dir = context.externalMediaDirs.first()
// also, is getting the first item the correct choice here?? what do they mean
@Suppress("DEPRECATION")
context.externalMediaDirs.first()
}
// prevent having resources added to system gallery // prevent having resources added to system gallery
// accessing this file every time the directory is read may be a little wasteful... // accessing this file every time the directory is read may be a little wasteful...
@@ -81,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)
@@ -93,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()) {
@@ -106,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() }
@@ -114,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 {
@@ -132,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"
@@ -149,7 +149,6 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
LAST_UPDATE_CHECK_TIME, LAST_UPDATE_CHECK_TIME,
LAST_LAUNCHER_UPDATE, LAST_LAUNCHER_UPDATE,
DISABLE_UPDATE_CACHE, DISABLE_UPDATE_CACHE,
PREBUNDLED_GEODE_MIGRATION_PERFORMED,
} }
private fun defaultValueForBooleanKey(key: Key): Boolean { private fun defaultValueForBooleanKey(key: Key): Boolean {
@@ -200,7 +199,6 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.LAST_UPDATE_CHECK_TIME -> "PreferenceLastUpdateCheckTime" Key.LAST_UPDATE_CHECK_TIME -> "PreferenceLastUpdateCheckTime"
Key.LAST_LAUNCHER_UPDATE -> "PreferenceLastLauncherUpdate" Key.LAST_LAUNCHER_UPDATE -> "PreferenceLastLauncherUpdate"
Key.DISABLE_UPDATE_CACHE -> "PreferenceDisableUpdateCache" Key.DISABLE_UPDATE_CACHE -> "PreferenceDisableUpdateCache"
Key.PREBUNDLED_GEODE_MIGRATION_PERFORMED -> "PreferencePrebundledGeodeMigrationPerformed"
// Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel" // Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel"
// Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate" // Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
// Key.FORCE_HRR -> "PreferenceForceHighRefreshRate" // Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
@@ -6,19 +6,17 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
const val MS_TO_SEC = 1000L const val MS_TO_SEC = 1000L
@Composable @Composable
fun useCountdownTimer( fun useCountdownTimer(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
time: Duration, time: Long,
onCountdownFinish: () -> Unit onCountdownFinish: () -> Unit
): Long { ): Long {
var millisUntilFinished by remember { var millisUntilFinished by remember {
mutableStateOf(time) mutableLongStateOf(time)
} }
var shouldBeCounting by remember { var shouldBeCounting by remember {
@@ -30,9 +28,9 @@ fun useCountdownTimer(
return@LaunchedEffect return@LaunchedEffect
} }
if (millisUntilFinished > Duration.ZERO) { if (millisUntilFinished > 0) {
delay(1.seconds) delay(MS_TO_SEC)
millisUntilFinished -= 1.seconds millisUntilFinished -= MS_TO_SEC
} else { } else {
onCountdownFinish() onCountdownFinish()
} }
@@ -60,5 +58,5 @@ fun useCountdownTimer(
} }
} }
return millisUntilFinished.inWholeSeconds return millisUntilFinished / MS_TO_SEC
} }
@@ -1,45 +1,31 @@
package org.cocos2dx.lib package org.cocos2dx.lib
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.*
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Paint.FontMetricsInt import android.graphics.Paint.FontMetricsInt
import android.graphics.Rect
import android.graphics.Typeface
import android.text.TextPaint import android.text.TextPaint
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.view.MotionEvent import android.view.MotionEvent
import androidx.core.graphics.createBitmap
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.util.LinkedList import java.util.*
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.ceil import kotlin.math.ceil
private const val HORIZONTALALIGN_LEFT = 1
private const val HORIZONTALALIGN_RIGHT = 2
private const val HORIZONTALALIGN_CENTER = 3
private const val VERTICALALIGN_TOP = 1
private const val VERTICALALIGN_BOTTOM = 2
private const val VERTICALALIGN_CENTER = 3
@Suppress("KotlinJniMissingFunction") @Suppress("KotlinJniMissingFunction")
object Cocos2dxBitmap { object Cocos2dxBitmap {
private lateinit var context: WeakReference<Context> private lateinit var context: WeakReference<Context>
@JvmStatic @JvmStatic
external fun nativeInitBitmapDC(pWidth: Int, pHeight: Int, pPixels: ByteArray) private external fun nativeInitBitmapDC(pWidth: Int, pHeight: Int, pPixels: ByteArray)
fun setContext(context: Context) { fun setContext(context: Context) {
this.context = WeakReference(context) this.context = WeakReference(context)
} }
@JvmStatic /*
@Suppress("unused")
fun createTextBitmap( fun createTextBitmap(
string: String, string: String,
fontName: String, fontName: String,
@@ -69,6 +55,7 @@ object Cocos2dxBitmap {
1.0f 1.0f
) )
} }
*/
@JvmStatic @JvmStatic
@Suppress("unused") @Suppress("unused")
@@ -92,18 +79,19 @@ object Cocos2dxBitmap {
strokeB: Float, strokeB: Float,
strokeSize: Float strokeSize: Float
) { ) {
val horizontalAlignment = alignment and 0x0F val bitmapTotalHeight: Int
val verticalAlignment = (alignment shr 4) and 0x0F val horizontalAlignment = alignment and 15
val verticalAlignment = alignment shr 4 and 15
val pString2 = refactorString(string) val pString2 = refactorString(string)
val paint = newPaint(fontName, fontSize, horizontalAlignment) val paint = newPaint(fontName, fontSize, horizontalAlignment)
paint.setARGB( paint.setARGB(
255, MotionEvent.ACTION_MASK,
(255.0 * fontTintR).toInt(), (255.0 * fontTintR.toDouble()).toInt(),
(255.0 * fontTintG).toInt(), (255.0 * fontTintG.toDouble()).toInt(),
(255.0 * fontTintB).toInt() (255.0 * fontTintB.toDouble()).toInt()
) )
val textProperty = computeTextProperty(pString2, width, height, paint) val textProperty = computeTextProperty(pString2, width, height, paint)
val bitmapTotalHeight = if (height == 0) { bitmapTotalHeight = if (height == 0) {
textProperty.totalHeight textProperty.totalHeight
} else { } else {
height height
@@ -113,8 +101,7 @@ object Cocos2dxBitmap {
var renderTextDeltaX = 0.0f var renderTextDeltaX = 0.0f
var renderTextDeltaY = 0.0f var renderTextDeltaY = 0.0f
if (shadow) { if (shadow) {
val shadowColor = 0xff7d7d7du.toInt() paint.setShadowLayer(shadowBlur, shadowDX, shadowDY, -8553091)
paint.setShadowLayer(shadowBlur, shadowDX, shadowDY, shadowColor)
bitmapPaddingX = abs(shadowDX) bitmapPaddingX = abs(shadowDX)
bitmapPaddingY = abs(shadowDY) bitmapPaddingY = abs(shadowDY)
if (shadowDX.toDouble() < 0.0) { if (shadowDX.toDouble() < 0.0) {
@@ -124,47 +111,56 @@ object Cocos2dxBitmap {
renderTextDeltaY = bitmapPaddingY renderTextDeltaY = bitmapPaddingY
} }
} }
val bitmap = Bitmap.createBitmap(
val bitmap = try { textProperty.maxWidth + bitmapPaddingX.toInt(),
createBitmap( bitmapPaddingY.toInt() + bitmapTotalHeight,
textProperty.maxWidth + bitmapPaddingX.toInt(), Bitmap.Config.ARGB_8888
bitmapTotalHeight + bitmapPaddingY.toInt() )
)
} catch (e: Exception) {
// bandaid fix, hopefully someone checks logs to see this
Log.e("Cocos2dxBitmap", "Error creating bitmap: $e")
nativeInitBitmapDC(0, 0, ByteArray(0))
return
}
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
val fontMetricsInt = paint.fontMetricsInt val fontMetricsInt = paint.fontMetricsInt
var y = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment) var y = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment)
val lines = textProperty.lines val lines = textProperty.lines
val length = lines.size
for (line in lines) { for (i in 0 until length) {
val x = computeX(line, textProperty.maxWidth, horizontalAlignment) val line = lines[i]
canvas.drawText(line, x + renderTextDeltaX, y + renderTextDeltaY, paint) canvas.drawText(
line!!,
computeX(
line,
textProperty.maxWidth,
horizontalAlignment
).toFloat() + renderTextDeltaX,
y.toFloat() + renderTextDeltaY,
paint
)
y += textProperty.heightPerLine y += textProperty.heightPerLine
} }
if (stroke) { if (stroke) {
val paintStroke = newPaint(fontName, fontSize, horizontalAlignment) val paintStroke = newPaint(fontName, fontSize, horizontalAlignment)
paintStroke.style = Paint.Style.STROKE paintStroke.style = Paint.Style.STROKE
paintStroke.strokeWidth = strokeSize * 0.5f paintStroke.strokeWidth = 0.5f * strokeSize
paintStroke.setARGB( paintStroke.setARGB(
255, MotionEvent.ACTION_MASK,
strokeR.toInt() * MotionEvent.ACTION_MASK, strokeR.toInt() * MotionEvent.ACTION_MASK,
strokeG.toInt() * MotionEvent.ACTION_MASK, strokeG.toInt() * MotionEvent.ACTION_MASK,
strokeB.toInt() * MotionEvent.ACTION_MASK strokeB.toInt() * MotionEvent.ACTION_MASK
) )
var y = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment) var y2 = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment)
val lines = textProperty.lines val lines2 = textProperty.lines
val length2 = lines2.size
for (line in lines) { for (i2 in 0 until length2) {
val x = computeX(line, textProperty.maxWidth, horizontalAlignment) val line2 = lines2[i2]
canvas.drawText(line, x + renderTextDeltaX, y + renderTextDeltaY, paintStroke) canvas.drawText(
y += textProperty.heightPerLine line2!!,
computeX(
line2,
textProperty.maxWidth,
horizontalAlignment
).toFloat() + renderTextDeltaX,
y2.toFloat() + renderTextDeltaY,
paintStroke
)
y2 += textProperty.heightPerLine
} }
} }
initNativeObject(bitmap) initNativeObject(bitmap)
@@ -172,24 +168,24 @@ object Cocos2dxBitmap {
private fun newPaint(fontName: String, fontSize: Int, horizontalAlignment: Int): Paint { private fun newPaint(fontName: String, fontSize: Int, horizontalAlignment: Int): Paint {
val paint = Paint() val paint = Paint()
paint.color = Color.WHITE paint.color = -1
paint.textSize = fontSize.toFloat() paint.textSize = fontSize.toFloat()
paint.isAntiAlias = true paint.isAntiAlias = true
if (fontName.endsWith(".ttf")) { if (fontName.endsWith(".ttf")) {
try { try {
context.get()!!.apply { context.get()?.apply {
paint.typeface = Cocos2dxTypefaces[this, fontName] paint.typeface = Cocos2dxTypefaces[this, fontName]
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Cocos2dxBitmap", "error to create ttf type face: $fontName - $e") Log.e("Cocos2dxBitmap", "error to create ttf type face: $fontName")
paint.typeface = Typeface.create(fontName, Typeface.NORMAL) paint.typeface = Typeface.create(fontName, Typeface.NORMAL)
} }
} else { } else {
paint.typeface = Typeface.create(fontName, Typeface.NORMAL) paint.typeface = Typeface.create(fontName, Typeface.NORMAL)
} }
when (horizontalAlignment) { when (horizontalAlignment) {
HORIZONTALALIGN_RIGHT -> paint.textAlign = Paint.Align.RIGHT 2 -> paint.textAlign = Paint.Align.RIGHT
HORIZONTALALIGN_CENTER -> paint.textAlign = Paint.Align.CENTER 3 -> paint.textAlign = Paint.Align.CENTER
else -> paint.textAlign = Paint.Align.LEFT else -> paint.textAlign = Paint.Align.LEFT
} }
return paint return paint
@@ -202,14 +198,14 @@ object Cocos2dxBitmap {
paint: Paint paint: Paint
): TextProperty { ): TextProperty {
val fm = paint.fontMetricsInt val fm = paint.fontMetricsInt
val h = fm.bottom - fm.top val h = ceil((fm.bottom - fm.top).toDouble()).toInt()
var maxContentWidth = 0 var maxContentWidth = 0
val lines = splitString(string, width, height, paint) val lines = splitString(string, width, height, paint)
if (width != 0) { if (width != 0) {
maxContentWidth = width maxContentWidth = width
} else { } else {
for (line in lines) { for (line in lines) {
val temp = ceil(paint.measureText(line, 0, line.length)).toInt() val temp = ceil(paint.measureText(line, 0, line!!.length).toDouble()).toInt()
if (temp > maxContentWidth) { if (temp > maxContentWidth) {
maxContentWidth = temp maxContentWidth = temp
} }
@@ -218,10 +214,10 @@ object Cocos2dxBitmap {
return TextProperty(maxContentWidth, h, lines) return TextProperty(maxContentWidth, h, lines)
} }
private fun computeX(@Suppress("UNUSED_PARAMETER") text: String, maxWidth: Int, horizontalAlignment: Int): Int { private fun computeX(@Suppress("UNUSED_PARAMETER") text: String?, maxWidth: Int, horizontalAlignment: Int): Int {
return when (horizontalAlignment) { return when (horizontalAlignment) {
HORIZONTALALIGN_RIGHT -> maxWidth 2 -> maxWidth
HORIZONTALALIGN_CENTER -> maxWidth / 2 3 -> maxWidth / 2
else -> 0 else -> 0
} }
} }
@@ -233,15 +229,13 @@ object Cocos2dxBitmap {
verticalAlignment: Int verticalAlignment: Int
): Int { ): Int {
val y = -fontMetricsInt.top val y = -fontMetricsInt.top
return if (constrainHeight > totalHeight) { return if (constrainHeight <= totalHeight) {
when (verticalAlignment) {
VERTICALALIGN_TOP -> -fontMetricsInt.top
VERTICALALIGN_CENTER -> -fontMetricsInt.top + (constrainHeight - totalHeight) / 2
VERTICALALIGN_BOTTOM -> -fontMetricsInt.top + (constrainHeight - totalHeight)
else -> y
}
} else {
y y
} else when (verticalAlignment) {
1 -> -fontMetricsInt.top
2 -> -fontMetricsInt.top + (constrainHeight - totalHeight)
3 -> -fontMetricsInt.top + (constrainHeight - totalHeight) / 2
else -> y
} }
} }
@@ -250,56 +244,56 @@ object Cocos2dxBitmap {
maxWidth: Int, maxWidth: Int,
maxHeight: Int, maxHeight: Int,
paint: Paint paint: Paint
): List<String> { ): Array<String?> {
val lines = string.split("\n") val lines: Array<String?> = string.split("\\n").toTypedArray()
val fm = paint.fontMetricsInt val fm = paint.fontMetricsInt
val heightPerLine = fm.bottom - fm.top val maxLines = maxHeight / ceil((fm.bottom - fm.top).toDouble()).toInt()
val maxLines = maxHeight / heightPerLine
return if (maxWidth != 0) { return if (maxWidth != 0) {
val strList = LinkedList<String>() val strList = LinkedList<String?>()
for (line in lines) { for (line in lines) {
val lineWidth = ceil(paint.measureText(line)).toInt() if (ceil(paint.measureText(line).toDouble()).toInt() > maxWidth) {
if (lineWidth > maxWidth) {
strList.addAll(divideStringWithMaxWidth(line, maxWidth, paint)) strList.addAll(divideStringWithMaxWidth(line, maxWidth, paint))
} else { } else {
strList.add(line) strList.add(line)
} }
if (maxLines > 0 && strList.size >= maxLines) { if (maxLines > 0 && strList.size >= maxLines) {
break break
} }
} }
if (maxLines > 0 && strList.size > maxLines) { if (maxLines > 0 && strList.size > maxLines) {
while (strList.size > maxLines) { while (strList.size > maxLines) {
strList.removeLast() strList.removeLast()
} }
} }
val ret = arrayOfNulls<String>(strList.size)
strList strList.toArray(ret)
} else if (maxHeight != 0 && lines.size > maxLines) { ret
lines.take(maxLines) } else if (maxHeight == 0 || lines.size <= maxLines) {
} else {
lines lines
} else {
val strList2 = LinkedList<String?>()
for (i in 0 until maxLines) {
strList2.add(lines[i])
}
val ret2 = arrayOfNulls<String>(strList2.size)
strList2.toArray(ret2)
ret2
} }
} }
private fun divideStringWithMaxWidth( private fun divideStringWithMaxWidth(
string: String, string: String?,
maxWidth: Int, maxWidth: Int,
paint: Paint paint: Paint
): LinkedList<String> { ): LinkedList<String?> {
val charLength = string.length val charLength = string!!.length
var start = 0 var start = 0
val strList = LinkedList<String>() val strList = LinkedList<String?>()
var i = 1 var i = 1
while (i <= charLength) { while (i <= charLength) {
val tempWidth = ceil(paint.measureText(string, start, i)).toInt() val tempWidth = ceil(paint.measureText(string, start, i).toDouble()).toInt()
if (tempWidth >= maxWidth) { if (tempWidth >= maxWidth) {
val lastIndexOfSpace = string.substring(0, i).lastIndexOf(" ") val lastIndexOfSpace = string.substring(0, i).lastIndexOf(" ")
if (lastIndexOfSpace != -1 && lastIndexOfSpace > start) { if (lastIndexOfSpace != -1 && lastIndexOfSpace > start) {
strList.add(string.substring(start, lastIndexOfSpace)) strList.add(string.substring(start, lastIndexOfSpace))
i = lastIndexOfSpace + 1 i = lastIndexOfSpace + 1
@@ -309,20 +303,16 @@ object Cocos2dxBitmap {
} else { } else {
strList.add(string.substring(start, i)) strList.add(string.substring(start, i))
} }
while (i < charLength && string[i] == ' ') { while (i < charLength && string[i] == ' ') {
i++ i++
} }
start = i start = i
} }
i++ i++
} }
if (start < charLength) { if (start < charLength) {
strList.add(string.substring(start)) strList.add(string.substring(start))
} }
return strList return strList
} }
@@ -330,22 +320,19 @@ object Cocos2dxBitmap {
if (pString.compareTo("") == 0) { if (pString.compareTo("") == 0) {
return " " return " "
} }
val strBuilder = StringBuilder(pString) val strBuilder = StringBuilder(pString)
var start = 0 var start = 0
var index = strBuilder.indexOf("\n") var index = strBuilder.indexOf("\n")
while (index != -1) { while (index != -1) {
start = if (index == 0 || strBuilder[index - 1] == '\n') { start = if (index == 0 || strBuilder[index - 1].code == 10) {
strBuilder.insert(start, " ") strBuilder.insert(start, " ")
index + 2 index + 2
} else { } else {
index + 1 index + 1
} }
if (start > strBuilder.length || index == strBuilder.length) { if (start > strBuilder.length || index == strBuilder.length) {
break break
} }
index = strBuilder.indexOf("\n", start) index = strBuilder.indexOf("\n", start)
} }
return strBuilder.toString() return strBuilder.toString()
@@ -353,10 +340,15 @@ object Cocos2dxBitmap {
private fun initNativeObject(pBitmap: Bitmap) { private fun initNativeObject(pBitmap: Bitmap) {
val pixels = getPixels(pBitmap) val pixels = getPixels(pBitmap)
nativeInitBitmapDC(pBitmap.width, pBitmap.height, pixels) if (pixels != null) {
nativeInitBitmapDC(pBitmap.width, pBitmap.height, pixels)
}
} }
private fun getPixels(pBitmap: Bitmap): ByteArray { private fun getPixels(pBitmap: Bitmap?): ByteArray? {
if (pBitmap == null) {
return null
}
val pixels = ByteArray(pBitmap.width * pBitmap.height * 4) val pixels = ByteArray(pBitmap.width * pBitmap.height * 4)
val buf = ByteBuffer.wrap(pixels) val buf = ByteBuffer.wrap(pixels)
buf.order(ByteOrder.nativeOrder()) buf.order(ByteOrder.nativeOrder())
@@ -369,18 +361,13 @@ object Cocos2dxBitmap {
private fun getFontSizeAccordingHeight(height: Int): Int { private fun getFontSizeAccordingHeight(height: Int): Int {
val paint = Paint() val paint = Paint()
val bounds = Rect() val bounds = Rect()
paint.typeface = Typeface.DEFAULT paint.typeface = Typeface.DEFAULT
var incrTextSize = 1 var incrTextSize = 1
var foundDesiredSize = false var foundDesiredSize = false
while (!foundDesiredSize) { while (!foundDesiredSize) {
paint.textSize = incrTextSize.toFloat() paint.textSize = incrTextSize.toFloat()
val text = "SghMNy" paint.getTextBounds("SghMNy", 0, "SghMNy".length, bounds)
paint.getTextBounds(text, 0, text.length, bounds)
incrTextSize++ incrTextSize++
if (height - bounds.height() <= 2) { if (height - bounds.height() <= 2) {
foundDesiredSize = true foundDesiredSize = true
} }
@@ -395,19 +382,23 @@ object Cocos2dxBitmap {
if (TextUtils.isEmpty(string)) { if (TextUtils.isEmpty(string)) {
return "" return ""
} }
val paint = TextPaint() val paint = TextPaint()
paint.typeface = Typeface.DEFAULT paint.typeface = Typeface.DEFAULT
paint.textSize = fontSize paint.textSize = fontSize
return TextUtils.ellipsize(string, paint, width, TextUtils.TruncateAt.END).toString() return TextUtils.ellipsize(string, paint, width, TextUtils.TruncateAt.END).toString()
} }
private class TextProperty( private class TextProperty constructor(
val maxWidth: Int, val maxWidth: Int,
val heightPerLine: Int, val heightPerLine: Int, pLines: Array<String?>
val lines: Collection<String>
) { ) {
val totalHeight: Int = lines.size * heightPerLine val lines: Array<String?>
val totalHeight: Int
init {
totalHeight = pLines.size * heightPerLine
lines = pLines
}
} }
} }
@@ -8,13 +8,11 @@ import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
import android.text.InputType import android.util.Log
import android.view.InputDevice import android.view.InputDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.Surface import android.view.Surface
import android.view.WindowInsets
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatEditText
@@ -27,54 +25,12 @@ private const val HANDLER_CLOSE_IME_KEYBOARD = 3
private const val MS_TO_NS = 1_000_000 private const val MS_TO_NS = 1_000_000
private const val SEC_TO_NS = 1_000_000_000.0 private const val SEC_TO_NS = 1_000_000_000.0
fun inputTypeForFlags(i: Int): Int {
if ((i and BaseRobTopActivity.KEYBOARD_INPUT_NUMERIC) != 0) {
return InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED or InputType.TYPE_CLASS_NUMBER
}
if ((i and BaseRobTopActivity.KEYBOARD_INPUT_PASSWORD) != 0) {
return InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
}
if ((i and BaseRobTopActivity.KEYBOARD_INPUT_MULTILINE) != 0) {
return InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_CLASS_TEXT
}
return InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_CLASS_TEXT
}
fun usesInlineKeyboard(): Boolean {
return Build.VERSION.SDK_INT >= 30
}
fun imeOptionsForFlags(i: Int): Int {
val i2 = if ((i and BaseRobTopActivity.KEYBOARD_INPUT_MULTILINE) == 0 || !usesInlineKeyboard())
EditorInfo.IME_ACTION_DONE else EditorInfo.IME_ACTION_NONE
return if (usesInlineKeyboard())
EditorInfo.IME_FLAG_NO_FULLSCREEN or EditorInfo.IME_FLAG_NO_EXTRACT_UI or i2
else i2
}
class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
companion object { companion object {
lateinit var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView lateinit var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView
private lateinit var handler: Handler private lateinit var handler: Handler
private lateinit var cocos2dxTextInputWrapper: Cocos2dxTextInputWrapper private lateinit var cocos2dxTextInputWrapper: Cocos2dxTextInputWrapper
private var keyboardVisible = false
private var keyboardHeight = 0
private var keyboardHideCheck = Runnable {
if (keyboardVisible) {
return@Runnable
}
cocos2dxGLSurfaceView.queueEvent {
cocos2dxGLSurfaceView.cocos2dxRenderer.handleTextClosed()
}
}
@JvmStatic @JvmStatic
fun openIMEKeyboard() { fun openIMEKeyboard() {
val msg = Message() val msg = Message()
@@ -94,30 +50,6 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
cocos2dxGLSurfaceView.queueEvent { cocos2dxGLSurfaceView.queueEvent {
Cocos2dxAccelerometer.onSensorChanged(x, y, z, timestamp) Cocos2dxAccelerometer.onSensorChanged(x, y, z, timestamp)
} }
@RequiresApi(Build.VERSION_CODES.R)
fun onKeyboardInsets(windowInsets: WindowInsets) {
if (!cocos2dxGLSurfaceView.sendNewCocosEvents) {
return
}
val visible = windowInsets.isVisible(WindowInsets.Type.ime())
val offsetHeight = if (visible) windowInsets.getInsets(WindowInsets.Type.ime()).bottom else 0
if (visible == keyboardVisible && offsetHeight == keyboardHeight) {
return
}
keyboardVisible = visible
keyboardHeight = offsetHeight
handler.removeCallbacks(keyboardHideCheck)
if (!visible) {
handler.postDelayed(keyboardHideCheck, 150L)
} else {
cocos2dxGLSurfaceView.cocos2dxRenderer.handleKeyboardWillShow(keyboardHeight)
}
}
} }
private lateinit var cocos2dxRenderer: Cocos2dxRenderer private lateinit var cocos2dxRenderer: Cocos2dxRenderer
@@ -131,10 +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
// 2.209+ events for keyboard and such
var sendNewCocosEvents = false
var cocos2dxEditText: AppCompatEditText? = null var cocos2dxEditText: AppCompatEditText? = null
set(value) { set(value) {
@@ -156,13 +84,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
HANDLER_OPEN_IME_KEYBOARD -> { HANDLER_OPEN_IME_KEYBOARD -> {
if (cocos2dxEditText?.requestFocus() == true) { if (cocos2dxEditText?.requestFocus() == true) {
cocos2dxEditText?.apply { cocos2dxEditText?.apply {
removeTextChangedListener(cocos2dxTextInputWrapper) removeTextChangedListener(
cocos2dxTextInputWrapper
if (sendNewCocosEvents) { )
inputType = inputTypeForFlags(BaseRobTopActivity.curKeyboardInputFlags)
imeOptions = imeOptionsForFlags(BaseRobTopActivity.curKeyboardInputFlags)
}
setText("") setText("")
val text = msg.obj as String val text = msg.obj as String
append(text) append(text)
@@ -175,13 +99,16 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
cocos2dxEditText, cocos2dxEditText,
0 0
) )
Log.d("GLSurfaceView", "showSoftInput")
return return
} }
return return
} }
HANDLER_CLOSE_IME_KEYBOARD -> { HANDLER_CLOSE_IME_KEYBOARD -> {
cocos2dxEditText?.apply { cocos2dxEditText?.apply {
removeTextChangedListener(cocos2dxTextInputWrapper) removeTextChangedListener(
cocos2dxTextInputWrapper
)
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow( (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(
windowToken, windowToken,
0 0
@@ -189,10 +116,8 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
requestFocus() requestFocus()
Log.d("GLSurfaceView", "HideSoftInput")
queueEvent { cocos2dxRenderer.handleTextClosed()
cocos2dxRenderer.handleTextClosed()
}
return return
} }
else -> return else -> return
@@ -363,103 +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 queueKeyboardEvent(keyCode: Int, event: KeyEvent, down: Boolean): Boolean {
val keypadCode = keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_BACK
if (event.isSystem && !keypadCode) {
return false
}
if (keypadCode && BaseRobTopActivity.blockBackButton) {
return true
}
queueEvent {
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
}
if (useKeyboardEvents) {
if (down) {
queueEvent {
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
}
} else {
queueEvent {
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
}
}
}
if (controllerEventsEnabled) {
queueEvent {
GeodeUtils.internalKeyEvent(
event.eventTime * MS_TO_NS,
event.deviceId,
event.source,
event.keyCode,
event.modifiers,
down,
event.repeatCount
)
}
}
// geode events override native callbacks
if (useKeyboardEvents || controllerEventsEnabled) {
return true
}
if (sendNewCocosEvents) {
if (KeyEvent.isGamepadButton(keyCode)) {
return false
}
if (keyCode == KeyEvent.KEYCODE_ESCAPE && BaseRobTopActivity.blockBackButton) {
return true
}
// messy logic
val isRepeat = event.repeatCount > 0
if (keypadCode) {
if (down) {
if (isRepeat || BaseRobTopActivity.blockBackButton) {
return true
}
queueEvent {
cocos2dxRenderer.handleKeyDown(keyCode)
}
return true
} else {
return false
}
}
val eventTimestamp = event.eventTime / 1000.0
val shiftPressed = event.isShiftPressed
val ctrlPressed = event.isCtrlPressed
val altPressed = event.isAltPressed
queueEvent {
cocos2dxRenderer.handleKeyboardEvent(
keyCode, down, isRepeat, eventTimestamp,
shiftPressed, ctrlPressed, altPressed
)
}
return true
}
return false
} }
private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean { private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
@@ -519,19 +347,65 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (!queueKeyboardEvent(keyCode, event, true)) { if (!useKeyboardEvents && !controllerEventsEnabled) {
return legacyKeyDown(keyCode, event) return legacyKeyDown(keyCode, event)
} }
return true return when (keyCode) {
// ignore system keys
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> {
super.onKeyDown(keyCode, event)
}
else -> {
if (BaseRobTopActivity.blockBackButton) {
return true
}
queueEvent {
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
if (useKeyboardEvents) {
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
}
if (controllerEventsEnabled) {
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, true, event.repeatCount)
}
}
true
}
}
} }
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
if (!queueKeyboardEvent(keyCode, event, false)) { if (!useKeyboardEvents && !controllerEventsEnabled) {
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
} }
return true return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> {
super.onKeyUp(keyCode, event)
}
else -> {
if (event.repeatCount != 0 || BaseRobTopActivity.blockBackButton) {
return true
}
queueEvent {
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
if (useKeyboardEvents) {
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
}
if (controllerEventsEnabled) {
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, false, event.repeatCount)
}
}
true
}
}
} }
// i'm lazy. // i'm lazy.
@@ -594,15 +468,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
override fun onGenericMotionEvent(event: MotionEvent?): Boolean { override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
val hasGeodeEvents = useKeyboardEvents || controllerEventsEnabled if (!useKeyboardEvents && !controllerEventsEnabled) {
if (!hasGeodeEvents && !sendNewCocosEvents) {
return super.onGenericMotionEvent(event) return super.onGenericMotionEvent(event)
} }
if (event?.action == MotionEvent.ACTION_SCROLL) { if (event?.action == MotionEvent.ACTION_SCROLL) {
val mouseX = event.x
val mouseY = event.y
val scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL) val scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL)
val scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL) val scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -616,11 +486,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
if (controllerEventsEnabled) { if (controllerEventsEnabled) {
GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY, mouseX, mouseY) GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY)
}
if (!hasGeodeEvents && sendNewCocosEvents) {
cocos2dxRenderer.handleScroll(mouseX, mouseY, scrollY, scrollX)
} }
} }
@@ -28,29 +28,12 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
@JvmStatic @JvmStatic
private external fun nativeKeyDown(keyCode: Int): Boolean private external fun nativeKeyDown(keyCode: Int): Boolean
@JvmStatic
private external fun nativeKeyboardEvent(
keyCode: Int,
down: Boolean,
isRepeat: Boolean,
timestamp: Double,
shiftPressed: Boolean,
ctrlPressed: Boolean,
altPressed: Boolean
)
@JvmStatic
private external fun nativeKeyboardWillShow(offset: Int)
@JvmStatic @JvmStatic
private external fun nativeOnPause() private external fun nativeOnPause()
@JvmStatic @JvmStatic
private external fun nativeOnResume() private external fun nativeOnResume()
@JvmStatic
private external fun nativeScroll(x: Float, y: Float, scrollY: Float, scrollX: Float)
@JvmStatic @JvmStatic
private external fun nativeRender() private external fun nativeRender()
@@ -150,18 +133,6 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
nativeKeyDown(keyCode) nativeKeyDown(keyCode)
} }
fun handleKeyboardWillShow(offset: Int) {
nativeKeyboardWillShow(offset)
}
fun handleKeyboardEvent(keyCode: Int, down: Boolean, isRepeat: Boolean, timestamp: Double, shiftPressed: Boolean, ctrlPressed: Boolean, altPressed: Boolean) {
nativeKeyboardEvent(keyCode, down, isRepeat, timestamp, shiftPressed, ctrlPressed, altPressed)
}
fun handleScroll(x: Float, y: Float, scrollY: Float, scrollX: Float) {
nativeScroll(x, y, scrollY, scrollX)
}
fun handleOnPause() { fun handleOnPause() {
nativeOnPause() nativeOnPause()
} }
@@ -5,10 +5,8 @@ import android.text.TextWatcher
import android.widget.TextView.OnEditorActionListener import android.widget.TextView.OnEditorActionListener
import android.text.Editable import android.text.Editable
import android.view.KeyEvent import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView import android.widget.TextView
import com.customRobTop.JniToCpp
class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView) : TextWatcher, OnEditorActionListener { class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView) : TextWatcher, OnEditorActionListener {
private var originText: String? = null private var originText: String? = null
@@ -21,33 +19,21 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
this.originText = originText this.originText = originText
} }
override fun afterTextChanged(editable: Editable) { override fun afterTextChanged(s: Editable) {
if (isFullScreenEdit) { if (!isFullScreenEdit) {
return var nModified = s.length - text!!.length
if (nModified > 0) {
cocos2dxGLSurfaceView.insertText(
s.subSequence(text!!.length, s.length).toString()
)
} else {
while (nModified < 0) {
cocos2dxGLSurfaceView.deleteBackward()
nModified++
}
}
text = s.toString()
} }
val cText = text ?: return
val str = editable.toString()
val unchanged = str.commonPrefixWith(cText)
var unchangedUntil = unchanged.length
if (unchangedUntil > 0 && unchangedUntil < str.length && Character.isLowSurrogate(str[unchangedUntil])) {
unchangedUntil--
}
val codePointLength = cText.codePointCount(unchangedUntil, cText.length)
repeat(codePointLength) {
cocos2dxGLSurfaceView.deleteBackward()
}
if (unchangedUntil < str.length) {
cocos2dxGLSurfaceView.insertText(
str.substring(unchangedUntil)
)
}
text = str
} }
override fun beforeTextChanged( override fun beforeTextChanged(
@@ -61,45 +47,22 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
override fun onTextChanged(pCharSequence: CharSequence, start: Int, before: Int, count: Int) {} override fun onTextChanged(pCharSequence: CharSequence, start: Int, before: Int, count: Int) {}
override fun onEditorAction(textView: TextView, actionID: Int, keyEvent: KeyEvent?): Boolean { override fun onEditorAction(textView: TextView, actionID: Int, keyEvent: KeyEvent?): Boolean {
val fullscreenIme = cocos2dxGLSurfaceView.cocos2dxEditText == textView && isFullScreenEdit if (cocos2dxGLSurfaceView.cocos2dxEditText == textView && isFullScreenEdit) {
if (fullscreenIme) { for (i in originText!!.length downTo 1) {
cocos2dxGLSurfaceView.deleteBackward()
}
var text = textView.text.toString() var text = textView.text.toString()
if (originText != text || !cocos2dxGLSurfaceView.sendNewCocosEvents) { if (!text.endsWith('\n')) {
if (cocos2dxGLSurfaceView.sendNewCocosEvents) { text += '\n'
cocos2dxGLSurfaceView.queueEvent {
JniToCpp.textInputCaretToEnd()
}
}
repeat(originText!!.length) {
cocos2dxGLSurfaceView.deleteBackward()
}
if (!cocos2dxGLSurfaceView.sendNewCocosEvents) {
if (!text.endsWith('\n')) {
text += '\n'
}
}
if (text.isNotEmpty()) {
cocos2dxGLSurfaceView.insertText(text)
}
} }
}
if (fullscreenIme || actionID == EditorInfo.IME_ACTION_DONE) { cocos2dxGLSurfaceView.insertText(text)
if (cocos2dxGLSurfaceView.sendNewCocosEvents) {
cocos2dxGLSurfaceView.queueEvent {
JniToCpp.textInputDone()
}
}
} }
if (actionID != 6) {
if (actionID == EditorInfo.IME_ACTION_DONE) { return false
cocos2dxGLSurfaceView.requestFocus()
Cocos2dxGLSurfaceView.closeIMEKeyboard()
} }
cocos2dxGLSurfaceView.requestFocus()
Cocos2dxGLSurfaceView.closeIMEKeyboard()
return false return false
} }
} }
@@ -1,28 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="1366" android:viewportWidth="1024"
android:viewportHeight="1366"> android:viewportHeight="1024">
<group> <group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
<clip-path <clip-path
android:pathData="M0,0h1366v1366h-1366z"/> android:pathData="M0,0h1024v1024h-1024z"/>
<path <path
android:pathData="M0,0h1366v1366h-1366z" android:pathData="M0,0h1024v1024h-1024z"
android:fillColor="#F4D48E"/> android:fillColor="#F4D48E"/>
<path <path
android:pathData="M598,683m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0" android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
android:fillColor="#F4D48E"/> android:fillColor="#F4D48E"/>
<path <path
android:pathData="M873.8,459.7C752.5,412.3 642.7,323 397.2,323C151.8,323 18.6,418.7 4.3,463.8C-10.1,508.8 4.7,562.4 -1.7,561.4L-25,878H1382L1373,631.1C1346.8,597.8 1260.8,569.9 1217.2,562.4C1173.6,554.9 996.8,507.8 873.8,459.7Z" android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
android:fillColor="#F4A47B"/> android:fillColor="#F4A47B"/>
<path <path
android:pathData="M647.9,685.4C569.9,647.4 412.4,573 277.3,573C142.1,573 80.5,611.7 39.1,632.4C-2.4,653.2 -9.5,656.1 -14.9,657.4L-25,978.5L650,985L647.9,685.4Z" android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
android:fillColor="#E27880"/> android:fillColor="#E27880"/>
<path <path
android:pathData="M1362.1,728.7C1327,696.7 1194.3,624.7 888.5,623C725.4,622.1 492.3,679.9 354.7,757.1C228.9,827.6 68.9,867 -5.2,873.5L-152,1140H1583L1362.1,728.7Z" android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
android:fillColor="#B9588F"/> android:fillColor="#B9588F"/>
<path <path
android:pathData="M1107,1027.2C885.5,1027.2 739,943.6 544.9,943.6C350.9,943.6 228.8,1031.7 186.1,1040.2L-8,1120.8V1385L1376,1379L1367,909C1361.5,929 1287.6,1027.2 1107,1027.2Z" android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
android:fillColor="#714A9A"/> android:fillColor="#714A9A"/>
</group> </group>
</vector> </vector>
@@ -1,29 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1366"
android:viewportHeight="1366">
<group>
<clip-path
android:pathData="M0,0h1366v1366h-1366z"/>
<path
android:pathData="M0,0h1366v1366h-1366z"
android:fillColor="#9EF4C3"/>
<path
android:pathData="M598,683m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
android:fillColor="#9EF4C3"/>
<path
android:pathData="M873.8,459.7C752.5,412.3 642.7,323 397.2,323C151.8,323 18.6,418.7 4.3,463.8C-10.1,508.8 4.7,562.4 -1.7,561.4L-25,878H1382L1373,631.1C1346.8,597.8 1260.8,569.9 1217.2,562.4C1173.6,554.9 996.8,507.8 873.8,459.7Z"
android:fillColor="#6DDBAD"/>
<path
android:pathData="M647.9,685.4C569.9,647.4 412.4,573 277.3,573C142.1,573 80.5,611.7 39.1,632.4C-2.4,653.2 -9.5,656.1 -14.9,657.4L-25,978.5L650,985L647.9,685.4Z"
android:fillColor="#64C780"/>
<path
android:pathData="M1362.1,728.7C1327,696.7 1194.3,624.7 888.5,623C725.4,622.1 492.3,679.9 354.7,757.1C228.9,827.6 68.9,867 -5.2,873.5L-152,1140H1583L1362.1,728.7Z"
android:fillColor="#46A47D"/>
<path
android:pathData="M1107,1027.2C885.5,1027.2 739,943.6 544.9,943.6C350.9,943.6 228.8,1031.7 186.1,1040.2L-8,1120.8V1385L1376,1379L1367,909C1361.5,929 1287.6,1027.2 1107,1027.2Z"
android:fillColor="#417563"/>
</group>
</vector>
@@ -1,29 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="1366" android:viewportWidth="1024"
android:viewportHeight="1366"> android:viewportHeight="1024">
<group> <group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
<clip-path <clip-path
android:pathData="M0,0h1366v1366h-1366z"/> android:pathData="M0,0h1024v1024h-1024z"/>
<path <path
android:pathData="M0,0h1366v1366h-1366z" android:pathData="M0,0h1024v1024h-1024z"
android:fillColor="#CC7C7C"/> android:fillColor="#CC7C7C"/>
<path <path
android:pathData="M598,683m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0" android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
android:fillColor="#CC7C7C"/> android:fillColor="#CC7C7C"/>
<path <path
android:pathData="M873.8,459.7C752.5,412.3 642.7,323 397.2,323C151.8,323 18.6,418.7 4.3,463.8C-10.1,508.8 4.7,562.4 -1.7,561.4L-25,878H1382L1373,631.1C1346.8,597.8 1260.8,569.9 1217.2,562.4C1173.6,554.9 996.8,507.8 873.8,459.7Z" android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
android:fillColor="#C6B471"/> android:fillColor="#C6B471"/>
<path <path
android:pathData="M647.9,685.4C569.9,647.4 412.4,573 277.3,573C142.1,573 80.5,611.7 39.1,632.4C-2.4,653.2 -9.5,656.1 -14.9,657.4L-25,978.5L650,985L647.9,685.4Z" android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
android:fillColor="#59AF7F"/> android:fillColor="#59AF7F"/>
<path <path
android:pathData="M1362.1,728.7C1327,696.7 1194.3,624.7 888.5,623C725.4,622.1 492.3,679.9 354.7,757.1C228.9,827.6 68.9,867 -5.2,873.5L-152,1140H1583L1362.1,728.7Z" android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
android:fillColor="#5F8CAB"/> android:fillColor="#5F8CAB"/>
<path <path
android:pathData="M1107,1027.2C885.5,1027.2 739,943.6 544.9,943.6C350.9,943.6 228.8,1031.7 186.1,1040.2L-8,1120.8V1385L1376,1379L1367,909C1361.5,929 1287.6,1027.2 1107,1027.2Z" android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
android:fillColor="#70558D"/> android:fillColor="#70558D"/>
</group> </group>
</vector> </vector>
@@ -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>
@@ -1,29 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="1366" android:viewportWidth="1024"
android:viewportHeight="1366"> android:viewportHeight="1024">
<group> <group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
<clip-path <clip-path
android:pathData="M0,0h1366v1366h-1366z"/> android:pathData="M0,0h1024v1024h-1024z"/>
<path <path
android:pathData="M0,0h1366v1366h-1366z" android:pathData="M0,0h1024v1024h-1024z"
android:fillColor="#65B8C2"/> android:fillColor="#65B8C2"/>
<path <path
android:pathData="M598,683m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0" android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
android:fillColor="#65B8C2"/> android:fillColor="#65B8C2"/>
<path <path
android:pathData="M873.8,459.7C752.5,412.3 642.7,323 397.2,323C151.8,323 18.6,418.7 4.3,463.8C-10.1,508.8 4.7,562.4 -1.7,561.4L-25,878H1382L1373,631.1C1346.8,597.8 1260.8,569.9 1217.2,562.4C1173.6,554.9 996.8,507.8 873.8,459.7Z" android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
android:fillColor="#DEA5CD"/> android:fillColor="#DEA5CD"/>
<path <path
android:pathData="M647.9,685.4C569.9,647.4 412.4,573 277.3,573C142.1,573 80.5,611.7 39.1,632.4C-2.4,653.2 -9.5,656.1 -14.9,657.4L-25,978.5L650,985L647.9,685.4Z" android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
android:fillColor="#DEE8EA"/> android:fillColor="#DEE8EA"/>
<path <path
android:pathData="M1362.1,728.7C1327,696.7 1194.3,624.7 888.5,623C725.4,622.1 492.3,679.9 354.7,757.1C228.9,827.6 68.9,867 -5.2,873.5L-152,1140H1583L1362.1,728.7Z" android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
android:fillColor="#C485BD"/> android:fillColor="#C485BD"/>
<path <path
android:pathData="M1107,1027.2C885.5,1027.2 739,943.6 544.9,943.6C350.9,943.6 228.8,1031.7 186.1,1040.2L-8,1120.8V1385L1376,1379L1367,909C1361.5,929 1287.6,1027.2 1107,1027.2Z" android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
android:fillColor="#259CB4"/> android:fillColor="#259CB4"/>
</group> </group>
</vector> </vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.
@@ -1,5 +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_green_background"/>
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
</adaptive-icon>
@@ -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>
+4 -8
View File
@@ -35,7 +35,7 @@
</string> </string>
<string name="launcher_game_update_required_short">A Geometry Dash update is required.</string> <string name="launcher_game_update_required_short">A Geometry Dash update is required.</string>
<string name="launcher_game_update_required">The currently installed version of Geometry Dash is no longer supported. Please update Geometry Dash to version %1$s to continue receiving Geode updates.</string> <string name="launcher_game_update_required">The currently installed version of Geometry Dash is no longer supported. Please update Geometry Dash to continue receiving Geode updates.</string>
<string name="launcher_update_available">A new launcher update is available!</string> <string name="launcher_update_available">A new launcher update is available!</string>
<string name="launcher_update_dismiss">Dismiss</string> <string name="launcher_update_dismiss">Dismiss</string>
@@ -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>
@@ -82,7 +81,7 @@
<string name="launcher_cancelled_crash">Launch paused due to a crash.</string> <string name="launcher_cancelled_crash">Launch paused due to a crash.</string>
<string name="launcher_cancelled_crash_instant">Geode crashed.</string> <string name="launcher_cancelled_crash_instant">Geode crashed.</string>
<string name="launcher_cancelled_error">Geode cannot be used.</string> <string name="launcher_cancelled_error">Geode cannot be used.</string>
<string name="launcher_cancelled_outdated">Geometry Dash version %1$s is required.</string> <string name="launcher_cancelled_outdated">A Geometry Dash update is required.</string>
<string name="launcher_cancelled_restart">Restart</string> <string name="launcher_cancelled_restart">Restart</string>
<string name="launcher_cancelled_resume">Resume</string> <string name="launcher_cancelled_resume">Resume</string>
<string name="launcher_cancelled_retry">Retry</string> <string name="launcher_cancelled_retry">Retry</string>
@@ -105,7 +104,6 @@
<string name="release_fetch_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string> <string name="release_fetch_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string>
<string name="release_fetch_external_in_use_short">Update failed. External library in use.</string> <string name="release_fetch_external_in_use_short">Update failed. External library in use.</string>
<string name="release_fetch_hash_validation_failed_short">Update failed. Integrity check failed.</string>
<string name="release_fetch_no_internet_short">Update failed. Internet not connected.</string> <string name="release_fetch_no_internet_short">Update failed. Internet not connected.</string>
<string name="release_fetch_generic_short">Update failed. Please try again later.</string> <string name="release_fetch_generic_short">Update failed. Please try again later.</string>
@@ -120,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>
@@ -337,8 +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_green">Android</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>
+8 -4
View File
@@ -1,9 +1,13 @@
buildscript {
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.4.1" apply false id("com.android.application") version "9.0.1" apply false
id("com.android.library") version "9.4.1" apply false id("com.android.library") version "9.0.1" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.21" apply false id("org.jetbrains.kotlin.plugin.serialization") version "2.3.10" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.4.21" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
} }
tasks.register<Delete>("clean") { tasks.register<Delete>("clean") {
Binary file not shown.
+2 -3
View File
@@ -1,9 +1,8 @@
#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.7.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+8 -5
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015 the original authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
############################################################################## ##############################################################################
# #
# gradlew start up script for POSIX generated by Gradle. # Gradle start up script for POSIX generated by Gradle.
# #
# Important for running: # Important for running:
# #
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole # bash, then to run this script, type that shell name before the whole
# command line, like: # command line, like:
# #
# ksh gradlew # ksh Gradle
# #
# Busybox and similar reduced shells will NOT work, because this script # Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features: # requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,6 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -171,6 +172,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -210,7 +212,8 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
Vendored
+24 -12
View File
@@ -19,12 +19,12 @@
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem gradlew startup script for Windows @rem Gradle startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled @rem Set local scope for the variables with windows NT shell
setlocal EnableExtensions if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,18 +65,30 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute gradlew @rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel :end
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts @rem End local scope for the variables with windows NT shell
"%COMSPEC%" /c exit %ERRORLEVEL% if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega