Compare commits

...
29 Commits
Author SHA1 Message Date
Chloe df513983d1 cover the license 2026-09-07 17:45:15 -07:00
Chloe a71071e3ac Merge branch 'main' into latency-test 2026-09-07 17:22:30 -07:00
Chloe 3194dff7ff Bundle Geode with Play Store build (#88)
* add gradle task to download geode binaries

* disable automatic update on play build

* get download url from release api

* fix incorrect priorities between external library and prebundled

* bundle geode resources
this also fixes the release api fetch as i never tested it until now, oops

* add message for loading from files dir

* raise minimum gd version with bundled geode

* clear old resources prior to copying
2026-09-07 17:19:12 -07:00
Chloe 8578200db1 Merge branch 'main' into latency-test 2026-09-07 00:52:32 -07:00
Chloe 9e86bbe355 idk where this import went 2026-09-07 00:45:13 -07:00
Chloe adb9dcc1e9 add toggle for frame pacing 2026-09-07 00:41:18 -07:00
Chloe a32589b06c disable auto swap interval 2026-09-07 00:41:18 -07:00
Chloe 1773e54c84 fix surface update being called outside gl thread 2026-09-07 00:41:18 -07:00
Chloe 5718b73470 my evil test 2026-09-07 00:41:17 -07:00
Chloe eef9160970 figure out how to update (almost) everything 2026-09-07 00:40:21 -07:00
Chloe 3b4951def1 implement new release api features (#86)
* implement new release api features

* make hash nullable
2026-08-29 17:06:45 -07:00
Chloe ef44d48ed0 light cleanup of cocos2dxbitmap while i'm at it 2026-08-01 22:56:53 -07:00
Chloe aae2aa2290 fix incorrect behavior in splitString 2026-08-01 22:54:42 -07:00
Oleksandr Nemesh 0a62082519 fix app icon not getting switched on google play (#84) 2026-08-01 04:47:45 +03:00
prevter 758d5ad432 remove some unused permissions
Note: I've tested it a bit and didn't find any problems. This change was required by Google Play to proceed with review
2026-07-05 21:53:55 +03:00
prevter b3a776ce7c fix openFolder util crashing 2026-07-05 21:52:25 +03:00
prevter 72f74fa084 more google play changes 2026-07-01 11:46:54 +03:00
Oleksandr Nemesh 0dba4f0606 add google play support (#82) 2026-06-26 14:28:12 -07:00
Chloe 2016aa4e90 android 17 fix 2026-06-23 19:04:08 -07:00
Chloe 38bc97b7b6 update actions to clear deprecation warnings 2026-04-07 13:04:07 -07:00
Chloe 0a4f13789f fallback to copy if resources rename fails 2026-04-06 04:16:07 -07:00
Chloe 3cb0998274 remove april check completely 2026-04-06 02:15:20 -07:00
Chloe 5d70cb484f update preparation 2026-04-05 23:34:17 -07:00
Chloe a27e67b41e add monochrome icon for sapphire 2026-04-05 23:34:07 -07:00
Chloe 8bff33c013 implement resize callback 2026-04-05 23:03:01 -07:00
Chloe c9c51f0032 remove cutout padding if removed at runtime 2026-04-05 22:54:25 -07:00
Chloe 82b6cf309f fix opening safe mode after crash 2026-04-05 22:49:04 -07:00
Chloe a7654cfc69 add more configuration changes 2026-04-03 18:22:10 -07:00
Chloe c1d136c0cc disable automatic sapphiring 2026-04-03 17:53:37 -07:00
40 changed files with 2636 additions and 357 deletions

No files matched your search

+59 -12
View File
@@ -12,17 +12,23 @@ 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@v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Java JDK - name: Setup Java JDK
uses: actions/setup-java@v4 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with: with:
java-version: 21 java-version: 21
distribution: temurin distribution: temurin
@@ -34,9 +40,19 @@ jobs:
- name: Fix gradle permissions - name: Fix gradle permissions
run: chmod +x ./gradlew run: chmod +x ./gradlew
- name: Build project - name: Build project (standard)
if: ${{ matrix.config.flavor == 'standard' }}
run: | run: |
./gradlew app:assemble${{ matrix.config.name }} \ ./gradlew app:assembleStandard${{ 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}}" \
@@ -44,35 +60,57 @@ 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/${{ matrix.config.lower }}/app-universal-${{ matrix.config.lower }}.apk APK_PATH=${{github.workspace}}/app/build/outputs/apk/standard/${{ matrix.config.lower }}/app-standard-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/${{ matrix.config.lower }}/app-armeabi-v7a-${{ matrix.config.lower }}.apk APK_PATH=${{github.workspace}}/app/build/outputs/apk/standard/${{ matrix.config.lower }}/app-standard-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.lower == 'release' }} if: ${{ matrix.config.flavor == 'standard' && matrix.config.lower == 'release' }}
run: | run: |
SHORT_SHA=${GITHUB_SHA::7} SHORT_SHA=${GITHUB_SHA::7}
MAPPING_PATH=${{github.workspace}}/app/build/outputs/mapping/${{ matrix.config.lower }}/mapping.txt MAPPING_PATH=${{github.workspace}}/app/build/outputs/mapping/standardRelease/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@v4 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: ${{ matrix.config.lower == 'release' }} if: ${{ matrix.config.flavor == 'standard' && matrix.config.lower == 'release' }}
with: with:
name: geode-launcher-${{ matrix.config.lower }} name: geode-launcher-${{ matrix.config.lower }}
path: | path: |
@@ -81,10 +119,19 @@ jobs:
${{ steps.rename_mappings.outputs.path }} ${{ steps.rename_mappings.outputs.path }}
- name: Upload debug build - name: Upload debug build
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: ${{ matrix.config.lower == 'debug' }} if: ${{ matrix.config.flavor == 'standard' && 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 }}
+283 -20
View File
@@ -1,4 +1,8 @@
val composeBOM: String by rootProject.extra import groovy.json.JsonSlurper
import java.io.IOException
import java.net.URI
import java.security.MessageDigest
import java.util.zip.ZipInputStream
kotlin { kotlin {
compilerOptions { compilerOptions {
@@ -12,15 +16,241 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose") id("org.jetbrains.kotlin.plugin.compose")
} }
val currentGeodeVersion = "5.10.1"
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 = 36 compileSdk = 37
defaultConfig { defaultConfig {
applicationId = "com.geode.launcher" applicationId = "com.geode.launcher"
minSdk = 23 minSdk = 23
targetSdk = 36 targetSdk = 36
versionCode = 28 versionCode = 30
versionName = "1.7.5" versionName = "1.8.0"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
@@ -39,7 +269,7 @@ android {
splits { splits {
abi { abi {
isEnable = true isEnable = !project.hasProperty("disableAbiSplits")
reset() reset()
//noinspection ChromeOsAbiSupport. i'm sorry! //noinspection ChromeOsAbiSupport. i'm sorry!
@@ -49,6 +279,24 @@ 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
@@ -64,12 +312,13 @@ 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_1_8 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_11
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true buildConfig = true
prefab = true
} }
packaging { packaging {
resources { resources {
@@ -86,29 +335,43 @@ android {
ndkVersion = "29.0.14206865" 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:$composeBOM")) implementation (platform("androidx.compose:compose-bom:2026.08.00"))
implementation ("androidx.core:core-ktx:1.18.0") implementation ("androidx.core:core-ktx:1.19.0")
implementation ("androidx.compose.ui:ui") implementation ("androidx.compose.ui:ui")
implementation ("androidx.compose.material3:material3") implementation ("androidx.compose.material3:material3")
implementation ("androidx.compose.ui:ui-tooling-preview") implementation ("androidx.compose.ui:ui-tooling-preview")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0")
implementation ("androidx.activity:activity-compose:1.13.0") implementation ("androidx.activity:activity-compose:1.13.0")
implementation ("androidx.activity:activity-ktx:1.13.0") implementation ("androidx.activity:activity-ktx:1.13.0")
implementation ("androidx.appcompat:appcompat:1.7.1") implementation ("androidx.appcompat:appcompat:1.8.0")
implementation ("androidx.documentfile:documentfile:1.1.0") implementation ("androidx.documentfile:documentfile:1.1.0")
implementation ("androidx.games:games-frame-pacing:2.1.3")
implementation ("com.squareup.okio:okio:3.17.0") implementation ("com.squareup.okio:okio:3.18.2")
implementation ("com.squareup.okhttp3:okhttp:5.3.2") implementation ("com.squareup.okhttp3:okhttp:5.5.0")
implementation ("com.squareup.okhttp3:okhttp-coroutines:5.3.2") implementation ("com.squareup.okhttp3:okhttp-coroutines:5.5.0")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0") implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.10.0") implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.11.0")
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.2") implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.45.0")
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2") implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.45.0")
implementation ("androidx.browser:browser:1.10.0") implementation ("androidx.browser:browser:1.10.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
@@ -0,0 +1,9 @@
<?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>
+3 -5
View File
@@ -17,8 +17,6 @@
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" />
@@ -127,7 +125,7 @@
</activity> </activity>
<activity <activity
android:name=".GeometryDashActivity" android:name=".GeometryDashActivity"
android:configChanges="orientation|screenSize|smallestScreenSize" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:screenOrientation="landscape" android:screenOrientation="landscape"
@@ -238,7 +236,7 @@
<provider <provider
android:name=".UserDirectoryProvider" android:name=".UserDirectoryProvider"
android:authorities="com.geode.launcher.user" android:authorities="${applicationId}.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">
@@ -248,7 +246,7 @@
</provider> </provider>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="com.geode.launcher.fileprovider" android:authorities="${applicationId}.fileprovider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
+5 -2
View File
@@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.21)
project(launcherfix) project(launcherfix)
add_library(launcherfix SHARED launcher-fix.cpp patcher.cpp) find_package(games-frame-pacing REQUIRED CONFIG)
add_library(launcherfix SHARED launcher-fix.cpp patcher.cpp pacing.cpp)
target_compile_features(launcherfix PUBLIC cxx_std_20) target_compile_features(launcherfix PUBLIC cxx_std_20)
set_target_properties(launcherfix PROPERTIES CXX_EXTENSIONS OFF) set_target_properties(launcherfix PROPERTIES CXX_EXTENSIONS OFF)
@@ -14,4 +16,5 @@ set_target_properties(launcherfix PROPERTIES
CXX_VISIBILITY_PRESET hidden CXX_VISIBILITY_PRESET hidden
) )
target_link_libraries(launcherfix PRIVATE log) target_link_libraries(launcherfix PRIVATE log android)
target_link_libraries(launcherfix PRIVATE games-frame-pacing::swappy_static)
+65
View File
@@ -0,0 +1,65 @@
#include <swappy/swappyGL.h>
#include <swappy/swappyGL_extra.h>
#include <android/native_window_jni.h>
#include <memory>
extern "C"
JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_initFramePacing(
JNIEnv *env,
jobject,
jobject activity
) {
SwappyGL_init(env, activity);
}
struct NativeWindowDeleter {
void operator()(ANativeWindow* p) const {
if (p != nullptr) ANativeWindow_release(p);
}
};
std::unique_ptr<ANativeWindow, NativeWindowDeleter> native_window = nullptr;
extern "C"
JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_destroyFramePacing(JNIEnv*, jobject) {
SwappyGL_destroy();
}
extern "C"
JNIEXPORT jboolean JNICALL Java_com_geode_launcher_LauncherFix_swapFrame(
JNIEnv*,
jobject,
jlong display,
jlong surface
) {
return SwappyGL_swap(
reinterpret_cast<EGLDisplay>(display),
reinterpret_cast<EGLSurface>(surface)
);
}
extern "C"
JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setSurface(
JNIEnv* env,
jobject,
jobject surface
) {
if (surface == nullptr) {
native_window.reset();
SwappyGL_setWindow(nullptr);
return;
}
native_window.reset(ANativeWindow_fromSurface(env, surface));
SwappyGL_setWindow(native_window.get());
}
extern "C"
JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setSwapInterval(
JNIEnv* env,
jobject,
jlong interval_ns
) {
SwappyGL_setAutoSwapInterval(false);
SwappyGL_setSwapIntervalNS(interval_ns);
}
File diff suppressed because it is too large. Load diff
@@ -5,7 +5,9 @@ import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.opengl.GLSurfaceView import android.opengl.EGL14
import android.opengl.EGLConfig
import android.opengl.EGLDisplay
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
@@ -43,9 +45,6 @@ import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import javax.microedition.khronos.egl.EGL10
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.egl.EGLDisplay
enum class DisplayMode { enum class DisplayMode {
DEFAULT, LIMITED, FULLSCREEN; DEFAULT, LIMITED, FULLSCREEN;
@@ -78,6 +77,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private var mAspectRatio = 0.0f private var mAspectRatio = 0.0f
private var mScreenZoom = 1.0f private var mScreenZoom = 1.0f
private var mScreenZoomFit = false private var mScreenZoomFit = false
private var mNativeFramePacing = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setupUIState() setupUIState()
@@ -131,6 +131,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
}) })
mGLSurfaceView?.manualBackEvents = true mGLSurfaceView?.manualBackEvents = true
if (mNativeFramePacing) {
LauncherFix.initFramePacing(this)
}
} }
private fun createVersionFile() { private fun createVersionFile() {
@@ -200,6 +204,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
right = displayInsets.safeInsetRight, right = displayInsets.safeInsetRight,
bottom = displayInsets.safeInsetBottom bottom = displayInsets.safeInsetBottom
) )
} else {
v.updatePadding(
left = 0, top = 0, right = 0, bottom = 0
)
} }
WindowInsets.CONSUMED WindowInsets.CONSUMED
} }
@@ -375,8 +383,15 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
return "$sourceDir!/lib/$architecture/" return "$sourceDir!/lib/$architecture/"
} }
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadGeodeLibrary() { private fun loadGeodeLibrary() {
@Suppress("SENSELESS_COMPARISON")
if (BuildConfig.PREBUNDLED_GEODE != null) {
// attempt to load an externally provided Geode first
if (!loadExternalGeode()) {
copyGeodeResources()
System.loadLibrary("geode")
}
} else {
// Load Geode if exists // Load Geode if exists
// bundling the object with the application allows for nicer backtraces // bundling the object with the application allows for nicer backtraces
try { try {
@@ -384,18 +399,29 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
System.loadLibrary("geode") System.loadLibrary("geode")
return return
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
if (!loadExternalGeode()) {
throw e
}
}
}
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadExternalGeode(): Boolean {
// but users may prefer it stored with data // but users may prefer it stored with data
val geodeFilename = LaunchUtils.geodeFilename val geodeFilename = LaunchUtils.geodeFilename
val geodePath = File(filesDir.path, "launcher/$geodeFilename") val geodePath = File(filesDir.path, "launcher/$geodeFilename")
if (geodePath.exists()) { if (geodePath.exists()) {
println("Loading Geode from ${geodePath.path}")
System.load(geodePath.path) System.load(geodePath.path)
return return true
} }
// you know zmx i have 0 clue what this does so im // you know zmx i have 0 clue what this does so im
// just gonna like copy the binary from external // just gonna like copy the binary from external
// also i get 20 million permission denied errors // also i get 20 million permission denied errors
val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!! val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)
?: return false
val copiedPath = File(filesDir.path, "copied") val copiedPath = File(filesDir.path, "copied")
if (copiedPath.exists()) { if (copiedPath.exists()) {
@@ -412,14 +438,16 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
) )
if (copiedGeodePath.exists()) { if (copiedGeodePath.exists()) {
// android 17 fix
copiedGeodePath.setWritable(false)
println("Loading Geode from ${externalGeodePath.name}") println("Loading Geode from ${externalGeodePath.name}")
System.load(copiedGeodePath.path) System.load(copiedGeodePath.path)
return return true
} }
} }
throw e return false
}
} }
private fun createView(): FrameLayout { private fun createView(): FrameLayout {
@@ -467,9 +495,11 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
val frameRate = mLimitedRefreshRate val frameRate = mLimitedRefreshRate
if (frameRate != null) { if (frameRate != null) {
renderer.limitFrameRate(frameRate) glSurfaceView.setMaxFrameRate(frameRate)
} }
glSurfaceView.nativeFramePacing = mNativeFramePacing
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
@@ -505,6 +535,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
} }
mNativeFramePacing = preferenceUtils.getBoolean(PreferenceUtils.Key.NATIVE_FRAME_PACING)
if (displayMode == DisplayMode.FULLSCREEN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (displayMode == DisplayMode.FULLSCREEN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
GeodeUtils.handleSafeArea = true GeodeUtils.handleSafeArea = true
@@ -538,6 +570,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
FMOD.close() FMOD.close()
if (mNativeFramePacing) {
LauncherFix.destroyFramePacing()
}
} }
private fun resumeGame() { private fun resumeGame() {
@@ -619,6 +655,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
mGLSurfaceView?.enableControllerEvents() mGLSurfaceView?.enableControllerEvents()
true true
} }
GeodeUtils.CAPABILITY_RESIZE_CALLBACK -> {
mGLSurfaceView?.supportsResizeEvents = true
true
}
else -> false else -> false
} }
} }
@@ -655,11 +695,57 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
} }
class EGLConfigChooser : GLSurfaceView.EGLConfigChooser { private fun copyGeodeResources() {
// this comes from EGL14, but is unavailable on EGL10 @Suppress("SENSELESS_COMPARISON")
// also EGL14 is incompatible with EGL10. so whatever if (BuildConfig.PREBUNDLED_GEODE == null) {
private var EGL_OPENGL_ES2_BIT: Int = 0x04 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 : ExplicitGLSurfaceView.EGLConfigChooser {
private data class ConfigValues( private data class ConfigValues(
val redSize: Int, val redSize: Int,
val greenSize: Int, val greenSize: Int,
@@ -669,29 +755,29 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
val stencilSize: Int, val stencilSize: Int,
) )
private fun getAttribValue(egl: EGL10, display: EGLDisplay, config: EGLConfig, attrib: Int): Int { private fun getAttribValue(display: EGLDisplay, config: EGLConfig, attrib: Int): Int {
val value = IntArray(1) val value = IntArray(1)
egl.eglGetConfigAttrib(display, config, attrib, value) EGL14.eglGetConfigAttrib(display, config, attrib, value, 0)
return value[0] return value[0]
} }
private fun testConfig(egl: EGL10, display: EGLDisplay, attrib: ConfigValues): EGLConfig? { private fun testConfig(display: EGLDisplay, attrib: ConfigValues): EGLConfig? {
val attribList = intArrayOf( val attribList = intArrayOf(
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL10.EGL_RED_SIZE, attrib.redSize, EGL14.EGL_RED_SIZE, attrib.redSize,
EGL10.EGL_GREEN_SIZE, attrib.greenSize, EGL14.EGL_GREEN_SIZE, attrib.greenSize,
EGL10.EGL_BLUE_SIZE, attrib.blueSize, EGL14.EGL_BLUE_SIZE, attrib.blueSize,
EGL10.EGL_ALPHA_SIZE, attrib.alphaSize, EGL14.EGL_ALPHA_SIZE, attrib.alphaSize,
EGL10.EGL_DEPTH_SIZE, attrib.depthSize, EGL14.EGL_DEPTH_SIZE, attrib.depthSize,
EGL10.EGL_STENCIL_SIZE, attrib.stencilSize, EGL14.EGL_STENCIL_SIZE, attrib.stencilSize,
EGL10.EGL_NONE EGL14.EGL_NONE
) )
val configs = arrayOfNulls<EGLConfig>(1) val configs = arrayOfNulls<EGLConfig>(1)
val numConfigs = IntArray(1) val numConfigs = IntArray(1)
val res = egl.eglChooseConfig(display, attribList, configs, configs.size, numConfigs) val res = EGL14.eglChooseConfig(display, attribList, 0, configs, 0, configs.size, numConfigs, 0)
if (res && numConfigs[0] > 0 && configs[0] != null) { if (res && numConfigs[0] > 0 && configs[0] != null) {
return configs[0] return configs[0]
} }
@@ -699,18 +785,18 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
return null return null
} }
private fun printConfig(egl: EGL10, display: EGLDisplay, config: EGLConfig) { private fun printConfig(display: EGLDisplay, config: EGLConfig) {
val id = getAttribValue(egl, display, config, EGL10.EGL_CONFIG_ID) val id = getAttribValue(display, config, EGL14.EGL_CONFIG_ID)
val red = getAttribValue(egl, display, config, EGL10.EGL_RED_SIZE) val red = getAttribValue(display, config, EGL14.EGL_RED_SIZE)
val green = getAttribValue(egl, display, config, EGL10.EGL_GREEN_SIZE) val green = getAttribValue(display, config, EGL14.EGL_GREEN_SIZE)
val blue = getAttribValue(egl, display, config, EGL10.EGL_BLUE_SIZE) val blue = getAttribValue(display, config, EGL14.EGL_BLUE_SIZE)
val alpha = getAttribValue(egl, display, config, EGL10.EGL_ALPHA_SIZE) val alpha = getAttribValue(display, config, EGL14.EGL_ALPHA_SIZE)
val depth = getAttribValue(egl, display, config, EGL10.EGL_DEPTH_SIZE) val depth = getAttribValue(display, config, EGL14.EGL_DEPTH_SIZE)
val stencil = getAttribValue(egl, display, config, EGL10.EGL_STENCIL_SIZE) val stencil = getAttribValue(display, config, EGL14.EGL_STENCIL_SIZE)
println("EGLConfig $id: (red = $red, green = $green, blue = $blue, alpha = $alpha, depth = $depth, stencil = $stencil)") println("EGLConfig $id: (red = $red, green = $green, blue = $blue, alpha = $alpha, depth = $depth, stencil = $stencil)")
} }
override fun chooseConfig(egl: EGL10, display: EGLDisplay): EGLConfig { override fun chooseConfig(display: EGLDisplay): EGLConfig {
val configs = listOf( val configs = listOf(
ConfigValues(8, 8, 8, 8, 16, 8), ConfigValues(8, 8, 8, 8, 16, 8),
ConfigValues(8, 8, 8, 0, 16, 8), ConfigValues(8, 8, 8, 0, 16, 8),
@@ -719,9 +805,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
) )
return configs.firstNotNullOf { return configs.firstNotNullOf {
val config = testConfig(egl, display, it) val config = testConfig(display, it)
if (config != null) { if (config != null) {
printConfig(egl, display, config) printConfig(display, config)
} }
config config
@@ -1,5 +1,7 @@
package com.geode.launcher package com.geode.launcher
import android.app.Activity
import android.view.Surface
import androidx.annotation.Keep import androidx.annotation.Keep
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
@@ -16,4 +18,10 @@ object LauncherFix {
external fun enableExceptionsRenaming() external fun enableExceptionsRenaming()
external fun performPatches() external fun performPatches()
external fun initFramePacing(activity: Activity)
external fun destroyFramePacing()
external fun swapFrame(display: Long, surface: Long): Boolean
external fun setSurface(surface: Surface?)
external fun setSwapInterval(intervalNs: Long)
} }
@@ -92,7 +92,6 @@ 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
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import com.geode.launcher.utils.checkIconDate
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.ConnectException import java.net.ConnectException
@@ -112,6 +111,16 @@ class MainActivity : ComponentActivity() {
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)
@@ -208,7 +217,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) details = stringResource(R.string.launcher_cancelled_outdated, GamePackageUtils.getSupportedUnifiedVersion())
) )
LaunchViewModel.LaunchCancelReason.AUTOMATIC, LaunchViewModel.LaunchCancelReason.AUTOMATIC,
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo( LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
@@ -257,7 +266,7 @@ fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Bool
) )
} }
is LaunchViewModel.LaunchUIState.Cancelled -> { is LaunchViewModel.LaunchUIState.Cancelled -> {
return mapCancelReasonToInfo(state.reason) mapCancelReasonToInfo(state.reason)
} }
} }
} }
@@ -717,6 +726,9 @@ 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)
} }
} }
@@ -779,7 +791,7 @@ fun AltMainScreen(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
val sapphireLogo = remember { val sapphireLogo = remember {
checkIconDate() || PreferenceUtils.get(context) PreferenceUtils.get(context)
.getString(PreferenceUtils.Key.SELECTED_ICON) == ApplicationIcon.SAPPHIRE.toId() .getString(PreferenceUtils.Key.SELECTED_ICON) == ApplicationIcon.SAPPHIRE.toId()
} }
@@ -826,10 +838,11 @@ 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
if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) { val currentUIState = launchUIState
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) { if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING && currentUIState.reason != LaunchViewModel.LaunchCancelReason.GAME_OUTDATED) {
UnsupportedVersionWarning() UnsupportedVersionWarning()
} }
@@ -202,6 +202,10 @@ 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")
if (BuildConfig.PREBUNDLED_GEODE != null)
"${BuildConfig.PREBUNDLED_GEODE}*"
else
PreferenceUtils.get(context).getString(PreferenceUtils.Key.CURRENT_VERSION_TAG) PreferenceUtils.get(context).getString(PreferenceUtils.Key.CURRENT_VERSION_TAG)
} }
@@ -36,6 +36,7 @@ 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
@@ -142,7 +143,7 @@ fun onSettings(context: Context) {
} }
@Composable @Composable
fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, enabled: Boolean = true, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit) { fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, 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) }
@@ -154,11 +155,11 @@ fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, enabled: Boole
interactionSource.interactions.collectLatest { interaction -> interactionSource.interactions.collectLatest { interaction ->
when (interaction) { when (interaction) {
is PressInteraction.Press -> { is PressInteraction.Press -> {
delay(viewConfiguration.longPressTimeoutMillis) delay(viewConfiguration.longPressTimeoutMillis.milliseconds)
// 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) delay(viewConfiguration.longPressTimeoutMillis.milliseconds)
haptics.performHapticFeedback(HapticFeedbackType.LongPress) haptics.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -186,6 +187,8 @@ fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, enabled: Boole
@Composable @Composable
fun UnsupportedVersionWarning(modifier: Modifier = Modifier) { fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
val gameVersion = GamePackageUtils.getSupportedUnifiedVersion()
InlineDialog( InlineDialog(
headline = { headline = {
Icon( Icon(
@@ -201,7 +204,7 @@ fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
}, },
body = { body = {
Text( Text(
stringResource(R.string.launcher_game_update_required), stringResource(R.string.launcher_game_update_required, gameVersion),
) )
}, },
modifier = modifier.padding(8.dp) modifier = modifier.padding(8.dp)
@@ -39,6 +39,8 @@ 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;
@@ -87,7 +89,7 @@ fun NotificationCardFromType(type: LaunchNotificationType) {
} }
AnimatedNotificationCard( AnimatedNotificationCard(
displayLength = 5000L, displayLength = 5.seconds,
onClick = { onClick = {
showInfoDialog = true showInfoDialog = true
} }
@@ -140,15 +142,15 @@ fun LaunchNotification(cards: List<LaunchNotificationType>) {
} }
@Composable @Composable
fun AnimatedNotificationCard(modifier: Modifier = Modifier, visibilityDelay: Long = 0L, displayLength: Long = 3000L, onClick: (() -> Unit)? = null, contents: @Composable () -> Unit) { fun AnimatedNotificationCard(modifier: Modifier = Modifier, visibilityDelay: Duration = Duration.ZERO, displayLength: Duration = 3.seconds, onClick: (() -> Unit)? = null, contents: @Composable () -> Unit) {
val state = remember { val state = remember {
MutableTransitionState(false).apply { MutableTransitionState(false).apply {
targetState = visibilityDelay <= 0 targetState = visibilityDelay <= Duration.ZERO
} }
} }
LaunchedEffect(true) { LaunchedEffect(true) {
if (visibilityDelay > 0) { if (visibilityDelay > Duration.ZERO) {
delay(visibilityDelay) delay(visibilityDelay)
state.targetState = true state.targetState = true
} }
@@ -7,6 +7,7 @@ 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
@@ -182,18 +183,9 @@ class LaunchViewModel(private val application: Application): ViewModel() {
return return
} }
// last launch crashed, so cancel // play store build only comes with the one build of geode
if (loadFailure != null) { val minVersionCode = if (BuildConfig.GOOGLE_PLAY_BUILD) Constants.SUPPORTED_VERSION_CODE else Constants.SUPPORTED_VERSION_CODE_MIN
// show a different message if this is the first thing users see if (GamePackageUtils.getGameVersionCode(packageManager) < minVersionCode) {
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
} }
@@ -205,6 +197,18 @@ 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
} }
@@ -247,6 +251,7 @@ 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)
@@ -261,11 +266,16 @@ class LaunchViewModel(private val application: Application): ViewModel() {
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,7 +4,6 @@ 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
@@ -60,7 +60,6 @@ import com.geode.launcher.utils.ApplicationIcon
import com.geode.launcher.utils.ApplicationIconDetails import com.geode.launcher.utils.ApplicationIconDetails
import com.geode.launcher.utils.IconUtils import com.geode.launcher.utils.IconUtils
import com.geode.launcher.utils.adaptiveIconPainterResource import com.geode.launcher.utils.adaptiveIconPainterResource
import com.geode.launcher.utils.checkIconDate
class ApplicationIconActivity : ComponentActivity() { class ApplicationIconActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -130,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}.${iconData.component}"), ComponentName(context, "${context.packageName.removeSuffix(".play")}.${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,
@@ -232,18 +231,14 @@ fun ApplicationIconScreen(
) { ) {
val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON) val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
val aprilFools = remember { checkIconDate() }
Column(Modifier Column(Modifier
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
) { ) {
ApplicationIcon.entries.forEach { ApplicationIcon.entries.forEach {
val iconData = IconUtils.getIconDetails(it) val iconData = IconUtils.getIconDetails(it)
if (!iconData.checkDate || aprilFools) {
IconCard(iconData, selectedIcon == iconData.id) IconCard(iconData, selectedIcon == iconData.id)
} }
}
} }
@@ -40,6 +40,7 @@ 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
@@ -168,6 +169,12 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
} }
) )
SettingsCard(
title = stringResource(R.string.preference_native_pacing),
description = stringResource(R.string.preference_native_pacing_description),
preferenceKey = PreferenceUtils.Key.NATIVE_FRAME_PACING,
)
/* /*
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
SettingsCard( SettingsCard(
@@ -204,6 +211,7 @@ 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
@@ -227,6 +235,7 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
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)) {
SettingsSelectCard( SettingsSelectCard(
@@ -70,7 +70,6 @@ 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
@@ -510,6 +509,10 @@ 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)) {
@@ -661,11 +664,14 @@ 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,
currentRelease ?: "unknown" if (BuildConfig.PREBUNDLED_GEODE != null)
"${BuildConfig.PREBUNDLED_GEODE}*"
else (currentRelease ?: "unknown")
), ),
displayInline = true displayInline = true
) )
@@ -3,10 +3,9 @@ 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 @OptIn(ExperimentalTime::class) constructor( data class Asset(
val url: String, val url: String,
val id: Int, val id: Int,
val name: String, val name: String,
@@ -14,10 +13,11 @@ data class Asset @OptIn(ExperimentalTime::class) constructor(
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 @OptIn(ExperimentalTime::class) constructor( data class Release(
val url: String, val url: String,
val id: Int, val id: Int,
val targetCommitish: String, val targetCommitish: String,
@@ -31,12 +31,29 @@ data class Release @OptIn(ExperimentalTime::class) constructor(
) )
@Serializable @Serializable
data class LoaderVersion @OptIn(ExperimentalTime::class) constructor( data class LoaderPlatformDownload(
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
@@ -45,7 +62,25 @@ data class LoaderPayload<T>(
val error: String val error: String
) )
data class DownloadableAsset(val url: String, val filename: String, val size: Long? = null) data class DownloadableAsset(
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 {
/** /**
@@ -83,7 +118,6 @@ 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
} }
@@ -100,11 +134,7 @@ 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 DownloadableAsset( return mapDownload(download)
url = download.browserDownloadUrl,
filename = download.name,
size = download.size.toLong()
)
} }
override fun getResourcesDownload(): DownloadableAsset? { override fun getResourcesDownload(): DownloadableAsset? {
@@ -112,11 +142,7 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
it.name == "resources.zip" it.name == "resources.zip"
} ?: return null } ?: return null
return DownloadableAsset( return mapDownload(download)
url = download.browserDownloadUrl,
filename = download.name,
size = download.size.toLong()
)
} }
} }
@@ -125,7 +151,6 @@ 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
} }
@@ -149,11 +174,7 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
override fun getDownload(): DownloadableAsset? { override fun getDownload(): DownloadableAsset? {
val download = getGitHubDownload() ?: return null val download = getGitHubDownload() ?: return null
return DownloadableAsset( return mapDownload(download)
url = download.browserDownloadUrl,
filename = download.name,
size = download.size.toLong()
)
} }
override fun getResourcesDownload(): DownloadableAsset? { override fun getResourcesDownload(): DownloadableAsset? {
@@ -166,24 +187,27 @@ 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 = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename", url = data.url,
filename = filename filename = filename,
hash = data.hash,
) )
} }
override fun getResourcesDownload(): DownloadableAsset { override fun getResourcesDownload(): DownloadableAsset {
val data = version.downloads.resources
val filename = "resources.zip" val filename = "resources.zip"
return DownloadableAsset( return DownloadableAsset(
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/resources.zip", url = data.url,
filename = filename filename = filename,
hash = data.hash,
) )
} }
} }
@@ -25,8 +25,8 @@ import okio.sink
import okio.source import okio.source
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException
import java.io.InterruptedIOException import java.io.InterruptedIOException
import java.io.IOException
import kotlin.math.max 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,7 +75,8 @@ 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
@@ -155,17 +156,17 @@ class ReleaseManager private constructor(
val outputFile = File(outputDirectory, download.filename) val outputFile = File(outputDirectory, download.filename)
try { try {
DownloadUtils.downloadStream( val hash = DownloadUtils.downloadFile(
httpClient, httpClient,
download.url, download.url,
onResponse = { body -> outputFile
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)
} }
@@ -183,41 +184,52 @@ class ReleaseManager private constructor(
val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext) val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext)
val downloadFile = getTempFile("resources.zip")
try { try {
if (!outputDir.exists()) { if (!outputDir.exists()) {
outputDir.mkdirs() outputDir.mkdirs()
} }
DownloadUtils.downloadStream( val hash = DownloadUtils.downloadFile(
httpClient, httpClient,
resourceAsset.url, resourceAsset.url,
downloadFile,
onProgress = { progress, outOf -> onProgress = { progress, outOf ->
if (!skipStateUpdate) if (!skipStateUpdate)
_uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize) _uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize)
}, },
onResponse = { body -> )
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( DownloadUtils.copyZipStreamToDirectory(
body.byteStream(), zip,
outputDir outputDir
) )
}
if (finalDir.exists()) { if (finalDir.exists()) {
finalDir.deleteRecursively() finalDir.deleteRecursively()
} }
if (!outputDir.renameTo(finalDir)) { if (!outputDir.renameTo(finalDir)) {
throw IOException("failed to rename resources output directory") println("Failed to rename temporary directory!")
DownloadUtils.copyDirectory(outputDir, finalDir)
} }
}
)
} catch (e: Exception) { } catch (e: Exception) {
sendError(e) sendError(e)
return false return false
} finally { } finally {
val tempPathClone = File(outputPath) val tempPathClone = File(outputPath)
runCatching { try {
if (downloadFile.exists()) downloadFile.delete()
if (tempPathClone.exists()) tempPathClone.deleteRecursively() if (tempPathClone.exists()) tempPathClone.deleteRecursively()
} } catch (_: IOException) {}
} }
return true return true
@@ -243,12 +255,14 @@ class ReleaseManager private constructor(
val initialSize = if (releaseAsset.size == null && resourcesAsset?.size == null) null else releaseSize + resourcesSize val initialSize = if (releaseAsset.size == null && resourcesAsset?.size == null) null else releaseSize + resourcesSize
_uiState.value = ReleaseManagerState.InDownload(0, initialSize) _uiState.value = ReleaseManagerState.InDownload(0, initialSize)
val outputFile = getTempFile() val outputFile = getTempFile(LaunchUtils.geodeFilename)
// 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
@@ -256,21 +270,30 @@ class ReleaseManager private constructor(
geodeParent.mkdirs() geodeParent.mkdirs()
} }
DownloadUtils.downloadStream( val hash = DownloadUtils.downloadFile(
httpClient, httpClient,
releaseAsset.url, releaseAsset.url,
downloadFile,
onProgress = { progress, outOf -> onProgress = { progress, outOf ->
if (!skipStateUpdate) if (!skipStateUpdate)
_uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize) _uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize)
releaseSize = max(outOf, releaseSize) releaseSize = max(outOf, releaseSize)
}, }
onResponse = { body -> )
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( DownloadUtils.extractFileFromZipStream(
body.byteStream(), zip,
outputFile, outputFile,
geodeFile.name geodeFile.name
) )
}
// work around a permission issue from adb push // work around a permission issue from adb push
if (geodeFile.exists()) { if (geodeFile.exists()) {
@@ -285,16 +308,15 @@ class ReleaseManager private constructor(
// attempt a manual copy if rename fails for whatever reason // attempt a manual copy if rename fails for whatever reason
DownloadUtils.copyFile(outputFile, geodeFile) 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)
runCatching { try {
if (downloadFile.exists()) downloadFile.delete()
if (tempFileClone.exists()) tempFileClone.delete() if (tempFileClone.exists()) tempFileClone.delete()
} } catch (_: IOException) {}
} }
if (resourcesAsset != null) { if (resourcesAsset != null) {
@@ -330,6 +352,11 @@ class ReleaseManager private constructor(
} }
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) {
@@ -339,6 +366,8 @@ 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
@@ -355,6 +384,8 @@ 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
@@ -364,6 +395,12 @@ 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())
@@ -372,6 +409,10 @@ 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
@@ -390,6 +431,11 @@ 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()) {
@@ -480,15 +526,14 @@ class ReleaseManager private constructor(
} }
} }
private fun getTempFile(): File { private fun getTempFile(filename: String): File {
val geodeName = LaunchUtils.geodeFilename
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 suffix = createRandomString()
val tmpName = "tmp-$suffix.$geodeName" val tmpName = "tmp-$suffix.$filename"
val tempFile = File(geodeDirectory, tmpName) val tempFile = File(geodeDirectory, tmpName)
@@ -7,6 +7,7 @@ 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
@@ -89,6 +90,10 @@ 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,10 +17,12 @@ 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
@@ -32,12 +34,31 @@ 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,
onResponse: suspend (ResponseBody) -> Unit, ): ResponseBody {
) {
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.build() .build()
@@ -64,12 +85,15 @@ object DownloadUtils {
val progressClient = progressClientBuilder.build() val progressClient = progressClientBuilder.build()
val call = progressClient.newCall(request) val call = progressClient.newCall(request)
call.executeAsync().use { response -> val response = call.executeAsync()
when (response.code) {
200 -> onResponse(response.body) val responseCode = response.code
else -> throw IOException("unexpected response ${response.code}") if (responseCode != 200) {
} response.close()
throw IOException("unexpected response ${response.code}")
} }
return response.body
} }
suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible { suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible {
@@ -160,6 +184,13 @@ 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,6 +8,7 @@ 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
@@ -86,7 +87,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(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)) val contentUri = ContentUris.withAppendedId("content://downloads/public_downloads".toUri(), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null) return getDataColumn(context, contentUri, null, null)
} }
@@ -63,6 +63,10 @@ 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,7 +32,6 @@ 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
@@ -215,13 +214,15 @@ 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/data/${context.packageName}/$relativePath"
} else {
"primary:Android/media/${context.packageName}/$relativePath" "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 {
@@ -409,7 +410,7 @@ object GeodeUtils {
} }
fun isGeodeUri(uri: Uri): Boolean { fun isGeodeUri(uri: Uri): Boolean {
return "com.geode.launcher.user" == uri.authority return "${BuildConfig.APPLICATION_ID}.user" == uri.authority
} }
private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal" private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal"
@@ -428,7 +429,7 @@ object GeodeUtils {
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) )
return permissions.all { permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
} }
} }
@@ -557,6 +558,7 @@ object GeodeUtils {
const val CAPABILITY_EXTENDED_INPUT = "extended_input" const val CAPABILITY_EXTENDED_INPUT = "extended_input"
const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs" const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs"
const val CAPABILITY_RICH_INPUT = "internal_callbacks_v2" const val CAPABILITY_RICH_INPUT = "internal_callbacks_v2"
const val CAPABILITY_RESIZE_CALLBACK = "resize_callbacks"
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null) private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
@@ -15,10 +15,6 @@ import androidx.compose.ui.res.painterResource
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.geode.launcher.R import com.geode.launcher.R
import kotlinx.datetime.Month
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import kotlin.time.Clock
// https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486 // https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486
@Composable @Composable
@@ -70,17 +66,11 @@ enum class ApplicationIcon {
} }
} }
fun checkIconDate(): Boolean {
val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
return currentDate.month == Month.APRIL && currentDate.day <= 7
}
data class ApplicationIconDetails( data class ApplicationIconDetails(
val id: String, val id: String,
val component: String, val component: String,
@param:DrawableRes val iconId: Int, @param:DrawableRes val iconId: Int,
@param:StringRes val nameId: Int, @param:StringRes val nameId: Int,
val checkDate: Boolean = false,
) )
object IconUtils { object IconUtils {
@@ -114,7 +104,6 @@ object IconUtils {
component = "MainActivitySapphire", component = "MainActivitySapphire",
iconId = R.mipmap.ic_launcher_sapphire, iconId = R.mipmap.ic_launcher_sapphire,
nameId = R.string.application_icon_sapphire, nameId = R.string.application_icon_sapphire,
checkDate = true
) )
} }
} }
@@ -2,6 +2,7 @@ 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
@@ -41,6 +42,9 @@ 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
} }
@@ -48,10 +52,14 @@ 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) {
context.getExternalFilesDir(null)!!.parentFile!!
} else {
// deprecated, but seems to be the best choice of directory (i forced mat to test it) // deprecated, but seems to be the best choice of directory (i forced mat to test it)
// also, is getting the first item the correct choice here?? what do they mean // also, is getting the first item the correct choice here?? what do they mean
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val dir = context.externalMediaDirs.first() 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...
@@ -149,6 +149,8 @@ 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,
NATIVE_FRAME_PACING,
} }
private fun defaultValueForBooleanKey(key: Key): Boolean { private fun defaultValueForBooleanKey(key: Key): Boolean {
@@ -199,6 +201,8 @@ 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.NATIVE_FRAME_PACING -> "PreferenceNativeFramePacing"
// 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,17 +6,19 @@ 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: Long, time: Duration,
onCountdownFinish: () -> Unit onCountdownFinish: () -> Unit
): Long { ): Long {
var millisUntilFinished by remember { var millisUntilFinished by remember {
mutableLongStateOf(time) mutableStateOf(time)
} }
var shouldBeCounting by remember { var shouldBeCounting by remember {
@@ -28,9 +30,9 @@ fun useCountdownTimer(
return@LaunchedEffect return@LaunchedEffect
} }
if (millisUntilFinished > 0) { if (millisUntilFinished > Duration.ZERO) {
delay(MS_TO_SEC) delay(1.seconds)
millisUntilFinished -= MS_TO_SEC millisUntilFinished -= 1.seconds
} else { } else {
onCountdownFinish() onCountdownFinish()
} }
@@ -58,5 +60,5 @@ fun useCountdownTimer(
} }
} }
return millisUntilFinished / MS_TO_SEC return millisUntilFinished.inWholeSeconds
} }
@@ -1,31 +1,45 @@
package org.cocos2dx.lib package org.cocos2dx.lib
import android.content.Context import android.content.Context
import android.graphics.* import android.graphics.Bitmap
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.* import java.util.LinkedList
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
private external fun nativeInitBitmapDC(pWidth: Int, pHeight: Int, pPixels: ByteArray) 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,
@@ -55,7 +69,6 @@ object Cocos2dxBitmap {
1.0f 1.0f
) )
} }
*/
@JvmStatic @JvmStatic
@Suppress("unused") @Suppress("unused")
@@ -79,19 +92,18 @@ object Cocos2dxBitmap {
strokeB: Float, strokeB: Float,
strokeSize: Float strokeSize: Float
) { ) {
val bitmapTotalHeight: Int val horizontalAlignment = alignment and 0x0F
val horizontalAlignment = alignment and 15 val verticalAlignment = (alignment shr 4) and 0x0F
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(
MotionEvent.ACTION_MASK, 255,
(255.0 * fontTintR.toDouble()).toInt(), (255.0 * fontTintR).toInt(),
(255.0 * fontTintG.toDouble()).toInt(), (255.0 * fontTintG).toInt(),
(255.0 * fontTintB.toDouble()).toInt() (255.0 * fontTintB).toInt()
) )
val textProperty = computeTextProperty(pString2, width, height, paint) val textProperty = computeTextProperty(pString2, width, height, paint)
bitmapTotalHeight = if (height == 0) { val bitmapTotalHeight = if (height == 0) {
textProperty.totalHeight textProperty.totalHeight
} else { } else {
height height
@@ -101,7 +113,8 @@ object Cocos2dxBitmap {
var renderTextDeltaX = 0.0f var renderTextDeltaX = 0.0f
var renderTextDeltaY = 0.0f var renderTextDeltaY = 0.0f
if (shadow) { if (shadow) {
paint.setShadowLayer(shadowBlur, shadowDX, shadowDY, -8553091) val shadowColor = 0xff7d7d7du.toInt()
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) {
@@ -111,56 +124,47 @@ object Cocos2dxBitmap {
renderTextDeltaY = bitmapPaddingY renderTextDeltaY = bitmapPaddingY
} }
} }
val bitmap = Bitmap.createBitmap(
val bitmap = try {
createBitmap(
textProperty.maxWidth + bitmapPaddingX.toInt(), textProperty.maxWidth + bitmapPaddingX.toInt(),
bitmapPaddingY.toInt() + bitmapTotalHeight, bitmapTotalHeight + bitmapPaddingY.toInt()
Bitmap.Config.ARGB_8888
) )
} 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 (i in 0 until length) { for (line in lines) {
val line = lines[i] val x = computeX(line, textProperty.maxWidth, horizontalAlignment)
canvas.drawText( canvas.drawText(line, x + renderTextDeltaX, y + renderTextDeltaY, paint)
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 = 0.5f * strokeSize paintStroke.strokeWidth = strokeSize * 0.5f
paintStroke.setARGB( paintStroke.setARGB(
MotionEvent.ACTION_MASK, 255,
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 y2 = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment) var y = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment)
val lines2 = textProperty.lines val lines = textProperty.lines
val length2 = lines2.size
for (i2 in 0 until length2) { for (line in lines) {
val line2 = lines2[i2] val x = computeX(line, textProperty.maxWidth, horizontalAlignment)
canvas.drawText( canvas.drawText(line, x + renderTextDeltaX, y + renderTextDeltaY, paintStroke)
line2!!, y += textProperty.heightPerLine
computeX(
line2,
textProperty.maxWidth,
horizontalAlignment
).toFloat() + renderTextDeltaX,
y2.toFloat() + renderTextDeltaY,
paintStroke
)
y2 += textProperty.heightPerLine
} }
} }
initNativeObject(bitmap) initNativeObject(bitmap)
@@ -168,24 +172,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 = -1 paint.color = Color.WHITE
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") Log.e("Cocos2dxBitmap", "error to create ttf type face: $fontName - $e")
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) {
2 -> paint.textAlign = Paint.Align.RIGHT HORIZONTALALIGN_RIGHT -> paint.textAlign = Paint.Align.RIGHT
3 -> paint.textAlign = Paint.Align.CENTER HORIZONTALALIGN_CENTER -> paint.textAlign = Paint.Align.CENTER
else -> paint.textAlign = Paint.Align.LEFT else -> paint.textAlign = Paint.Align.LEFT
} }
return paint return paint
@@ -198,14 +202,14 @@ object Cocos2dxBitmap {
paint: Paint paint: Paint
): TextProperty { ): TextProperty {
val fm = paint.fontMetricsInt val fm = paint.fontMetricsInt
val h = ceil((fm.bottom - fm.top).toDouble()).toInt() val h = fm.bottom - fm.top
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).toDouble()).toInt() val temp = ceil(paint.measureText(line, 0, line.length)).toInt()
if (temp > maxContentWidth) { if (temp > maxContentWidth) {
maxContentWidth = temp maxContentWidth = temp
} }
@@ -214,10 +218,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) {
2 -> maxWidth HORIZONTALALIGN_RIGHT -> maxWidth
3 -> maxWidth / 2 HORIZONTALALIGN_CENTER -> maxWidth / 2
else -> 0 else -> 0
} }
} }
@@ -229,14 +233,16 @@ object Cocos2dxBitmap {
verticalAlignment: Int verticalAlignment: Int
): Int { ): Int {
val y = -fontMetricsInt.top val y = -fontMetricsInt.top
return if (constrainHeight <= totalHeight) { return if (constrainHeight > totalHeight) {
y when (verticalAlignment) {
} else when (verticalAlignment) { VERTICALALIGN_TOP -> -fontMetricsInt.top
1 -> -fontMetricsInt.top VERTICALALIGN_CENTER -> -fontMetricsInt.top + (constrainHeight - totalHeight) / 2
2 -> -fontMetricsInt.top + (constrainHeight - totalHeight) VERTICALALIGN_BOTTOM -> -fontMetricsInt.top + (constrainHeight - totalHeight)
3 -> -fontMetricsInt.top + (constrainHeight - totalHeight) / 2
else -> y else -> y
} }
} else {
y
}
} }
private fun splitString( private fun splitString(
@@ -244,56 +250,56 @@ object Cocos2dxBitmap {
maxWidth: Int, maxWidth: Int,
maxHeight: Int, maxHeight: Int,
paint: Paint paint: Paint
): Array<String?> { ): List<String> {
val lines: Array<String?> = string.split("\\n").toTypedArray() val lines = string.split("\n")
val fm = paint.fontMetricsInt val fm = paint.fontMetricsInt
val maxLines = maxHeight / ceil((fm.bottom - fm.top).toDouble()).toInt() val heightPerLine = fm.bottom - fm.top
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) {
if (ceil(paint.measureText(line).toDouble()).toInt() > maxWidth) { val lineWidth = ceil(paint.measureText(line)).toInt()
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.toArray(ret) strList
ret } else if (maxHeight != 0 && lines.size > maxLines) {
} else if (maxHeight == 0 || lines.size <= maxLines) { lines.take(maxLines)
lines
} else { } else {
val strList2 = LinkedList<String?>() lines
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).toDouble()).toInt() val tempWidth = ceil(paint.measureText(string, start, i)).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
@@ -303,16 +309,20 @@ 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
} }
@@ -320,19 +330,22 @@ 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].code == 10) { start = if (index == 0 || strBuilder[index - 1] == '\n') {
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()
@@ -340,15 +353,10 @@ object Cocos2dxBitmap {
private fun initNativeObject(pBitmap: Bitmap) { private fun initNativeObject(pBitmap: Bitmap) {
val pixels = getPixels(pBitmap) val pixels = getPixels(pBitmap)
if (pixels != null) {
nativeInitBitmapDC(pBitmap.width, pBitmap.height, pixels) 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())
@@ -361,13 +369,18 @@ 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()
paint.getTextBounds("SghMNy", 0, "SghMNy".length, bounds) val text = "SghMNy"
paint.getTextBounds(text, 0, text.length, bounds)
incrTextSize++ incrTextSize++
if (height - bounds.height() <= 2) { if (height - bounds.height() <= 2) {
foundDesiredSize = true foundDesiredSize = true
} }
@@ -382,23 +395,19 @@ 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 constructor( private class TextProperty(
val maxWidth: Int, val maxWidth: Int,
val heightPerLine: Int, pLines: Array<String?> val heightPerLine: Int,
val lines: Collection<String>
) { ) {
val lines: Array<String?> val totalHeight: Int = lines.size * heightPerLine
val totalHeight: Int
init {
totalHeight = pLines.size * heightPerLine
lines = pLines
}
} }
} }
@@ -17,15 +17,18 @@ import android.view.inputmethod.InputMethodManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatEditText
import com.customRobTop.BaseRobTopActivity import com.customRobTop.BaseRobTopActivity
import com.geode.launcher.ExplicitGLSurfaceView
import com.geode.launcher.LauncherFix
import com.geode.launcher.utils.GeodeUtils import com.geode.launcher.utils.GeodeUtils
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToLong
private const val HANDLER_OPEN_IME_KEYBOARD = 2 private const val HANDLER_OPEN_IME_KEYBOARD = 2
private const val HANDLER_CLOSE_IME_KEYBOARD = 3 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
class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { class Cocos2dxGLSurfaceView(context: Context) : ExplicitGLSurfaceView(context) {
companion object { companion object {
lateinit var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView lateinit var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView
private lateinit var handler: Handler private lateinit var handler: Handler
@@ -63,6 +66,8 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
var manualBackEvents = false var manualBackEvents = false
var controllerEventsEnabled = false var controllerEventsEnabled = false
private set private set
var supportsResizeEvents = false
var saveNativeFramePacing = false
var cocos2dxEditText: AppCompatEditText? = null var cocos2dxEditText: AppCompatEditText? = null
set(value) { set(value) {
@@ -138,6 +143,10 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
println("updateRefreshRate: selecting refresh rate of ${chosenDisplay.refreshRate} (display ${chosenDisplay.modeId})") println("updateRefreshRate: selecting refresh rate of ${chosenDisplay.refreshRate} (display ${chosenDisplay.modeId})")
if (saveNativeFramePacing) {
LauncherFix.setSwapInterval((1_000_000_000L / chosenRefreshRate).roundToLong())
}
holder.surface.setFrameRate(chosenRefreshRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT) holder.surface.setFrameRate(chosenRefreshRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT)
if (isAttachedToWindow) { if (isAttachedToWindow) {
(context as Activity).window.attributes.preferredRefreshRate = chosenRefreshRate (context as Activity).window.attributes.preferredRefreshRate = chosenRefreshRate
@@ -152,6 +161,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
renderMode = RENDERMODE_CONTINUOUSLY renderMode = RENDERMODE_CONTINUOUSLY
queueEvent { cocos2dxRenderer.handleOnResume() } queueEvent { cocos2dxRenderer.handleOnResume() }
} }
@@ -288,6 +298,12 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
if (!isInEditMode) { if (!isInEditMode) {
cocos2dxRenderer.setScreenWidthAndHeight(newSurfaceWidth, newSurfaceHeight) cocos2dxRenderer.setScreenWidthAndHeight(newSurfaceWidth, newSurfaceHeight)
} }
if (supportsResizeEvents) {
queueEvent {
GeodeUtils.resizeSurface(newSurfaceWidth, newSurfaceHeight)
}
}
} }
private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean { private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
@@ -526,6 +542,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
setRenderer(this.cocos2dxRenderer) setRenderer(this.cocos2dxRenderer)
} }
override fun setNativeFramePacing(nativeFramePacing: Boolean) {
super.setNativeFramePacing(nativeFramePacing)
saveNativeFramePacing = nativeFramePacing
}
private fun getContentText(): String { private fun getContentText(): String {
return cocos2dxRenderer.getContentText() return cocos2dxRenderer.getContentText()
} }
@@ -560,6 +581,10 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
inputManager.unregisterInputDeviceListener(InputListener) inputManager.unregisterInputDeviceListener(InputListener)
} }
fun setMaxFrameRate(rate: Int) {
cocos2dxRenderer.limitFrameRate(rate)
}
object InputListener : InputManager.InputDeviceListener { object InputListener : InputManager.InputDeviceListener {
override fun onInputDeviceAdded(deviceId: Int) { override fun onInputDeviceAdded(deviceId: Int) {
GeodeUtils.inputDeviceAdded(deviceId) GeodeUtils.inputDeviceAdded(deviceId)
@@ -1,15 +1,14 @@
package org.cocos2dx.lib package org.cocos2dx.lib
import android.opengl.GLSurfaceView import android.opengl.EGLConfig
import android.os.Build import android.os.Build
import javax.microedition.khronos.egl.EGLConfig import com.geode.launcher.ExplicitGLSurfaceView
import javax.microedition.khronos.opengles.GL10
private const val NANOSECONDS_PER_SECOND = 1000000000L private const val NANOSECONDS_PER_SECOND = 1000000000L
private const val NANOSECONDS_PER_MICROSECOND = 1000000L private const val NANOSECONDS_PER_MICROSECOND = 1000000L
@Suppress("unused", "KotlinJniMissingFunction") @Suppress("unused", "KotlinJniMissingFunction")
class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceView.Renderer { class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : ExplicitGLSurfaceView.Renderer {
companion object { companion object {
@JvmStatic @JvmStatic
fun setAnimationInterval(@Suppress("UNUSED_PARAMETER") animationInterval: Double) { fun setAnimationInterval(@Suppress("UNUSED_PARAMETER") animationInterval: Double) {
@@ -80,12 +79,12 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
screenHeight = surfaceHeight screenHeight = surfaceHeight
} }
override fun onSurfaceCreated(gl10: GL10?, eglConfig: EGLConfig?) { override fun onSurfaceCreated(eglConfig: EGLConfig?) {
nativeInit(screenWidth, screenHeight) nativeInit(screenWidth, screenHeight)
lastTickInNanoSeconds = System.nanoTime() lastTickInNanoSeconds = System.nanoTime()
} }
override fun onSurfaceChanged(gl10: GL10?, width: Int, height: Int) { override fun onSurfaceChanged(width: Int, height: Int) {
if (setFrameRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (setFrameRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
handler.updateRefreshRate() handler.updateRefreshRate()
} }
@@ -93,7 +92,7 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
println("renderer surfaceChanged: ${width}x${height}@${handler.display.refreshRate}fps") println("renderer surfaceChanged: ${width}x${height}@${handler.display.refreshRate}fps")
} }
override fun onDrawFrame(gl: GL10?) { override fun onDrawFrame() {
val animationInterval = mAnimationInterval val animationInterval = mAnimationInterval
if (animationInterval == null) { if (animationInterval == null) {
nativeRender() nativeRender()
@@ -0,0 +1,18 @@
<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>
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_sapphire_background"/> <background android:drawable="@drawable/ic_launcher_sapphire_background"/>
<foreground android:drawable="@drawable/ic_launcher_sapphire_foreground"/> <foreground android:drawable="@drawable/ic_launcher_sapphire_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_sapphire_monochrome" />
</adaptive-icon> </adaptive-icon>
+6 -2
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 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 version %1$s 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>
@@ -82,7 +82,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">A Geometry Dash update is required.</string> <string name="launcher_cancelled_outdated">Geometry Dash version %1$s 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,6 +105,7 @@
<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>
@@ -297,6 +298,9 @@
<string name="preference_profiles_delete_alt">Delete</string> <string name="preference_profiles_delete_alt">Delete</string>
<string name="preference_profiles_delete_undo_alt">Undo</string> <string name="preference_profiles_delete_undo_alt">Undo</string>
<string name="preference_native_pacing">Native frame pacing</string>
<string name="preference_native_pacing_description">Reduces latency, but may worsen frame drops.</string>
<!-- ApplicationLogsActivity strings --> <!-- ApplicationLogsActivity strings -->
<string name="title_activity_application_logs">Application logs</string> <string name="title_activity_application_logs">Application logs</string>
<string name="application_logs_no_logs">No logs available.</string> <string name="application_logs_no_logs">No logs available.</string>
+4 -8
View File
@@ -1,13 +1,9 @@
buildscript {
val composeBOM by extra("2026.03.01")
}
// 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.0.1" apply false id("com.android.application") version "9.4.0" apply false
id("com.android.library") version "9.0.1" apply false id("com.android.library") version "9.4.0" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.20" apply false id("org.jetbrains.kotlin.plugin.serialization") version "2.4.10" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.4.10" apply false
} }
tasks.register<Delete>("clean") { tasks.register<Delete>("clean") {
Binary file not shown.
+3 -2
View File
@@ -1,8 +1,9 @@
#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.3.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.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
+5 -8
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 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 @@
############################################################################## ##############################################################################
# #
# Gradle start up script for POSIX generated by Gradle. # gradlew 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 Gradle # ksh gradlew
# #
# 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/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/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,7 +114,6 @@ 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.
@@ -172,7 +171,6 @@ 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" )
@@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
Vendored
+12 -24
View File
@@ -19,12 +19,12 @@
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem gradlew startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables, and ensure extensions are enabled
if "%OS%"=="Windows_NT" setlocal setlocal EnableExtensions
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
goto fail "%COMSPEC%" /c exit 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,30 +65,18 @@ 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
goto fail "%COMSPEC%" /c exit 1
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute gradlew
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* @rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@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
:end :exitWithErrorLevel
@rem End local scope for the variables with windows NT shell @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
if %ERRORLEVEL% equ 0 goto mainEnd "%COMSPEC%" /c exit %ERRORLEVEL%
: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