Compare commits

...
137 Commits
Author SHA1 Message Date
qimiko 6258700bc4 small oversight 2024-05-31 19:16:48 -07:00
qimiko a62bdcc51c ignore kotlin directory 2024-05-31 16:52:59 -07:00
qimiko 8341c15475 stop the xml warning 2024-05-31 16:52:03 -07:00
qimiko ce8c9496de my messages 2024-05-31 14:20:32 -07:00
qimiko 779a099c16 1 3 4 2024-05-31 13:49:22 -07:00
qimiko 031c7d215f ignore geode failures 2024-05-31 05:39:29 -07:00
qimiko 75596fefd2 proper refresh rate selection 2024-05-31 05:38:39 -07:00
qimiko efaeeba5ec add game mode config files 2024-05-22 18:49:27 -07:00
qimiko 3161ee0974 fix compose compiler 2024-05-22 18:49:14 -07:00
qimiko 127fd1bfd1 explicitly set the max refresh rate 2024-05-22 18:39:06 -07:00
qimiko b7be446ecd did you see they made a new kotlin 2024-05-22 18:38:39 -07:00
qimiko 18cb201a14 better to not reduce all information 2024-05-20 01:50:33 -07:00
qimiko 545a0d6532 further string tweaks 2024-05-20 01:43:41 -07:00
qimiko 574a4b5c22 tweak some strings 2024-05-20 00:53:06 -07:00
qimiko 028bf647e8 don't show play store if not installed 2024-05-20 00:49:26 -07:00
qimiko 93b90742ed increment version 2024-05-19 19:14:50 -07:00
qimiko 61d754bdd0 remove some unused options 2024-05-19 02:42:33 -07:00
qimiko 62568198a1 waited too long again :( 2024-05-17 19:30:30 -07:00
qimiko 649cba97a6 add input timestamp api 2024-05-17 19:23:05 -07:00
qimiko c3adddc86b declare the app as a game 2024-05-17 19:21:01 -07:00
qimiko 1c96c43c2d package libc++ alongside apk 2024-04-22 08:19:04 -07:00
qimiko d1abde68b9 allow some config changes 2024-04-20 05:14:38 -07:00
qimiko 52b997a5b4 tell people where to go for an updated game 2024-04-20 05:13:09 -07:00
qimiko b384da1825 waited too long 2024-04-20 05:12:09 -07:00
qimiko ef206baf69 force set egl version 2024-04-17 23:36:29 -07:00
qimiko 5392d42d97 add method to validate gd's certificate
unused for now
2024-03-17 00:05:42 -07:00
qimiko 2e8cc302d0 sure 2024-03-16 23:09:34 -07:00
qimiko a59cdd5d16 move gd functions to a separate file 2024-03-16 23:08:23 -07:00
qimiko d89e0f1134 agp upgrade assistant 2024-03-07 02:04:59 -07:00
qimiko cae1dcd493 bump ndk 2024-02-25 02:51:29 -07:00
Chloe ff40776458 update site on release 2024-02-24 17:32:23 -07:00
qimiko d7021ba54c add ability to load mods from assets
this replaces the old "load testing libraries" option, which hasn't been available in the ui anyways
2024-02-22 14:24:11 -07:00
qimiko 8fcc011f71 show message for api ratelimits 2024-02-21 15:13:05 -07:00
qimiko 6c62229c03 redo error info component organization 2024-02-20 20:41:53 -07:00
qimiko 9353c14dcf remove debug check in update 2024-02-20 15:16:47 -07:00
qimiko 299f081919 increment version 2024-02-20 15:08:56 -07:00
qimiko 919822300d add missing mediacodec get functions
relates to #13
2024-02-20 14:50:28 -07:00
qimiko 1b136cd4e9 enable package optimizations 2024-02-20 02:30:35 -07:00
qimiko 2d4a027001 nice filename 2024-02-18 18:53:04 -07:00
qimiko ae3e25dbf9 block launch on outdated game versions 2024-02-18 05:32:49 -07:00
qimiko 189350c55c update gradle 2024-02-18 05:22:17 -07:00
qimiko 9c8a5ab372 change main activity control flow 2024-02-18 05:19:45 -07:00
qimiko e37de9fc3a use copy for loading as a fallback 2024-02-18 05:04:21 -07:00
qimiko 13c69cb30f show device info in load failed popup 2024-02-18 04:52:40 -07:00
qimiko eeb6460548 let the crash popup be scrolled 2024-02-17 19:29:37 -07:00
qimiko 92c8c719eb why did i lock it to s_v2 anyways 2024-02-17 01:53:14 -07:00
qimiko 645ed772f4 use daynight for dialogs 2024-02-16 18:09:31 -07:00
qimiko 4114cc0078 copy change 2024-02-16 18:08:26 -07:00
qimiko aee4c49015 version increment 2024-02-16 18:01:16 -07:00
qimiko 4165c22c2b crash ui tweaks 2024-02-16 17:59:38 -07:00
qimiko c53a23faf9 replace screen ratio with more detailed option 2024-02-13 23:40:12 -07:00
qimiko cd73362c1c one last thing 2024-02-13 21:34:37 -07:00
qimiko a730eab331 oopsies 2024-02-13 21:22:43 -07:00
qimiko 7bc7b898a2 add crash info popup 2024-02-13 21:20:12 -07:00
qimiko 83557c3ebe pass file name to savefile intent 2024-02-13 15:42:18 -07:00
qimiko 58725d95d2 supposedly this was necessary 2024-02-13 15:41:25 -07:00
qimiko 7db022f392 add option to limit aspect ratio 2024-02-13 01:48:35 -07:00
qimiko 44bbb25c7e increment version code 2024-02-12 23:35:03 -07:00
qimiko 004526e685 redo load failed popup 2024-02-12 23:33:00 -07:00
qimiko 9c7d87ff8a Merge branch 'input' 2024-02-12 11:28:13 -07:00
qimiko 642fdc36ee cleanup motionevent 2024-02-11 00:55:33 -07:00
qimiko c6155443a2 platformcapability changed to bool 2024-02-10 22:07:22 -07:00
qimiko 420bca5ece rrrgh android 10 storage permissions fix 2024-02-10 21:22:15 -07:00
qimiko b96561bf33 add keyboard input support 2024-02-10 18:07:06 -07:00
qimiko 61c018ee0d delay launch with safe mode dialog 2024-02-08 22:11:33 -07:00
qimiko 41e3e0a133 package maintenance 2024-02-07 22:19:10 -07:00
qimiko 3853a469cd add internal request all files perm 2024-02-04 12:46:25 -07:00
qimiko 62b31b8924 hide load failure option 2024-02-03 18:32:14 -07:00
qimiko ab1d0c96f4 have internal links open storage provider 2024-02-03 18:30:54 -07:00
qimiko 96354096e2 minor update 2024-02-03 18:08:31 -07:00
qimiko 25b686f183 fix update warning dismiss 2024-02-03 18:06:07 -07:00
qimiko 7694236434 disallow ignoring older gd version warning 2024-02-03 18:03:40 -07:00
qimiko 7c0f1da312 clear button for text inputs 2024-02-03 05:28:19 -07:00
qimiko 019d9747b5 add ui to launch in safe mode
closes #17
2024-02-02 02:06:43 -07:00
qimiko e806ec223a use all files permission on newer devices 2024-02-02 00:04:07 -07:00
qimiko 9f58e76f97 maintenance 2 2024-02-01 13:15:30 -07:00
qimiko 371750a184 enable gradle cache 2024-01-31 16:51:09 -07:00
qimiko 2494bda4e8 send vibration on log long press 2024-01-30 03:17:23 -07:00
qimiko b67be09cd3 update readme 2024-01-30 02:22:49 -07:00
qimiko 09b1c9bc3a forgot to set failure callback :( 2024-01-30 02:20:48 -07:00
qimiko 483fb56e25 feeling gradle 2024-01-30 02:20:48 -07:00
Chloe 7857fae68f appease github 2024-01-30 00:46:19 -07:00
qimiko 61b84cd223 accidentally left in an import 2024-01-30 00:40:08 -07:00
qimiko bd67332adc remove safe mode launch code
no sense having it in onLaunch parameters until i figure out how i want the ui to work
2024-01-30 00:39:04 -07:00
qimiko 84657fd6b3 add safe mode handling 2024-01-30 00:06:29 -07:00
qimiko f197b45f9e increment launcher version 2024-01-29 23:49:07 -07:00
qimiko d0dc8bec5d build file maintenance 2024-01-29 23:45:56 -07:00
qimiko c9e0408eb2 close dropdown on dialog open 2024-01-28 19:02:13 -07:00
qimiko b50a5b2b54 add option to filter by log level 2024-01-28 18:48:11 -07:00
qimiko c8e3d7404c allow downgrading geode 2024-01-28 17:55:01 -07:00
qimiko c4c41e324e move settings components into another file 2024-01-28 16:16:55 -07:00
qimiko 4d324913df promote load errors to log errors 2024-01-28 06:51:51 -07:00
qimiko 19bd549b1f request file permissions when necessary
fixes #8 by making it irrelevant
2024-01-28 00:20:18 -07:00
qimiko 9dc501c9ab make geode load failures a hard error 2024-01-27 23:16:33 -07:00
qimiko 5521869ac9 add launch arguments option 2024-01-27 22:55:34 -07:00
mat c9a712f692 im stupid 2024-01-27 13:21:46 -03:00
mat 6a0b3fc8bd we're stupid 2024-01-27 13:17:21 -03:00
alk 684fd99b83 add release as a build i think 2024-01-27 18:34:18 +03:00
qimiko fd83daa776 generate more specific warnings 2024-01-27 03:13:37 -07:00
qimiko 9a87716635 add fallback mode to log export 2024-01-27 02:30:36 -07:00
Chloe 31566ce64b Merge pull request #12 from dankmeme01/main
Add support for requesting permissions from native code
2024-01-26 15:24:48 -07:00
dankmeme01 d078879e7b change handling of failures 2024-01-26 23:04:33 +01:00
dankmeme01 5fa42b6ca5 remove unused imports 2024-01-26 22:35:28 +01:00
dankmeme01 e164b87436 make permission requesting use launchers and add callback 2024-01-26 22:35:08 +01:00
dankmeme01 433d6a0f46 add support for requesting permissions from mods or Geode 2024-01-26 21:30:30 +01:00
mat cf8b1edbe2 Merge pull request #11 from Fleeym/crashfix
Hopefully fix crashes when tabbing out and back in
2024-01-25 15:39:55 -03:00
Fleeym 500e00a9cc ask for more file permission stuff 2024-01-25 19:58:18 +02:00
Fleeym c3f80e9c74 fix opengl context being deleted after it no longer exists 2024-01-25 19:58:09 +02:00
Chloe 7eb06e175f Merge pull request #10 from Fleeym/flame-stuff
fix file utils, move them to a separate class
2024-01-24 17:20:52 -07:00
Chloe 141681be05 add license 2024-01-23 20:24:37 -07:00
Fleeym dcfab5e001 fix file utils, move them to a separate class 2024-01-24 04:45:09 +02:00
qimiko 498bbf4528 accidentally broke a dependency 2024-01-23 05:14:36 -07:00
qimiko dc72608622 properly set value in set 2024-01-23 05:11:56 -07:00
qimiko a50e0a0830 remove extra println 2024-01-23 05:05:06 -07:00
qimiko db707dfce1 only show unsupported version once 2024-01-23 05:03:37 -07:00
qimiko 929cc7615e api to set preference through compose 2024-01-23 05:03:18 -07:00
qimiko bb2e3fbb1b version strings are meaningless 2024-01-23 04:48:55 -07:00
qimiko 7d548aef6c remove unused import 2024-01-23 03:18:54 -07:00
qimiko 7f64236e89 add launcher update check 2024-01-23 03:15:39 -07:00
qimiko 199b6f142b use nightly by default in debug mode 2024-01-23 03:12:44 -07:00
qimiko af87320f11 clarify unsupported version warning 2024-01-23 02:18:48 -07:00
qimiko 665756d85b automatically trim log messages 2024-01-23 01:33:54 -07:00
qimiko 590006413c filter by crash buffer option 2024-01-23 01:31:38 -07:00
qimiko 1b079fa3ff add ability to export logs to file 2024-01-23 01:01:14 -07:00
qimiko cb16db3bf9 use specific message for abi differences 2024-01-23 00:19:55 -07:00
qimiko de6fad41f6 fix nightly download url 2024-01-22 11:38:11 -07:00
qimiko d1a43230a9 gd may have updated 2024-01-22 08:16:42 -07:00
qimiko 442ef83339 show launcher version in settings 2024-01-22 00:28:29 -07:00
qimiko 1eab544692 reorganize release api 2024-01-22 00:16:01 -07:00
qimiko af0ebeab84 save game during onstop 2024-01-21 16:15:22 -07:00
qimiko 0844dc97d2 use kotlin stream copyto for file copies 2024-01-21 16:15:11 -07:00
qimiko 42c10c2729 navigation bar visibility changes 2024-01-21 03:40:26 -07:00
qimiko cf568bcbe2 use release hash instead of modified time 2024-01-20 20:28:08 -07:00
qimiko 36152d8773 add icon to retry button 2024-01-20 20:26:00 -07:00
qimiko 3f371be097 pretend log messages are utf8 2024-01-20 20:17:39 -07:00
qimiko 5eba097e38 only request permissions when needed 2024-01-20 18:37:59 -07:00
qimiko 7e868d8d0a log optimizations 2024-01-20 18:09:53 -07:00
55 changed files with 3328 additions and 1263 deletions

No files matched your search

+16 -7
View File
@@ -7,6 +7,15 @@ on:
jobs: jobs:
build: build:
strategy:
matrix:
config:
- name: Release
lower: release
- name: Debug
lower: debug
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -27,7 +36,7 @@ jobs:
- name: Build project - name: Build project
run: | run: |
./gradlew app:assembleDebug \ ./gradlew app:assemble${{ matrix.config.name }} \
-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}}" \
@@ -37,8 +46,8 @@ jobs:
id: rename_apk_universal id: rename_apk_universal
run: | run: |
SHORT_SHA=${GITHUB_SHA::7} SHORT_SHA=${GITHUB_SHA::7}
APK_PATH=${{github.workspace}}/app/build/outputs/apk/debug/app-universal-debug.apk APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-universal-${{ matrix.config.lower }}.apk
OUT_PATH=${{github.workspace}}/geode-launcher-debug-$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
@@ -46,15 +55,15 @@ jobs:
id: rename_apk id: rename_apk
run: | run: |
SHORT_SHA=${GITHUB_SHA::7} SHORT_SHA=${GITHUB_SHA::7}
APK_PATH=${{github.workspace}}/app/build/outputs/apk/debug/app-armeabi-v7a-debug.apk APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-armeabi-v7a-${{ matrix.config.lower }}.apk
OUT_PATH=${{github.workspace}}/geode-launcher-debug-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: Upload development build - name: Upload development build
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: geode-launcher-debug 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 }}
+24
View File
@@ -0,0 +1,24 @@
name: Update Website
on:
release:
types:
- released
workflow_dispatch:
jobs:
trigger:
name: Trigger site build
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GEODE_BOT_PUSH_BIN_TOKEN }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'geode-sdk',
repo: 'website',
workflow_id: 'build.yml',
ref: 'main'
})
+1
View File
@@ -10,3 +10,4 @@
local.properties local.properties
app/release/ app/release/
.vscode/ .vscode/
.kotlin/
+23
View File
@@ -0,0 +1,23 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
-4
View File
@@ -1,7 +1,3 @@
# Geode Android Launcher # Geode Android Launcher
Launches a vanilla copy of Geometry Dash with the Geode loader added. Launches a vanilla copy of Geometry Dash with the Geode loader added.
## TODO:
* Figure out better way of packaging libgeode
-97
View File
@@ -1,97 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
}
android {
compileSdk 34
defaultConfig {
applicationId "com.geode.launcher"
minSdk 23
targetSdk 34
versionCode 6
versionName "1.0.0-beta.3"
vectorDrawables {
useSupportLibrary true
}
externalNativeBuild {
cmake {
arguments "-DUSE_TULIPHOOK:BOOL=OFF"
}
}
ndkConfig.abiFilters "arm64-v8a", "armeabi-v7a"
}
splits {
abi {
enable true
reset()
//noinspection ChromeOsAbiSupport (not my fault)
include "arm64-v8a", "armeabi-v7a"
universalApk true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
// enables a polyfill for java Instant on api levels < 26 (used for updater)
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_compiler
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
}
}
namespace 'com.geode.launcher'
ndkVersion '26.1.10909125'
}
dependencies {
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.compose.ui:ui"
implementation 'androidx.compose.material3:material3'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.activity:activity-ktx:1.8.2'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation "androidx.documentfile:documentfile:1.0.1"
implementation "com.squareup.okio:okio:3.7.0"
implementation "com.squareup.okhttp3:okhttp:4.12.0"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.0"
debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-test-manifest"
debugImplementation "androidx.compose.ui:ui-tooling-preview"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
}
+105
View File
@@ -0,0 +1,105 @@
val composeBOM: String by rootProject.extra
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.serialization")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
compileSdk = 34
defaultConfig {
applicationId = "com.geode.launcher"
minSdk = 23
targetSdk = 34
versionCode = 16
versionName = "1.3.4"
vectorDrawables {
useSupportLibrary = true
}
@Suppress("UnstableApiUsage")
externalNativeBuild {
cmake {
arguments("-DUSE_TULIPHOOK:BOOL=OFF", "-DANDROID_STL=c++_shared")
}
}
//noinspection ChromeOsAbiSupport (not my fault)
ndk.abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
splits {
abi {
isEnable = true
reset()
//noinspection ChromeOsAbiSupport. i'm sorry!
include("arm64-v8a", "armeabi-v7a")
isUniversalApk = true
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
// enables a polyfill for java Instant on api levels < 26 (used for updater)
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
buildConfig = true
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
namespace = "com.geode.launcher"
ndkVersion = "26.3.11579264"
}
dependencies {
implementation (platform("androidx.compose:compose-bom:$composeBOM"))
implementation ("androidx.core:core-ktx:1.13.1")
implementation ("androidx.compose.ui:ui")
implementation ("androidx.compose.material3:material3")
implementation ("androidx.compose.ui:ui-tooling-preview")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.1")
implementation ("androidx.activity:activity-compose:1.9.0")
implementation ("androidx.activity:activity-ktx:1.9.0")
implementation ("androidx.appcompat:appcompat:1.7.0")
implementation ("androidx.documentfile:documentfile:1.0.1")
implementation ("com.squareup.okio:okio:3.9.0")
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3")
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
debugImplementation ("androidx.compose.ui:ui-tooling")
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.0.4")
}
+6 -20
View File
@@ -1,21 +1,7 @@
# Add project specific ProGuard rules here. # Add wildcard keep rules to all of the library code in use
# You can control the set of applied configuration files using the -keep class org.fmod.** { *; }
# proguardFiles setting in build.gradle. -keep class org.cocos2dx.lib.** { *; }
# -keep class com.customRobTop.** { *; }
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following # note: if you're going to add more rules, consider the @Keep annotation
# and specify the fully qualified class name to the JavaScript interface # this should really only be kept to non-custom code
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+16 -2
View File
@@ -9,8 +9,15 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<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_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature <uses-feature
android:name="android.hardware.wifi" android:name="android.hardware.wifi"
@@ -21,10 +28,12 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:appCategory="game"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:theme="@style/Theme.GeodeLauncher" android:theme="@style/Theme.GeodeLauncher"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
@@ -47,6 +56,7 @@
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:screenOrientation="landscape" android:screenOrientation="landscape"
android:theme="@style/Theme.NoActionBar.Fullscreen" android:theme="@style/Theme.NoActionBar.Fullscreen"
android:configChanges="orientation|screenSize|smallestScreenSize"
android:windowSoftInputMode="stateAlwaysHidden"> android:windowSoftInputMode="stateAlwaysHidden">
<meta-data <meta-data
android:name="android.app.lib_name" android:name="android.app.lib_name"
@@ -82,6 +92,10 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<meta-data
android:name="android.game_mode_config"
android:resource="@xml/game_mode_config" />
</application> </application>
</manifest> </manifest>
@@ -9,7 +9,10 @@ import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -20,10 +23,14 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
@@ -38,14 +45,21 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -64,6 +78,8 @@ import kotlinx.coroutines.launch
class ApplicationLogsActivity : ComponentActivity() { class ApplicationLogsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME) val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
@@ -92,43 +108,48 @@ fun getLogColor(priority: LogPriority): Color {
// i just copied these from my ide tbh // i just copied these from my ide tbh
return if (isDark) { return if (isDark) {
when (priority) { when (priority) {
LogPriority.ERROR -> Color(0xffdf5151) LogPriority.ERROR -> Color(0xff_df_51_51)
LogPriority.WARN -> Color(0xffbcb500) LogPriority.WARN -> Color(0xff_bc_b5_00)
LogPriority.FATAL -> Color(0xffb8b6fc) LogPriority.FATAL -> Color(0xff_b8_b6_fc)
LogPriority.DEBUG -> Color(0xff_aa_aa_aa)
LogPriority.VERBOSE -> Color(0xff_77_77_77)
else -> Color.Unspecified else -> Color.Unspecified
} }
} else { } else {
when (priority) { when (priority) {
LogPriority.ERROR -> Color(0xffdf5151) LogPriority.ERROR -> Color(0xff_df_51_51)
LogPriority.WARN -> Color(0xffeab700) LogPriority.WARN -> Color(0xff_ea_b7_00)
LogPriority.FATAL -> Color(0xffa142f4) LogPriority.FATAL -> Color(0xff_a1_42_f4)
LogPriority.DEBUG -> Color(0xff_77_77_77)
LogPriority.VERBOSE -> Color(0xff_aa_aa_aa)
else -> Color.Unspecified else -> Color.Unspecified
} }
} }
} }
fun copyLogText(context: Context, logMessage: String) { @Composable
val clipboardManager = fun SelectLogLevelDialog(
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager onDismissRequest: () -> Unit,
clipboardManager.setPrimaryClip( onSelect: (LogPriority) -> Unit,
ClipData.newPlainText( initialValue: LogPriority
context.getString(R.string.application_log_copy_label), ) {
logMessage SelectDialog(
title = stringResource(R.string.application_logs_level_dialog_name),
onDismissRequest = onDismissRequest,
onSelect = { onSelect(it) },
initialValue = initialValue,
toLabel = {
// i'm lazy
it.name.lowercase().replaceFirstChar { c -> c.uppercaseChar() }
},
optionsCount = LogPriority.FATAL downTo LogPriority.VERBOSE
) )
)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText(
context,
context.getString(R.string.text_copied),
Toast.LENGTH_SHORT
).show()
}
} }
@Composable @Composable
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) { fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
val context = LocalContext.current val haptics = LocalHapticFeedback.current
val clipboardManager = LocalClipboardManager.current
Column( Column(
modifier = modifier modifier = modifier
@@ -136,7 +157,8 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onLongPress = { onLongPress = {
copyLogText(context, logLine.asSimpleString) haptics.performHapticFeedback(HapticFeedbackType.LongPress)
clipboardManager.setText(AnnotatedString(logLine.asSimpleString))
} }
) )
}, },
@@ -149,7 +171,7 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
) )
Text( Text(
"[${logLine.tag}]: ${logLine.message}", "[${logLine.tag}]: ${logLine.messageTrimmed}",
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
style = Typography.bodyMedium style = Typography.bodyMedium
) )
@@ -171,10 +193,40 @@ fun ApplicationLogsScreen(
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState() val listState = rememberLazyListState()
var showMoreOptions by remember { mutableStateOf(false) }
val saveLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
if (uri != null) {
coroutineScope.launch {
val logs = logViewModel.getLogData()
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.bufferedWriter().use { writer ->
writer.write(logs)
}
}
Toast.makeText(context, R.string.application_logs_export_completed, Toast.LENGTH_SHORT)
.show()
}
}
}
LaunchedEffect(logLines.size) { LaunchedEffect(logLines.size) {
listState.scrollToItem(logLines.size) listState.scrollToItem(logLines.size)
} }
var showLogLevelSelect by remember { mutableStateOf(false) }
if (showLogLevelSelect) {
SelectLogLevelDialog(
onDismissRequest = { showLogLevelSelect = false },
onSelect = {
logViewModel.logLevel = it
showLogLevelSelect = false
},
initialValue = logViewModel.logLevel
)
}
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
@@ -183,14 +235,13 @@ fun ApplicationLogsScreen(
navigationIcon = { navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) { IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_icon_alt) contentDescription = stringResource(R.string.back_icon_alt)
) )
} }
}, },
actions = { actions = {
IconButton(onClick = { IconButton(onClick = {
if (!logLines.isEmpty()) {
coroutineScope.launch { coroutineScope.launch {
val data = logViewModel.getLogData() val data = logViewModel.getLogData()
@@ -203,20 +254,92 @@ fun ApplicationLogsScreen(
context.startActivity(shareIntent) context.startActivity(shareIntent)
} }
} }, enabled = !logLines.isEmpty()) {
}) {
Icon( Icon(
Icons.Filled.Share, Icons.Filled.Share,
contentDescription = stringResource(R.string.application_logs_share) contentDescription = stringResource(R.string.application_logs_share)
) )
} }
IconButton(onClick = { IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
logViewModel.clearLogs() Icon(
onBackPressedDispatcher?.onBackPressed() Icons.Filled.MoreVert,
}) { contentDescription = stringResource(R.string.application_logs_more)
)
}
DropdownMenu(
expanded = showMoreOptions,
onDismissRequest = { showMoreOptions = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_bug_report),
contentDescription = null,
)
},
trailingIcon = {
if (logViewModel.filterCrashes) {
Icon(
Icons.Filled.Check,
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
)
}
},
text = {
Text(stringResource(R.string.application_logs_crash_only))
}, onClick = {
logViewModel.toggleCrashBuffer()
showMoreOptions = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_filter_list),
contentDescription = null,
)
},
text = {
Text(stringResource(R.string.application_logs_level))
},
onClick = {
showLogLevelSelect = true
showMoreOptions = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_save),
contentDescription = null,
)
},
text = {
Text(stringResource(R.string.application_logs_export))
},
onClick = {
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
showMoreOptions = false
},
enabled = !logLines.isEmpty()
)
DropdownMenuItem(
leadingIcon = {
Icon( Icon(
painterResource(R.drawable.icon_delete), painterResource(R.drawable.icon_delete),
contentDescription = stringResource(R.string.application_logs_delete) contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_logs_delete))
}, onClick = {
logViewModel.clearLogs()
showMoreOptions = false
}
) )
} }
}, },
@@ -260,7 +383,7 @@ fun ApplicationLogsScreen(
logLines logLines
) { line -> ) { line ->
LogCard(line) LogCard(line)
Divider() HorizontalDivider()
} }
} }
} }
@@ -12,6 +12,7 @@ import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -21,9 +22,11 @@ import androidx.core.view.WindowInsetsControllerCompat
import com.customRobTop.BaseRobTopActivity import com.customRobTop.BaseRobTopActivity
import com.customRobTop.JniToCpp import com.customRobTop.JniToCpp
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.ConstrainedFrameLayout
import com.geode.launcher.utils.DownloadUtils import com.geode.launcher.utils.DownloadUtils
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.GeodeUtils import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import org.cocos2dx.lib.Cocos2dxEditText import org.cocos2dx.lib.Cocos2dxEditText
import org.cocos2dx.lib.Cocos2dxGLSurfaceView import org.cocos2dx.lib.Cocos2dxGLSurfaceView
@@ -33,9 +36,21 @@ import org.fmod.FMOD
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException
enum class DisplayMode {
DEFAULT, LIMITED, FULLSCREEN;
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener { companion object {
fun fromInt(i: Int) = when (i) {
1 -> LIMITED
2 -> FULLSCREEN
else -> DEFAULT
}
}
}
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener {
private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null
private val sTag = GeometryDashActivity::class.simpleName private val sTag = GeometryDashActivity::class.simpleName
private var mIsRunning = false private var mIsRunning = false
@@ -43,13 +58,16 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private var mHasWindowFocus = false private var mHasWindowFocus = false
private var mReceiver: BroadcastReceiver? = null private var mReceiver: BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) { private var displayMode = DisplayMode.DEFAULT
super.onCreate(savedInstanceState) private var forceRefreshRate = false
override fun onCreate(savedInstanceState: Bundle?) {
setupUIState() setupUIState()
super.onCreate(savedInstanceState)
// return back to main if Geometry Dash isn't found // return back to main if Geometry Dash isn't found
if (!LaunchUtils.isGeometryDashInstalled(packageManager)) { if (!GamePackageUtils.isGameInstalled(packageManager)) {
returnToMain() returnToMain()
return return
} }
@@ -57,37 +75,47 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
try { try {
tryLoadGame() tryLoadGame()
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
e.printStackTrace() Log.e("GeodeLauncher", "Library linkage failure", e)
// generates helpful information for use in debugging library load failures // generates helpful information for use in debugging library load failures
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0) val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
val failureReason = LaunchUtils.diagnoseLoadErrors(this, gdPackageInfo) val abiMismatch = GamePackageUtils.detectAbiMismatch(this, gdPackageInfo, e)
returnToMain( val is64bit = LaunchUtils.is64bit
getString(R.string.load_failed_link_error), val errorMessage = when {
getString(R.string.load_failed_link_error_description, failureReason.name) abiMismatch && is64bit -> LaunchUtils.LauncherError.LINKER_NEEDS_32BIT
) abiMismatch -> LaunchUtils.LauncherError.LINKER_NEEDS_64BIT
else -> LaunchUtils.LauncherError.LINKER_FAILED
}
returnToMain(errorMessage, e.message, e.stackTraceToString())
return return
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e("GeodeLauncher", "Uncaught error during game load", e)
returnToMain( returnToMain(
getString(R.string.load_failed_generic_error), LaunchUtils.LauncherError.GENERIC,
getString(R.string.load_failed_generic_error_description, e.message ?: "UnknownException") e.message ?: "Unknown Exception",
e.stackTraceToString()
) )
return return
} }
} }
private fun returnToMain(returnTitle: String? = null, returnMessage: String? = null) { private fun returnToMain(
error: LaunchUtils.LauncherError? = null,
returnMessage: String? = null,
returnExtendedMessage: String? = null
) {
val launchIntent = Intent(this, MainActivity::class.java).apply { val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
if (!returnTitle.isNullOrEmpty() && !returnMessage.isNullOrEmpty()) { if (error != null && !returnMessage.isNullOrEmpty()) {
putExtra(Constants.LAUNCHER_KEY_RETURN_TITLE, returnTitle) putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR, error)
putExtra(Constants.LAUNCHER_KEY_RETURN_MESSAGE, returnMessage) putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE, returnMessage)
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE, returnExtendedMessage)
} }
} }
@@ -99,39 +127,44 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
setupRedirection(gdPackageInfo) setupRedirection(gdPackageInfo)
// request read and write permissions
requestPermissions(
arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
),
0
)
Cocos2dxHelper.init(this, this) Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this) GeodeUtils.setContext(this)
GeodeUtils.setCapabilityListener(this)
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME) tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME) tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
if (getLoadTesting()) { loadInternalMods()
loadTestingLibraries()
}
setContentView(createView()) setContentView(createView())
setupPostLibraryLoad(gdPackageInfo) setupPostLibraryLoad(gdPackageInfo)
if (!loadGeodeLibrary()) { try {
Log.w("GeodeLauncher", "could not load Geode object!") loadGeodeLibrary()
} catch (e: UnsatisfiedLinkError) {
handleGeodeException(e)
} catch (e: Exception) {
handleGeodeException(e)
}
}
private fun handleGeodeException(e: Throwable) {
e.printStackTrace()
// ignore load failures if the game is newer than what's supported
// so people in the future can use their save data
if (GamePackageUtils.getGameVersionCode(packageManager) <= Constants.SUPPORTED_VERSION_CODE) {
throw e
} }
} }
private fun setupRedirection(packageInfo: PackageInfo) { private fun setupRedirection(packageInfo: PackageInfo) {
try { try {
LaunchUtils.addAssetsFromPackage(assets, packageInfo) GamePackageUtils.addAssetsFromPackage(assets, packageInfo)
} catch (e: NoSuchMethodException) { } catch (e: NoSuchMethodException) {
e.printStackTrace() Log.e("GeodeLauncher", "Failed to add asset redirection", e)
} }
try { try {
@@ -143,7 +176,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR) LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR)
LauncherFix.setDataPath(saveDir.path) LauncherFix.setDataPath(saveDir.path)
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
e.printStackTrace() Log.e("GeodeLauncher", "Failed to load LauncherFix", e)
} }
} }
@@ -164,9 +197,69 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun tryLoadLibrary(packageInfo: PackageInfo, libraryName: String) { private fun tryLoadLibrary(packageInfo: PackageInfo, libraryName: String) {
val nativeDir = getNativeLibraryDirectory(packageInfo.applicationInfo) val nativeDir = getNativeLibraryDirectory(packageInfo.applicationInfo)
val libraryPath = if (nativeDir.endsWith('/')) "${nativeDir}lib$libraryName.so" else "$nativeDir/lib$libraryName.so" val libraryPath = if (nativeDir.endsWith('/')) "${nativeDir}lib$libraryName.so" else "$nativeDir/lib$libraryName.so"
try {
System.load(libraryPath) System.load(libraryPath)
} catch (ule: UnsatisfiedLinkError) {
// some devices (samsung a series, cough) have an overly restrictive application classloader
// this is a workaround for that. hopefully
if (ule.message?.contains("not accessible for the namespace") != true) {
throw ule
}
println("Using copy for library $libraryName")
loadLibraryCopy(libraryName, libraryPath)
}
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadLibraryCopy(libraryName: String, libraryPath: String) {
if (libraryPath.contains("!/")) {
// library in apk can't be loaded directly
loadLibraryFromAssetsCopy(libraryName)
return
}
val library = File(libraryPath)
val libraryCopy = File(cacheDir, "lib$libraryName.so")
libraryCopy.outputStream().use { libraryOutput ->
library.inputStream().use { inputStream ->
DownloadUtils.copyFile(inputStream, libraryOutput)
}
}
System.load(libraryCopy.path)
return
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadLibraryFromAssetsCopy(libraryName: String) {
// loads a library loaded in assets
// this copies the library to a non-compressed directory
val arch = LaunchUtils.applicationArchitecture
val libraryFd = try {
assets.openNonAssetFd("lib/$arch/lib$libraryName.so")
} catch (_: Exception) {
throw UnsatisfiedLinkError("Could not find library lib$libraryName.so for abi $arch")
}
// copy the library to a path we can access
// there doesn't seem to be a way to load a library from a file descriptor
val libraryCopy = File(cacheDir, "lib$libraryName.so")
libraryCopy.outputStream().use { libraryOutput ->
libraryFd.createInputStream().use { inputStream ->
DownloadUtils.copyFile(inputStream, libraryOutput)
}
}
System.load(libraryCopy.path)
return
} }
private fun getNativeLibraryDirectory(applicationInfo: ApplicationInfo): String { private fun getNativeLibraryDirectory(applicationInfo: ApplicationInfo): String {
@@ -195,26 +288,26 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadGeodeLibrary(): Boolean { private fun loadGeodeLibrary() {
// Load Geode if exists // Load Geode if exists
// bundling the object with the application allows for nicer backtraces // bundling the object with the application allows for nicer backtraces
try { try {
// put libgeode.so in jniLibs/armeabi-v7a to get this // put libgeode.so in jniLibs/armeabi-v7a to get this
System.loadLibrary("geode") System.loadLibrary("geode")
return true return
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
// 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()) {
System.load(geodePath.path) System.load(geodePath.path)
return true return
} }
// 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) ?: return false val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!!
val copiedPath = File(filesDir.path, "copied") val copiedPath = File(filesDir.path, "copied")
if (copiedPath.exists()) { if (copiedPath.exists()) {
@@ -231,24 +324,33 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
) )
if (copiedGeodePath.exists()) { if (copiedGeodePath.exists()) {
try {
println("Loading Geode from ${externalGeodePath.name}") println("Loading Geode from ${externalGeodePath.name}")
System.load(copiedGeodePath.path) System.load(copiedGeodePath.path)
} catch (e: UnsatisfiedLinkError) { return
e.printStackTrace()
}
}
} }
} }
return false throw e
}
} }
private fun createView(): FrameLayout { private fun createView(): FrameLayout {
val frameLayoutParams = ViewGroup.LayoutParams(-1, -1) val frameLayoutParams = ViewGroup.LayoutParams(
val frameLayout = FrameLayout(this) ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
val frameLayout = ConstrainedFrameLayout(this)
frameLayout.layoutParams = frameLayoutParams frameLayout.layoutParams = frameLayoutParams
val editTextLayoutParams = ViewGroup.LayoutParams(-1, -2)
if (displayMode == DisplayMode.LIMITED) {
// despite not being perfectly 16:9, this is what Android calls "16:9"
frameLayout.aspectRatio = 1.86f
}
val editTextLayoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val editText = Cocos2dxEditText(this) val editText = Cocos2dxEditText(this)
editText.layoutParams = editTextLayoutParams editText.layoutParams = editTextLayoutParams
frameLayout.addView(editText) frameLayout.addView(editText)
@@ -258,6 +360,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
this.mGLSurfaceView = glSurfaceView this.mGLSurfaceView = glSurfaceView
frameLayout.addView(this.mGLSurfaceView) frameLayout.addView(this.mGLSurfaceView)
glSurfaceView.setEGLContextClientVersion(2)
glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8) glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8)
if (isAndroidEmulator()) { if (isAndroidEmulator()) {
@@ -265,7 +368,13 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
glSurfaceView.initView() glSurfaceView.initView()
glSurfaceView.setCocos2dxRenderer(Cocos2dxRenderer())
val renderer = Cocos2dxRenderer(glSurfaceView)
glSurfaceView.setCocos2dxRenderer(renderer)
if (forceRefreshRate) {
renderer.setFrameRate = true
}
editText.inputType = 145 editText.inputType = 145
glSurfaceView.cocos2dxEditText = editText glSurfaceView.cocos2dxEditText = editText
@@ -276,18 +385,39 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private fun setupUIState() { private fun setupUIState() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
displayMode = DisplayMode.fromInt(
PreferenceUtils.get(this).getInt(PreferenceUtils.Key.DISPLAY_MODE)
)
forceRefreshRate = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.FORCE_HRR)
if (displayMode == DisplayMode.FULLSCREEN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
hideSystemUi() hideSystemUi()
} }
private fun hideSystemUi() { private fun hideSystemUi() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
WindowCompat.getInsetsController(window, window.decorView).apply { WindowCompat.getInsetsController(window, window.decorView).apply {
hide(WindowInsetsCompat.Type.systemBars())
systemBarsBehavior = systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
hide(WindowInsetsCompat.Type.systemBars())
} }
} }
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
super.onWindowFocusChanged(hasWindowFocus)
mHasWindowFocus = hasWindowFocus
if (hasWindowFocus && !mIsOnPause) {
resumeGame()
}
hideSystemUi()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
unregisterReceivers() unregisterReceivers()
@@ -315,6 +445,15 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
} }
override fun onRestart() {
super.onRestart()
mIsOnPause = false
BaseRobTopActivity.isPaused = false
if (!this.mIsRunning) {
resumeGame()
}
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
mIsOnPause = true mIsOnPause = true
@@ -343,14 +482,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
} }
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
super.onWindowFocusChanged(hasWindowFocus)
mHasWindowFocus = hasWindowFocus
if (hasWindowFocus && !mIsOnPause) {
resumeGame()
}
}
override fun runOnGLThread(runnable: Runnable) { override fun runOnGLThread(runnable: Runnable) {
mGLSurfaceView?.queueEvent(runnable) mGLSurfaceView?.queueEvent(runnable)
} }
@@ -389,38 +520,48 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
return isEmulator return isEmulator
} }
private fun getLoadTesting(): Boolean { override fun onCapabilityAdded(capability: String): Boolean {
val preferences = PreferenceUtils.get(this) return when (capability) {
return preferences.getBoolean(PreferenceUtils.Key.LOAD_TESTING) GeodeUtils.CAPABILITY_EXTENDED_INPUT -> {
mGLSurfaceView?.useKeyboardEvents = true
true
}
GeodeUtils.CAPABILITY_TIMESTAMP_INPUT -> {
mGLSurfaceView?.sendTimestampEvents = true
true
}
else -> false
}
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") /**
private fun loadTestingLibraries() { * Copies a mod from the launcher's assets to the Geode mods directory.
// clear data dir * This method is not recommended for casual use, the new mod will not be automatically removed.
val testDirPath = File(filesDir.path + File.separator + "testlib" + File.separator) */
if (testDirPath.exists()) { private fun loadInternalMods() {
testDirPath.deleteRecursively() val internalModBase = "mods"
val modListing = try {
assets.list(internalModBase)
} catch (ioe: IOException) {
emptyArray<String>()
} }
testDirPath.mkdir()
val dir = LaunchUtils.getBaseDirectory(this) val modDirectory = File(
val testingPath = File(dir, "test") LaunchUtils.getBaseDirectory(this),
"game/geode/mods"
testingPath.walk().forEach {
if (it.isFile) {
// welcome to the world of Android classloader permissions
val outputFile = File(testDirPath, it.name)
DownloadUtils.copyFile(
FileInputStream(it),
FileOutputStream(outputFile)
) )
try { modDirectory.mkdirs()
println("Loading test library ${outputFile.name}")
System.load(outputFile.path) modListing?.forEach { fileName ->
} catch (e: UnsatisfiedLinkError) { if (fileName.endsWith(".geode")) {
e.printStackTrace() val modOutput = File(modDirectory, fileName)
}
val mod = assets.open("$internalModBase/$fileName")
DownloadUtils.copyFile(mod, modOutput.outputStream())
println("Copied internal mod $fileName")
} }
} }
} }
@@ -1,7 +1,9 @@
package com.geode.launcher package com.geode.launcher
import androidx.annotation.Keep
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
@Keep
object LauncherFix { object LauncherFix {
fun loadLibrary() { fun loadLibrary() {
System.loadLibrary(Constants.LAUNCHER_FIX_LIB_NAME) System.loadLibrary(Constants.LAUNCHER_FIX_LIB_NAME)
@@ -1,18 +1,31 @@
package com.geode.launcher package com.geode.launcher
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.DocumentsContract
import android.text.format.Formatter.formatShortFileSize import android.text.format.Formatter.formatShortFileSize
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -25,16 +38,26 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.api.ReleaseViewModel import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme import com.geode.launcher.ui.theme.Theme
@@ -42,21 +65,42 @@ import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
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.ReleaseManager import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.useCountdownTimer import com.geode.launcher.utils.useCountdownTimer
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import java.net.ConnectException import java.net.ConnectException
import java.net.UnknownHostException import java.net.UnknownHostException
data class LoadFailureInfo(
val title: LaunchUtils.LauncherError,
val description: String? = null,
val extendedMessage: String? = null
)
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val gdInstalled = LaunchUtils.isGeometryDashInstalled(packageManager) val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
val geodeInstalled = LaunchUtils.isGeodeInstalled(this) val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
val returnMessage = intent.extras?.getString(Constants.LAUNCHER_KEY_RETURN_MESSAGE) val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
val returnTitle = intent.extras?.getString(Constants.LAUNCHER_KEY_RETURN_TITLE) val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
@Suppress("DEPRECATION") // the new api is android 13 only (why)
val returnError = intent.extras?.getSerializable(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR)
as? LaunchUtils.LauncherError
val loadFailureInfo = if (returnError != null && returnMessage != null) {
LoadFailureInfo(returnError, returnMessage, returnExtendedMessage)
} else if (LaunchUtils.lastSessionCrashed(this)) {
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
} else { null }
setContent { setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME) val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
@@ -70,12 +114,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
val loadFailed = !returnMessage.isNullOrEmpty() && !returnTitle.isNullOrEmpty() MainScreen(gdInstalled, geodeInstalled, loadFailureInfo)
MainScreen(gdInstalled, geodeInstalled, loadFailed)
if (!returnMessage.isNullOrEmpty() && !returnTitle.isNullOrEmpty()) {
LoadFailedDialog(returnTitle, returnMessage)
}
} }
} }
} }
@@ -90,12 +129,49 @@ class MainActivity : ComponentActivity() {
} }
} }
@Composable
fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String, onDismiss: () -> Unit) {
val uriHandler = LocalUriHandler.current
ElevatedCard(modifier) {
Column(
// buttons add enough padding already, lower to compensate
modifier = Modifier.padding(
top = 20.dp,
start = 12.dp,
end = 12.dp,
bottom = 8.dp
),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
stringResource(R.string.launcher_update_available),
modifier = Modifier.padding(horizontal = 10.dp)
)
Row(modifier = Modifier.align(Alignment.End)) {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.launcher_update_dismiss))
}
Spacer(Modifier.size(4.dp))
TextButton(onClick = { uriHandler.openUri(openTo) }) {
Text(stringResource(R.string.launcher_download))
}
}
}
}
}
@Composable @Composable
fun UpdateProgressIndicator( fun UpdateProgressIndicator(
message: String, message: String,
releaseViewModel: ReleaseViewModel, releaseViewModel: ReleaseViewModel,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
progress: Float? = null progress: (() -> Float)? = null
) { ) {
Column( Column(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
@@ -109,7 +185,7 @@ fun UpdateProgressIndicator(
if (progress == null) { if (progress == null) {
LinearProgressIndicator() LinearProgressIndicator()
} else { } else {
LinearProgressIndicator(progress) LinearProgressIndicator(progress = progress)
} }
TextButton( TextButton(
@@ -147,6 +223,11 @@ fun UpdateMessageIndicator(
releaseViewModel.runReleaseCheck(true) releaseViewModel.runReleaseCheck(true)
}, },
) { ) {
Icon(
Icons.Filled.Refresh,
contentDescription = stringResource(R.string.launcher_retry_icon_alt)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.release_fetch_button_retry)) Text(stringResource(R.string.release_fetch_button_retry))
} }
} }
@@ -155,41 +236,21 @@ fun UpdateMessageIndicator(
} }
@Composable @Composable
fun LoadFailedDialog(returnTitle: String, returnMessage: String) { fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
var showDialog by remember { mutableStateOf(true) }
if (showDialog) {
AlertDialog(
icon = {
Icon(
painterResource(R.drawable.icon_error),
contentDescription = stringResource(R.string.launcher_error_icon_alt)
)
},
title = { Text(returnTitle) },
text = { Text(returnMessage) },
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text(stringResource(R.string.message_box_accept))
}
},
onDismissRequest = { showDialog = false }
)
}
}
@Composable
fun UpdateWarning() {
val context = LocalContext.current val context = LocalContext.current
val packageManager = context.packageManager val packageManager = context.packageManager
val gdVersionCode = remember { LaunchUtils.getGeometryDashVersionCode(packageManager) } val gdVersionCode = remember { GamePackageUtils.getGameVersionCode(packageManager) }
val gdVersionString = remember { LaunchUtils.getGeometryDashVersionString(packageManager) } val gdVersionString = remember { GamePackageUtils.getGameVersionString(packageManager) }
var showUpdateWarning by remember { mutableStateOf(true) } var lastDismissedVersion by PreferenceUtils.useLongPreference(
preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE
)
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE) { val canDismissRelease = gdVersionCode >= Constants.SUPPORTED_VERSION_CODE
if (showUpdateWarning) { val shouldDismiss = canDismissRelease && gdVersionCode == lastDismissedVersion
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && !shouldDismiss) {
AlertDialog( AlertDialog(
icon = { icon = {
Icon( Icon(
@@ -201,34 +262,65 @@ fun UpdateWarning() {
Text(stringResource(R.string.launcher_unsupported_version_title)) Text(stringResource(R.string.launcher_unsupported_version_title))
}, },
text = { text = {
val message = if (gdVersionCode >= Constants.SUPPORTED_VERSION_CODE) {
R.string.launcher_unsupported_version_description
} else {
R.string.launcher_outdated_version_description
}
Text(stringResource( Text(stringResource(
R.string.launcher_unsupported_version_description, message, gdVersionString, Constants.SUPPORTED_VERSION_STRING
gdVersionString, Constants.SUPPORTED_VERSION_STRING
)) ))
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
showUpdateWarning = false onDismiss()
onLaunch(context) lastDismissedVersion = gdVersionCode
onLaunch(context, inSafeMode)
}) { }) {
Text(stringResource(R.string.message_box_accept)) Text(stringResource(R.string.message_box_accept))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showUpdateWarning = false }) { TextButton(onClick = { onDismiss() }) {
Text(stringResource(R.string.message_box_cancel)) Text(stringResource(R.string.message_box_cancel))
} }
}, },
onDismissRequest = { showUpdateWarning = false } onDismissRequest = { onDismiss() }
) )
}
} else { } else {
LaunchedEffect(gdVersionCode) { LaunchedEffect(gdVersionCode) {
onLaunch(context) onLaunch(context, inSafeMode)
} }
} }
} }
@Composable
fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
AlertDialog(
icon = {
Icon(
Icons.Filled.Warning,
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
)
},
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.message_box_cancel))
}
},
confirmButton = {
TextButton(onClick = onLaunch) {
Text(stringResource(R.string.message_box_continue))
}
},
onDismissRequest = onDismiss
)
}
@Composable @Composable
fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) { fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) {
val context = LocalContext.current val context = LocalContext.current
@@ -245,22 +337,23 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource( ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
R.string.release_fetch_manual_check_required R.string.release_fetch_manual_check_required
) )
else -> state.exception.message else -> null
} }
} }
else -> state.exception.message else -> null
} }
UpdateMessageIndicator( UpdateMessageIndicator(
stringResource(R.string.release_fetch_failed, message ?: ""), stringResource(
R.string.release_fetch_failed,
message ?: stringResource(R.string.release_fetch_try_later)
),
modifier = modifier, modifier = modifier,
allowRetry = true, allowRetry = true,
releaseViewModel = releaseViewModel releaseViewModel = releaseViewModel
) )
} }
is ReleaseViewModel.ReleaseUIState.InDownload -> { is ReleaseViewModel.ReleaseUIState.InDownload -> {
val progress = state.downloaded / state.outOf.toDouble()
val downloaded = remember(state.downloaded) { val downloaded = remember(state.downloaded) {
formatShortFileSize(context, state.downloaded) formatShortFileSize(context, state.downloaded)
} }
@@ -277,7 +370,10 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
), ),
modifier = modifier, modifier = modifier,
releaseViewModel = releaseViewModel, releaseViewModel = releaseViewModel,
progress = progress.toFloat() progress = {
val progress = state.downloaded / state.outOf.toDouble()
progress.toFloat()
}
) )
} }
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> { is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
@@ -313,9 +409,328 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
} }
} }
} }
val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState()
val nextUpdateValue = nextUpdate
if (!releaseViewModel.isInUpdate && nextUpdateValue != null) {
val updateUrl = nextUpdateValue.getLauncherDownload()?.browserDownloadUrl
?: nextUpdateValue.htmlUrl
LauncherUpdateIndicator(
modifier = modifier,
openTo = updateUrl,
onDismiss = {
releaseViewModel.dismissLauncherUpdate()
}
)
}
}
@Composable
fun ErrorInfoBody(failureReason: LaunchUtils.LauncherError, modifier: Modifier = Modifier) {
val headline = when (failureReason) {
LaunchUtils.LauncherError.LINKER_FAILED -> stringResource(R.string.load_failed_link_error_description)
LaunchUtils.LauncherError.CRASHED -> stringResource(R.string.load_failed_crashed_description)
else -> stringResource(R.string.load_failed_generic_error_description)
}
val recommendations = when (failureReason) {
LaunchUtils.LauncherError.LINKER_FAILED -> listOf(
stringResource(R.string.load_failed_recommendation_reinstall),
stringResource(R.string.load_failed_recommendation_update),
)
LaunchUtils.LauncherError.CRASHED -> listOf(
stringResource(R.string.load_failed_recommendation_safe_mode),
stringResource(R.string.load_failed_recommendation_report)
)
else -> listOf(
stringResource(R.string.load_failed_recommendation_reinstall),
stringResource(R.string.load_failed_recommendation_update),
stringResource(R.string.load_failed_recommendation_report)
)
}
Column(modifier = modifier) {
Text(headline)
recommendations.forEach { r ->
Text("\u2022\u00A0\u00A0$r")
}
}
}
@Composable
fun ErrorInfoTitle(failureReason: LaunchUtils.LauncherError) {
val message = when (failureReason) {
LaunchUtils.LauncherError.LINKER_NEEDS_64BIT,
LaunchUtils.LauncherError.LINKER_NEEDS_32BIT,
LaunchUtils.LauncherError.LINKER_FAILED -> stringResource(R.string.load_failed_link_error)
LaunchUtils.LauncherError.GENERIC -> stringResource(R.string.load_failed_generic_error)
LaunchUtils.LauncherError.CRASHED -> stringResource(R.string.load_failed_crashed)
}
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.icon_error),
contentDescription = stringResource(R.string.launcher_error_icon_alt),
modifier = Modifier.size(28.dp)
)
Text(message, style = Typography.headlineMedium)
}
}
fun onShareCrash(context: Context) {
val lastCrash = LaunchUtils.getLastCrash(context)
if (lastCrash == null) {
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
return
}
val baseDirectory = LaunchUtils.getBaseDirectory(context)
val providerLocation = lastCrash.toRelativeString(baseDirectory)
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", documentsPath)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
type = "application/octet-stream"
}
try {
context.startActivity(shareIntent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
}
}
fun onShowLogs(context: Context) {
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
context.startActivity(launchIntent)
}
@Composable
fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(
stringResource(R.string.launcher_error_details),
style = Typography.headlineSmall
)
Spacer(Modifier.size(8.dp))
Text(
description,
fontFamily = FontFamily.Monospace,
style = Typography.bodyMedium
)
Spacer(Modifier.size(4.dp))
Text(
stringResource(R.string.launcher_error_device_info, Build.MODEL, Build.VERSION.RELEASE),
style = Typography.labelMedium
)
}
}
@Composable
@OptIn(ExperimentalLayoutApi::class)
fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(
12.dp, alignment = Alignment.CenterHorizontally
)
) {
if (extraDetails != null) {
Button(onClick = { onShowLogs(context) }) {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_error_view_logs))
}
FilledTonalButton(onClick = {
clipboardManager.setText(AnnotatedString(extraDetails))
}) {
Icon(
painterResource(R.drawable.icon_content_copy),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_error_copy))
}
} else {
Button(onClick = { onShareCrash(context) }) {
Icon(
Icons.Filled.Share,
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_error_export_crash))
}
FilledTonalButton(onClick = { onShowLogs(context) }) {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_error_view_logs))
}
}
}
}
fun onDownloadGame(context: Context) {
val appUrl = "https://play.google.com/store/apps/details?id=${Constants.PACKAGE_NAME}"
try {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(appUrl)
setPackage("com.android.vending")
}
context.startActivity(intent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
}
}
@Composable
fun GooglePlayBadge(modifier: Modifier = Modifier) {
val context = LocalContext.current
Image(
painter = painterResource(id = R.drawable.google_play_badge),
contentDescription = stringResource(R.string.launcher_download_game),
modifier = modifier
.width(196.dp)
.clip(AbsoluteRoundedCornerShape(4.dp))
.clickable { onDownloadGame(context) }
)
}
@Composable
fun DownloadRecommendation(needsUniversal: Boolean, modifier: Modifier = Modifier) {
val uriHandler = LocalUriHandler.current
val context = LocalContext.current
val version = BuildConfig.VERSION_NAME
val showDownload = remember {
!GamePackageUtils.isGameInstalled(context.packageManager) ||
!GamePackageUtils.identifyGameLegitimacy(context.packageManager)
}
val downloadBase = "https://github.com/geode-sdk/android-launcher/releases/download/$version"
val legacyDownloadUrl = "$downloadBase/geode-launcher-v$version-android32.apk"
val universalDownloadUrl = "$downloadBase/geode-launcher-v$version.apk"
val downloadUrl = if (needsUniversal)
universalDownloadUrl else legacyDownloadUrl
val downloadText = if (needsUniversal)
stringResource(R.string.launcher_download_universal)
else
stringResource(R.string.launcher_download_32bit)
val promptTitle = stringResource(R.string.load_failed_abi_error)
val reinstallText = stringResource(R.string.load_failed_recommendation_reinstall)
val recommendationText = if (needsUniversal)
stringResource(R.string.load_failed_recommendation_switch_64bit_abi)
else
stringResource(R.string.load_failed_recommendation_switch_32bit_abi)
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(
12.dp, alignment = Alignment.CenterVertically
)
) {
Text(promptTitle)
Text("\u2022\u00A0\u00A0$reinstallText")
if (showDownload) {
GooglePlayBadge()
}
Text("\u2022\u00A0\u00A0$recommendationText")
ClickableText(
text = AnnotatedString(downloadText),
onClick = {
uriHandler.openUri(downloadUrl)
},
style = TextStyle.Default.copy(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline
),
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ErrorInfoSheet(failureInfo: LoadFailureInfo, onDismiss: () -> Unit) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
// second container created for scroll (before padding is added)
Box(Modifier.verticalScroll(rememberScrollState())) {
Column(modifier = Modifier
.padding(horizontal = 24.dp)
) {
ErrorInfoTitle(failureInfo.title)
Spacer(Modifier.size(16.dp))
if (failureInfo.title.isAbiFailure()) {
DownloadRecommendation(
needsUniversal = failureInfo.title == LaunchUtils.LauncherError.LINKER_NEEDS_64BIT,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 16.dp)
)
} else {
ErrorInfoBody(failureInfo.title)
if (failureInfo.description != null) {
ErrorInfoDescription(
failureInfo.description,
modifier = Modifier.padding(vertical = 8.dp)
)
}
ErrorInfoActions(
extraDetails = failureInfo.extendedMessage ?: failureInfo.description,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 16.dp)
)
}
}
}
}
}
fun onLaunch(context: Context, safeMode: Boolean = false) {
if (safeMode) {
GeodeUtils.setAdditionalLaunchArguments(GeodeUtils.ARGUMENT_SAFE_MODE)
} else {
GeodeUtils.clearLaunchArguments()
} }
fun onLaunch(context: Context) {
val launchIntent = Intent(context, GeometryDashActivity::class.java) val launchIntent = Intent(context, GeometryDashActivity::class.java)
launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
@@ -327,65 +742,51 @@ fun onSettings(context: Context) {
context.startActivity(launchIntent) context.startActivity(launchIntent)
} }
suspend fun showFailureSnackbar(
context: Context,
loadFailureInfo: LoadFailureInfo,
snackbarHostState: SnackbarHostState,
onActionPerformed: () -> Unit
) {
val message = if (loadFailureInfo.title == LaunchUtils.LauncherError.CRASHED) {
context.getString(R.string.launcher_crashed)
} else {
context.getString(R.string.launcher_failed_to_load)
}
val res = snackbarHostState.showSnackbar(
message = message,
actionLabel = context.getString(R.string.launcher_error_more),
duration = SnackbarDuration.Indefinite,
)
when (res) {
SnackbarResult.ActionPerformed -> {
onActionPerformed()
}
SnackbarResult.Dismissed -> {}
}
}
@Composable @Composable
fun MainScreen( fun PlayButton(
gdInstalled: Boolean = true, stopAutomaticLaunch: Boolean,
geodePreinstalled: Boolean = true, blockLaunch: Boolean,
disableAutomaticLaunch: Boolean = false, onPlayGame: (Boolean) -> Unit
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
) { ) {
val context = LocalContext.current val context = LocalContext.current
val shouldAutomaticallyLaunch = PreferenceUtils.useBooleanPreference( var showSafeModeDialog by remember { mutableStateOf(false) }
var launchInSafeMode by remember { mutableStateOf(false) }
val shouldAutomaticallyLaunch by PreferenceUtils.useBooleanPreference(
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
) )
val shouldUpdate by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.UPDATE_AUTOMATICALLY) if (shouldAutomaticallyLaunch && !stopAutomaticLaunch && !showSafeModeDialog) {
val autoUpdateState by releaseViewModel.uiState.collectAsState()
val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
?.hasUpdated ?: false
val geodeInstalled = geodePreinstalled || geodeJustInstalled
var beginLaunch by remember { mutableStateOf(false) }
LaunchedEffect(shouldUpdate) {
if (shouldUpdate && !releaseViewModel.hasPerformedCheck) {
releaseViewModel.runReleaseCheck()
} else {
releaseViewModel.useGlobalCheckState()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.geode_logo),
contentDescription = context.getString(R.string.launcher_logo_alt),
modifier = Modifier.size(136.dp, 136.dp)
)
Text(
context.getString(R.string.launcher_title),
fontSize = 32.sp,
modifier = Modifier.padding(12.dp)
)
if (gdInstalled && geodeInstalled) {
if (shouldAutomaticallyLaunch.value && !releaseViewModel.isInUpdate && !disableAutomaticLaunch) {
val countdownTimer = useCountdownTimer( val countdownTimer = useCountdownTimer(
time = 3000, time = 3000,
onCountdownFinish = { onCountdownFinish = { onPlayGame(launchInSafeMode) }
// just in case this changed async
if (shouldAutomaticallyLaunch.value) {
beginLaunch = true
}
}
) )
if (countdownTimer != 0L) { if (countdownTimer != 0L) {
@@ -402,10 +803,42 @@ fun MainScreen(
} }
} }
// compose apis don't provide a good way of adding long press to a button
val interactionSource = remember { MutableInteractionSource() }
val viewConfiguration = LocalViewConfiguration.current
val haptics = LocalHapticFeedback.current
LaunchedEffect(interactionSource) {
interactionSource.interactions.collectLatest { interaction ->
when (interaction) {
is PressInteraction.Press -> {
launchInSafeMode = false
delay(viewConfiguration.longPressTimeoutMillis)
// perform a second delay to make the action more obvious
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
delay(viewConfiguration.longPressTimeoutMillis)
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
showSafeModeDialog = true
}
is PressInteraction.Release -> {
if (!showSafeModeDialog) {
onPlayGame(launchInSafeMode)
}
}
}
}
}
Row { Row {
Button( Button(
onClick = { beginLaunch = true }, onClick = { },
enabled = !releaseViewModel.isInUpdate enabled = !blockLaunch,
interactionSource = interactionSource
) { ) {
Icon( Icon(
Icons.Filled.PlayArrow, Icons.Filled.PlayArrow,
@@ -422,7 +855,136 @@ fun MainScreen(
) )
} }
} }
} else if (gdInstalled) {
if (showSafeModeDialog) {
SafeModeDialog(
onDismiss = {
launchInSafeMode = false
showSafeModeDialog = false
},
onLaunch = {
launchInSafeMode = true
onPlayGame(true)
showSafeModeDialog = false
}
)
}
}
@Composable
fun LaunchBlockedLabel(text: String) {
val context = LocalContext.current
val showDownload = remember {
GamePackageUtils.isGameInstalled(context.packageManager) &&
!GamePackageUtils.identifyGameLegitimacy(context.packageManager)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text, textAlign = TextAlign.Center, modifier = Modifier.padding(12.dp))
if (showDownload) {
GooglePlayBadge()
}
}
}
@Composable
fun MainScreen(
gdInstalled: Boolean = true,
geodePreinstalled: Boolean = true,
loadFailureInfo: LoadFailureInfo? = null,
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
) {
val context = LocalContext.current
val shouldUpdate by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
val autoUpdateState by releaseViewModel.uiState.collectAsState()
val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
?.hasUpdated ?: false
val geodeInstalled = geodePreinstalled || geodeJustInstalled
var beginLaunch by remember { mutableStateOf(false) }
var launchInSafeMode by remember { mutableStateOf(false) }
LaunchedEffect(shouldUpdate) {
if (shouldUpdate && !releaseViewModel.hasPerformedCheck && gdInstalled) {
releaseViewModel.runReleaseCheck()
} else {
releaseViewModel.useGlobalCheckState()
}
}
val snackbarHostState = remember { SnackbarHostState() }
// automatically show load failed dialog (but not crash)
var showErrorInfo by remember { mutableStateOf(
loadFailureInfo != null && loadFailureInfo.title != LaunchUtils.LauncherError.CRASHED
)}
val hasError = loadFailureInfo != null
LaunchedEffect(hasError, showErrorInfo) {
if (!showErrorInfo && loadFailureInfo != null) {
showFailureSnackbar(context, loadFailureInfo, snackbarHostState) {
showErrorInfo = true
}
}
}
if (showErrorInfo && loadFailureInfo != null) {
ErrorInfoSheet(loadFailureInfo, onDismiss = { showErrorInfo = false })
}
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.geode_logo),
contentDescription = context.getString(R.string.launcher_logo_alt),
modifier = Modifier.size(136.dp, 136.dp)
)
Text(
context.getString(R.string.launcher_title),
fontSize = 32.sp,
modifier = Modifier.padding(12.dp)
)
when {
gdInstalled && geodeInstalled -> {
val gdVersion = remember {
GamePackageUtils.getGameVersionCode(context.packageManager)
}
if (gdVersion < Constants.SUPPORTED_VERSION_CODE) {
val versionName = remember {
GamePackageUtils.getUnifiedVersionName(context.packageManager)
}
LaunchBlockedLabel(stringResource(R.string.game_outdated, versionName))
} else {
val stopLaunch = releaseViewModel.isInUpdate || hasError
PlayButton(
stopAutomaticLaunch = stopLaunch,
blockLaunch = releaseViewModel.isInUpdate,
onPlayGame = { safeMode ->
launchInSafeMode = safeMode
beginLaunch = true
},
)
}
}
gdInstalled -> {
Text( Text(
context.getString(R.string.geode_download_title), context.getString(R.string.geode_download_title),
modifier = Modifier.padding(12.dp) modifier = Modifier.padding(12.dp)
@@ -448,29 +1010,23 @@ fun MainScreen(
) )
} }
} }
} else {
Text(
context.getString(R.string.game_not_found),
modifier = Modifier.padding(12.dp)
)
OutlinedButton(onClick = { onSettings(context) }) {
Icon(
Icons.Filled.Settings,
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(context.getString(R.string.launcher_settings))
} }
else -> LaunchBlockedLabel(stringResource(R.string.game_not_found))
} }
UpdateCard( UpdateCard(
releaseViewModel, releaseViewModel,
modifier = Modifier.padding(12.dp) modifier = Modifier
.padding(12.dp)
) )
} }
}
if (beginLaunch) { if (beginLaunch) {
UpdateWarning() UpdateWarning(launchInSafeMode) {
beginLaunch = false
launchInSafeMode = false
}
} }
} }
@@ -13,6 +13,7 @@ import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
@@ -20,22 +21,18 @@ import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -44,9 +41,8 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.api.ReleaseViewModel import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme import com.geode.launcher.ui.theme.Theme
@@ -58,7 +54,9 @@ import java.net.UnknownHostException
class SettingsActivity : ComponentActivity() { class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME) val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption) val theme = Theme.fromInt(themeOption)
@@ -81,16 +79,6 @@ class SettingsActivity : ComponentActivity() {
} }
} }
/*
fun onExportSaveData(context: Context) {
Toast.makeText(
context,
"This function is not implemented yet!",
Toast.LENGTH_SHORT
).show()
}
*/
fun onOpenFolder(context: Context) { fun onOpenFolder(context: Context) {
val file = LaunchUtils.getBaseDirectory(context) val file = LaunchUtils.getBaseDirectory(context)
val clipboardManager = val clipboardManager =
@@ -157,7 +145,9 @@ fun UpdateIndicator(
enablePopup = true enablePopup = true
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble() val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
CircularProgressIndicator(progress.toFloat()) CircularProgressIndicator(
progress = { progress.toFloat() },
)
} }
else -> {} else -> {}
} }
@@ -203,6 +193,15 @@ fun themeToKey(theme: Int): String {
} }
} }
@Composable
fun displayOptionToKey(option: Int): String {
return when (option) {
1 -> stringResource(R.string.preference_display_mode_legacy)
2 -> stringResource(R.string.preference_display_mode_fullscreen)
else -> stringResource(R.string.preference_display_mode_default)
}
}
fun updateTheme(context: Context, theme: Int) { fun updateTheme(context: Context, theme: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
@@ -271,7 +270,7 @@ fun SettingsScreen(
navigationIcon = { navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) { IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = context.getString(R.string.back_icon_alt) contentDescription = context.getString(R.string.back_icon_alt)
) )
} }
@@ -312,19 +311,24 @@ fun SettingsScreen(
description = context.getString(R.string.preference_load_automatically_description), description = context.getString(R.string.preference_load_automatically_description),
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY, preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
) )
OptionsButton( SettingsSelectCard(
title = context.getString(R.string.preferences_copy_external_button), title = stringResource(R.string.preference_display_mode_name),
description = LaunchUtils.getBaseDirectory(context).path, dialogTitle = stringResource(R.string.preference_display_mode_select),
onClick = { onOpenFolder(context) } // necessary api doesn't exist on older versions of android
maxVal = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) 2 else 1,
preferenceKey = PreferenceUtils.Key.DISPLAY_MODE,
toLabel = { displayOptionToKey(it) }
) )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
SettingsCard(
title = context.getString(R.string.preference_force_hrr),
preferenceKey = PreferenceUtils.Key.FORCE_HRR,
)
}
OptionsButton( OptionsButton(
title = stringResource(R.string.preferences_open_file_manager), title = stringResource(R.string.preferences_open_file_manager),
onClick = { onOpenFileManager(context) } onClick = { onOpenFileManager(context) }
) )
OptionsButton(
title = stringResource(R.string.preferences_view_logs),
onClick = { onOpenLogs(context) }
)
} }
OptionsGroup(context.getString(R.string.preference_category_updater)) { OptionsGroup(context.getString(R.string.preference_category_updater)) {
@@ -361,251 +365,36 @@ fun SettingsScreen(
UpdateIndicator(snackbarHostState, updateStatus) UpdateIndicator(snackbarHostState, updateStatus)
} }
} }
/*
OptionsGroup("Data") { OptionsGroup(stringResource(R.string.preference_category_developer)) {
SettingsStringCard(
title = stringResource(R.string.preference_launch_arguments_name),
dialogTitle = stringResource(R.string.preference_launch_arguments_set_title),
preferenceKey = PreferenceUtils.Key.LAUNCH_ARGUMENTS,
filterInput = { it.filter { c ->
// if only there was a better way to define this!
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(){}<>[]?:;'\"~`-_+=\\| ".contains(c)
}}
)
OptionsButton( OptionsButton(
title = "Export save data", title = context.getString(R.string.preferences_copy_external_button),
onClick = { onExportSaveData(context) } description = LaunchUtils.getBaseDirectory(context).path,
onClick = { onOpenFolder(context) }
) )
} OptionsButton(
*/ title = stringResource(R.string.preferences_view_logs),
} onClick = { onOpenLogs(context) }
}
) )
} }
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
return preferences.toggleBoolean(preferenceKey)
}
fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
return preferences.getBoolean(preferenceKey)
}
@Composable
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
Column {
Text( Text(
title, stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME),
style = Typography.titleMedium, style = Typography.labelSmall,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
)
content()
}
}
@Composable
fun SettingsSelectCard(
title: String,
dialogTitle: String,
maxVal: Int,
preferenceKey: PreferenceUtils.Key,
toLabel: @Composable (Int) -> String,
extraSelectBehavior: ((Int) -> Unit)? = null
) {
val preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
var showDialog by remember { mutableStateOf(false) }
OptionsCard(
title = { OptionsTitle(title = title) },
modifier = Modifier
.clickable(
onClick = {
showDialog = true
},
role = Role.Button
)
) {
Text(toLabel(preferenceValue))
}
if (showDialog) {
val context = LocalContext.current
SelectDialog(
title = dialogTitle,
onDismissRequest = {
showDialog = false
},
onSelect = { selected ->
showDialog = false
PreferenceUtils.get(context)
.setInt(preferenceKey, selected)
extraSelectBehavior?.invoke(selected)
},
initialValue = preferenceValue,
toLabel = toLabel,
optionsCount = maxVal
) )
} }
} }
@Composable
fun SelectDialog(
title: String,
onDismissRequest: () -> Unit,
onSelect: (Int) -> Unit,
initialValue: Int,
toLabel: @Composable (Int) -> String,
optionsCount: Int,
) {
var selectedValue by remember { mutableIntStateOf(initialValue) }
Dialog(onDismissRequest = onDismissRequest) {
Card(
shape = RoundedCornerShape(16.dp)
) {
// styling a dialog is actually a little hard if you're doing what i'm doing
// maybe there's a better way to make these padding values...
Column {
Text(
title,
style = Typography.titleLarge,
modifier = Modifier.padding(
start = 28.dp,
top = 24.dp,
bottom = 12.dp
) )
)
// do not give the row or column padding!! it messes up the selection effect
(0..optionsCount).forEach { id ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { selectedValue = id },
role = Role.RadioButton
)
.padding(horizontal = 12.dp)
) {
RadioButton(
selected = selectedValue == id,
onClick = { selectedValue = id }
)
Text(
toLabel(id),
style = Typography.bodyMedium
)
}
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 16.dp,
end = 16.dp,
top = 4.dp
)
) {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
TextButton(onClick = { onSelect(selectedValue) }) {
Text(stringResource(R.string.message_box_accept))
}
}
}
}
}
}
@Composable
fun OptionsButton(title: String, description: String? = null, onClick: () -> Unit) {
OptionsCard(
title = {
OptionsTitle(
title = title,
description = description
)
},
modifier = Modifier
.clickable(onClick = onClick, role = Role.Button)
) { }
}
@Composable
fun SettingsCard(title: String, description: String? = null, preferenceKey: PreferenceUtils.Key) {
val context = LocalContext.current
val settingEnabled = remember {
mutableStateOf(getSetting(context, preferenceKey))
}
OptionsCard(
title = {
OptionsTitle(
Modifier.fillMaxWidth(0.75f),
title = title,
description = description
)
},
modifier = Modifier.toggleable(
value = settingEnabled.value,
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
role = Role.Switch,
)
) {
Switch(checked = settingEnabled.value, onCheckedChange = null)
}
}
@Composable
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null) {
Column(
modifier,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(title)
if (!description.isNullOrEmpty()) {
Text(
description,
style = Typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
@Composable
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) {
Row(
modifier
.fillMaxWidth()
.height(64.dp)
.padding(horizontal = 16.dp),
Arrangement.SpaceBetween,
Alignment.CenterVertically,
) {
title()
content()
}
}
@Preview(showBackground = true)
@Composable
fun OptionsCardPreview() {
GeodeLauncherTheme {
OptionsGroup(title = "Preview Group") {
SettingsCard(
title = "Load files from /test",
description = "Very long testing description goes here. It is incredibly long, it should wrap onto a new line.",
preferenceKey = PreferenceUtils.Key.LOAD_TESTING
)
SettingsCard(
title = "Testing option 2",
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
)
}
}
} }
@Preview(showSystemUi = true) @Preview(showSystemUi = true)
@@ -0,0 +1,366 @@
package com.geode.launcher
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.PreferenceUtils
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
return preferences.toggleBoolean(preferenceKey)
}
fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
return preferences.getBoolean(preferenceKey)
}
@Composable
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
Column {
Text(
title,
style = Typography.titleMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
content()
}
}
@Composable
fun SettingsSelectCard(
title: String,
dialogTitle: String,
maxVal: Int,
preferenceKey: PreferenceUtils.Key,
toLabel: @Composable (Int) -> String,
extraSelectBehavior: ((Int) -> Unit)? = null
) {
val preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
var showDialog by remember { mutableStateOf(false) }
OptionsCard(
title = { OptionsTitle(title = title) },
modifier = Modifier
.clickable(
onClick = {
showDialog = true
},
role = Role.Button
)
) {
Text(toLabel(preferenceValue))
}
if (showDialog) {
val context = LocalContext.current
SelectDialog(
title = dialogTitle,
onDismissRequest = {
showDialog = false
},
onSelect = { selected ->
showDialog = false
PreferenceUtils.get(context)
.setInt(preferenceKey, selected)
extraSelectBehavior?.invoke(selected)
},
initialValue = preferenceValue,
toLabel = toLabel,
optionsCount = 0..maxVal
)
}
}
@Composable
fun SettingsStringCard(
title: String,
dialogTitle: String,
preferenceKey: PreferenceUtils.Key,
filterInput: ((String) -> String)? = null
) {
var preferenceValue by PreferenceUtils.useStringPreference(preferenceKey)
var showDialog by remember { mutableStateOf(false) }
OptionsCard(
title = {
OptionsTitle(title = title, description = preferenceValue)
},
modifier = Modifier
.clickable(
onClick = {
showDialog = true
},
role = Role.Button
)
) { }
if (showDialog) {
StringDialog(
title = dialogTitle,
onDismissRequest = { showDialog = false },
onSelect = {
preferenceValue = it
showDialog = false
},
initialValue = preferenceValue ?: "",
filterInput = filterInput
)
}
}
@Composable
fun StringDialog(
title: String,
onDismissRequest: () -> Unit,
onSelect: (String) -> Unit,
initialValue: String,
filterInput: ((String) -> String)? = null
) {
var enteredValue by remember { mutableStateOf(initialValue) }
AlertDialog(
onDismissRequest = { onDismissRequest() },
title = {
Text(title)
},
text = {
OutlinedTextField(
value = enteredValue,
onValueChange = {
enteredValue = filterInput?.invoke(it) ?: it
},
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Ascii,
autoCorrect = false
),
trailingIcon = {
if (enteredValue.isNotEmpty()) {
IconButton(onClick = { enteredValue = "" }) {
Icon(
Icons.Filled.Clear,
contentDescription = stringResource(R.string.preference_text_clear)
)
}
}
}
)
},
confirmButton = {
TextButton(onClick = { onSelect(enteredValue) }) {
Text(stringResource(R.string.message_box_accept))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
},
)
}
@Composable
fun <T> SelectDialog(
title: String,
onDismissRequest: () -> Unit,
onSelect: (T) -> Unit,
initialValue: T,
toLabel: @Composable (T) -> String,
optionsCount: Iterable<T>,
) {
var selectedValue by remember { mutableStateOf(initialValue) }
Dialog(onDismissRequest = onDismissRequest) {
Card(
shape = RoundedCornerShape(16.dp)
) {
// styling a dialog is actually a little hard if you're doing what i'm doing
// maybe there's a better way to make these padding values...
Column {
Text(
title,
style = Typography.titleLarge,
modifier = Modifier.padding(
start = 28.dp,
top = 24.dp,
bottom = 12.dp
)
)
// do not give the row or column padding!! it messes up the selection effect
optionsCount.forEach { id ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { selectedValue = id },
role = Role.RadioButton
)
.padding(horizontal = 12.dp)
) {
RadioButton(
selected = selectedValue == id,
onClick = { selectedValue = id }
)
Text(
toLabel(id),
style = Typography.bodyMedium
)
}
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 16.dp,
end = 16.dp,
top = 4.dp
)
) {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
TextButton(onClick = { onSelect(selectedValue) }) {
Text(stringResource(R.string.message_box_accept))
}
}
}
}
}
}
@Composable
fun OptionsButton(title: String, description: String? = null, onClick: () -> Unit) {
OptionsCard(
title = {
OptionsTitle(
title = title,
description = description
)
},
modifier = Modifier
.clickable(onClick = onClick, role = Role.Button)
) { }
}
@Composable
fun SettingsCard(title: String, description: String? = null, preferenceKey: PreferenceUtils.Key) {
val context = LocalContext.current
val settingEnabled = remember {
mutableStateOf(getSetting(context, preferenceKey))
}
OptionsCard(
title = {
OptionsTitle(
Modifier.fillMaxWidth(0.75f),
title = title,
description = description
)
},
modifier = Modifier.toggleable(
value = settingEnabled.value,
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
role = Role.Switch,
)
) {
Switch(checked = settingEnabled.value, onCheckedChange = null)
}
}
@Composable
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null) {
Column(
modifier,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(title)
if (!description.isNullOrEmpty()) {
Text(
description,
style = Typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
@Composable
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) {
Row(
modifier
.fillMaxWidth()
.height(64.dp)
.padding(horizontal = 16.dp),
Arrangement.SpaceBetween,
Alignment.CenterVertically,
) {
title()
content()
}
}
@Preview(showBackground = true)
@Composable
fun OptionsCardPreview() {
GeodeLauncherTheme {
OptionsGroup(title = "Preview Group") {
SettingsCard(
title = "Load files from /test",
description = "Very long testing description goes here. It is incredibly long, it should wrap onto a new line.",
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
)
SettingsCard(
title = "Testing option 2",
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
)
}
}
}
@@ -3,11 +3,13 @@ package com.geode.launcher
import android.database.Cursor import android.database.Cursor
import android.database.MatrixCursor import android.database.MatrixCursor
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.CancellationSignal import android.os.CancellationSignal
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.DocumentsProvider import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import java.io.File import java.io.File
@@ -150,7 +152,7 @@ class UserDirectoryProvider : DocumentsProvider() {
notifyFileChange(file) notifyFileChange(file)
} }
override fun renameDocument(documentId: String, displayName: String): String? { override fun renameDocument(documentId: String, displayName: String): String {
val file = getFileForDocumentId(documentId) val file = getFileForDocumentId(documentId)
val dest = findFileNameForNewFile(File(file.parentFile, displayName)) val dest = findFileNameForNewFile(File(file.parentFile, displayName))
file.renameTo(dest) file.renameTo(dest)
@@ -203,4 +205,19 @@ class UserDirectoryProvider : DocumentsProvider() {
val uri = getDocumentUri(parentDocumentId) val uri = getDocumentUri(parentDocumentId)
context!!.contentResolver.notifyChange(uri, null) context!!.contentResolver.notifyChange(uri, null)
} }
@RequiresApi(Build.VERSION_CODES.O)
override fun findDocumentPath(
parentDocumentId: String?,
childDocumentId: String
): DocumentsContract.Path {
if (!parentDocumentId.isNullOrEmpty()) {
// not implementing this for now...
return super.findDocumentPath(parentDocumentId, childDocumentId)
}
val nonRootPath = listOf(ROOT) + childDocumentId.removePrefix(ROOT).split("/")
return DocumentsContract.Path(ROOT, nonRootPath)
}
} }
@@ -0,0 +1,36 @@
package com.geode.launcher.activityresult
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.activity.result.contract.ActivityResultContract
open class GeodeOpenFileActivityResult : ActivityResultContract<GeodeOpenFileActivityResult.OpenFileParams, Uri?>() {
class OpenFileParams(val extraMimes: Array<String>, val defaultPath: Uri?) {}
override fun createIntent(context: Context, input: OpenFileParams): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_MIME_TYPES, input.extraMimes)
.setType("*/*")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (input.defaultPath != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
}
}
return intent
}
final override fun getSynchronousResult(
context: Context,
input: OpenFileParams
): SynchronousResult<Uri?>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
@@ -0,0 +1,55 @@
package com.geode.launcher.activityresult
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.activity.result.contract.ActivityResultContract
open class GeodeOpenFilesActivityResult : ActivityResultContract<GeodeOpenFilesActivityResult.OpenFileParams, List<@JvmSuppressWildcards Uri>>() {
class OpenFileParams(val extraMimes: Array<String>, val defaultPath: Uri?) {}
override fun createIntent(context: Context, input: OpenFileParams): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra(Intent.EXTRA_MIME_TYPES, input.extraMimes)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
.setType("*/*")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (input.defaultPath != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
}
}
return intent
}
final override fun getSynchronousResult(
context: Context,
input: OpenFileParams
): SynchronousResult<List<@JvmSuppressWildcards Uri>>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> {
// Gracefully stolen from ActivityResultContracts.Intent.getclipDataUris
val clipData = intent.takeIf {
resultCode == Activity.RESULT_OK
}?.clipData
val resultSet = LinkedHashSet<Uri>()
intent?.data?.let { data ->
resultSet.add(data)
}
if (clipData == null && resultSet.isEmpty()) {
return emptyList()
} else if (clipData != null) {
for (i in 0 until clipData.itemCount) {
val uri = clipData.getItemAt(i).uri
if (uri != null) {
resultSet.add(uri)
}
}
}
return ArrayList(resultSet)
}
}
@@ -0,0 +1,41 @@
package com.geode.launcher.activityresult
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.activity.result.contract.ActivityResultContract
import androidx.documentfile.provider.DocumentFile
import java.io.File
open class GeodeSaveFileActivityResult : ActivityResultContract<GeodeSaveFileActivityResult.SaveFileParams, Uri?>() {
class SaveFileParams(val mimeType: String?, val initialPath: File?)
override fun createIntent(context: Context, input: SaveFileParams): Intent {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setType(input.mimeType ?: "*/*")
if (input.initialPath != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val uri = DocumentFile.fromFile(input.initialPath).uri
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
}
intent.putExtra(Intent.EXTRA_TITLE, input.initialPath.name)
}
return intent
}
final override fun getSynchronousResult(
context: Context,
input: SaveFileParams
): SynchronousResult<Uri?>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
@@ -1,66 +0,0 @@
package com.geode.launcher.api
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import kotlinx.serialization.json.okio.decodeFromBufferedSource
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
import java.net.URL
class ReleaseRepository(private val httpClient: OkHttpClient) {
companion object {
private const val GITHUB_API_BASE = "https://api.github.com/repos/geode-sdk/geode"
private const val GITHUB_API_HEADER = "X-GitHub-Api-Version"
private const val GITHUB_API_VERSION = "2022-11-28"
}
suspend fun getLatestNightlyRelease(): Release? {
val nightlyPath = "$GITHUB_API_BASE/releases/tags/nightly"
val url = URL(nightlyPath)
return getReleaseByUrl(url)
}
suspend fun getLatestRelease(): Release? {
val releasePath = "$GITHUB_API_BASE/releases/latest"
val url = URL(releasePath)
return getReleaseByUrl(url)
}
@OptIn(ExperimentalSerializationApi::class)
private suspend fun getReleaseByUrl(url: URL): Release? {
val request = Request.Builder()
.url(url)
.addHeader("Accept", "application/json")
.addHeader(GITHUB_API_HEADER, GITHUB_API_VERSION)
.build()
val call = httpClient.newCall(request)
val response = call.executeCoroutine()
return when (response.code) {
200 -> {
val format = Json {
namingStrategy = JsonNamingStrategy.SnakeCase
ignoreUnknownKeys = true
}
val release = format.decodeFromBufferedSource<Release>(
response.body!!.source()
)
release
}
404 -> {
null
}
else -> {
throw IOException("unknown response ${response.code}")
}
}
}
}
@@ -5,14 +5,14 @@ import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaInstant import kotlinx.datetime.toJavaInstant
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import java.io.DataInputStream import okio.BufferedSource
import java.io.IOException import java.io.IOException
fun DataInputStream.readCChar(): Char { fun BufferedSource.readCChar(): Char {
return this.readByte().toInt().toChar() return this.readUtf8CodePoint().toChar()
} }
fun DataInputStream.readCString(): String { fun BufferedSource.readCString(): String {
val buffer = StringBuilder() val buffer = StringBuilder()
var lastByte = this.readCChar() var lastByte = this.readCChar()
@@ -24,40 +24,55 @@ fun DataInputStream.readCString(): String {
return buffer.toString() return buffer.toString()
} }
fun DataInputStream.readLeUShort(): UShort {
val byteA = this.readByte().toInt() and 0xff
val byteB = this.readByte().toInt() and 0xff
val shortValue = (byteB shl 8) or byteA
return shortValue.toUShort()
}
fun DataInputStream.readLeInt(): Int {
val byteA = this.readByte().toInt() and 0xff
val byteB = this.readByte().toInt() and 0xff
val byteC = this.readByte().toInt() and 0xff
val byteD = this.readByte().toInt() and 0xff
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
}
fun DataInputStream.readLeUInt(): UInt {
val byteA = this.readByte().toUInt() and 0xffu
val byteB = this.readByte().toUInt() and 0xffu
val byteC = this.readByte().toUInt() and 0xffu
val byteD = this.readByte().toUInt() and 0xffu
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
}
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int) data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
enum class LogPriority { enum class LogPriority {
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT; UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
class LogPriorityIterator(
start: LogPriority,
private val end: LogPriority,
private val step: Int
) : Iterator<LogPriority> {
private var hasNext: Boolean = if (step > 0) start <= end else start >= end
private var next = if (hasNext) start else end
override fun hasNext() = hasNext
override fun next(): LogPriority {
val current = next
if (current == end) {
if (!hasNext) {
throw NoSuchElementException()
}
hasNext = false
} else {
next = LogPriority.fromInt(next.toInt() + step)
}
return current
}
}
class LogPriorityProgression(
override val start: LogPriority,
override val endInclusive: LogPriority,
private val step: Int = 1
) : Iterable<LogPriority>, ClosedRange<LogPriority> {
override fun iterator(): Iterator<LogPriority> {
return LogPriorityIterator(start, endInclusive, step)
}
}
infix fun downTo(to: LogPriority) = LogPriorityProgression(this, to, -1)
operator fun rangeTo(to: LogPriority) = LogPriorityProgression(this, to)
companion object { companion object {
fun fromByte(byte: Byte): LogPriority { fun fromByte(byte: Byte): LogPriority = fromInt(byte.toInt())
return when (byte.toInt()) {
fun fromInt(int: Int) = when (int) {
0x1 -> DEFAULT 0x1 -> DEFAULT
0x2 -> VERBOSE 0x2 -> VERBOSE
0x3 -> DEBUG 0x3 -> DEBUG
@@ -69,6 +84,17 @@ enum class LogPriority {
else -> UNKNOWN else -> UNKNOWN
} }
} }
fun toInt() = when (this) {
DEFAULT -> 0x1
VERBOSE -> 0x2
DEBUG -> 0x3
INFO -> 0x4
WARN -> 0x5
ERROR -> 0x6
FATAL -> 0x7
SILENT -> 0x8
else -> 0x0
} }
fun toChar(): Char { fun toChar(): Char {
@@ -107,7 +133,7 @@ data class LogLine(
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)") else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
} }
fun fromInputStream(readStream: DataInputStream): LogLine { fun fromBufferedSource(source: BufferedSource): LogLine {
/* /*
// from android <liblog/include/log/log_read.h> // from android <liblog/include/log/log_read.h>
// there are multiple logger entry formats // there are multiple logger entry formats
@@ -135,27 +161,27 @@ data class LogLine(
}; };
*/ */
/* val payloadLength = */ readStream.readLeUShort() /* val payloadLength = */ source.readShortLe()
val headerSize = readStream.readLeUShort() val headerSize = source.readShortLe().toUShort()
val entryVersion = headerSizeToVersion(headerSize) val entryVersion = headerSizeToVersion(headerSize)
val pid = readStream.readLeInt() val pid = source.readIntLe()
val tid = readStream.readLeUInt().toInt() val tid = source.readIntLe()
val sec = readStream.readLeUInt().toLong() val sec = source.readIntLe().toLong()
val nSec = readStream.readLeUInt().toInt() val nSec = source.readIntLe()
val lid = readStream.readLeUInt().toInt() val lid = source.readIntLe()
val uid = if (entryVersion == EntryVersion.V4) val uid = if (entryVersion == EntryVersion.V4)
readStream.readLeUInt().toInt() else 0 source.readIntLe() else 0
val processInformation = ProcessInformation(pid, tid, uid) val processInformation = ProcessInformation(pid, tid, uid)
val time = Instant.fromEpochSeconds(sec, nSec) val time = Instant.fromEpochSeconds(sec, nSec)
val priorityByte = readStream.readByte() val priorityByte = source.readByte()
val priority = LogPriority.fromByte(priorityByte) val priority = LogPriority.fromByte(priorityByte)
val tag = readStream.readCString() val tag = source.readCString()
val message = readStream.readCString() val message = source.readCString()
return LogLine( return LogLine(
process = processInformation, process = processInformation,
@@ -179,6 +205,10 @@ data class LogLine(
val identifier = time.toJavaInstant() val identifier = time.toJavaInstant()
val formattedTime = this.time.toLocalDateTime(TimeZone.currentSystemDefault()) val messageTrimmed by lazy { this.message.trim() }
val asSimpleString = "$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.message}"
val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
val asSimpleString by lazy {
"$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
}
} }
@@ -4,14 +4,16 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import java.io.DataInputStream import okio.buffer
import okio.source
import java.io.EOFException import java.io.EOFException
import java.util.LinkedList import java.io.IOException
/** /**
* ViewModel to manage log output from logcat. * ViewModel to manage log output from logcat.
@@ -25,25 +27,59 @@ class LogViewModel: ViewModel() {
private val _lineState = mutableStateListOf<LogLine>() private val _lineState = mutableStateListOf<LogLine>()
val lineState = _lineState val lineState = _lineState
private var _isLoading = MutableStateFlow(false) private val _isLoading = MutableStateFlow(false)
var isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
private var failedToLoad = false
private var logJob: Job? = null
var filterCrashes = false
private set
// skip debug/verbose lines by default
var logLevel = LogPriority.INFO
set(value) {
field = value
_lineState.clear()
loadLogs()
}
fun toggleCrashBuffer() {
filterCrashes = !filterCrashes
_lineState.clear()
loadLogs()
}
private suspend fun logOutput(): List<LogLine> { private suspend fun logOutput(): List<LogLine> {
val logLines = LinkedList<LogLine>() val logLines = ArrayList<LogLine>()
val logStream = DataInputStream(
// -B = binary format, -d = dump logs // -B = binary format, -d = dump logs
Runtime.getRuntime().exec("logcat -B -d").inputStream val logCommand = if (filterCrashes) "logcat -b crash -B -d"
) else "logcat -B -d"
val logProcess = try {
Runtime.getRuntime().exec(logCommand)
} catch (ioe: IOException) {
ioe.printStackTrace()
logLines += LogLine.showException(ioe)
failedToLoad = true
return logLines
}
val logSource = logProcess.inputStream.source().buffer()
try { try {
coroutineScope { coroutineScope {
// this runs until the stream is exhausted // this runs until the stream is exhausted
while (true) { while (true) {
val line = LogLine.fromInputStream(logStream) val line = LogLine.fromBufferedSource(logSource)
// skip debug/verbose lines if (line.priority >= logLevel) {
if (line.priority >= LogPriority.INFO) {
logLines += line logLines += line
} }
@@ -55,12 +91,17 @@ class LogViewModel: ViewModel() {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
logLines += LogLine.showException(e) logLines += LogLine.showException(e)
// technically it maybe didn't completely fail...
failedToLoad = true
} }
return logLines return logLines
} }
fun clearLogs() { fun clearLogs() {
logJob?.cancel()
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
// -c = clear // -c = clear
Runtime.getRuntime().exec("logcat -c") Runtime.getRuntime().exec("logcat -c")
@@ -70,17 +111,40 @@ class LogViewModel: ViewModel() {
_lineState.clear() _lineState.clear()
} }
fun getLogData(): String { private fun dumpLogcatText(): String {
return _lineState.joinToString("\n") { it.asSimpleString } val logCommand = if (filterCrashes) "logcat -b crash -d"
else "logcat -d"
val logProcess = try {
Runtime.getRuntime().exec(logCommand)
} catch (ioe: IOException) {
ioe.printStackTrace()
return ioe.stackTraceToString()
} }
init { return logProcess.inputStream.bufferedReader().readText()
}
fun getLogData(): String = if (failedToLoad) {
dumpLogcatText()
} else {
_lineState.joinToString("\n") { it.asSimpleString }
}
private fun loadLogs() {
_isLoading.value = true _isLoading.value = true
viewModelScope.launch(Dispatchers.IO) { logJob?.cancel()
logJob = viewModelScope.launch(Dispatchers.IO) {
val lines = logOutput() val lines = logOutput()
_lineState.addAll(lines) _lineState.addAll(lines)
_isLoading.value = false _isLoading.value = false
logJob = null
} }
} }
init {
loadLogs()
}
} }
@@ -79,24 +79,6 @@ fun GeodeLauncherTheme(
else -> LightColorScheme else -> LightColorScheme
} }
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
with ((view.context as Activity).window) {
WindowCompat.setDecorFitsSystemWindows(this, false)
statusBarColor = Color.Transparent.toArgb()
navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(this, view).apply {
isAppearanceLightStatusBars = theme == Theme.LIGHT
isAppearanceLightNavigationBars = theme == Theme.LIGHT
}
}
}
}
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, typography = Typography,
@@ -1,4 +1,4 @@
package com.geode.launcher.api package com.geode.launcher.updater
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
@@ -23,7 +23,8 @@ class Release(
val tagName: String, val tagName: String,
val createdAt: Instant, val createdAt: Instant,
val publishedAt: Instant, val publishedAt: Instant,
val assets: List<Asset> val assets: List<Asset>,
val htmlUrl: String
) { ) {
fun getDescription(): String { fun getDescription(): String {
if (tagName == "nightly") { if (tagName == "nightly") {
@@ -42,7 +43,7 @@ class Release(
return createdAt.epochSeconds return createdAt.epochSeconds
} }
fun getAndroidDownload(): Asset? { fun getGeodeDownload(): Asset? {
// try to find an asset that matches the architecture first // try to find an asset that matches the architecture first
val platform = LaunchUtils.platformName val platform = LaunchUtils.platformName
@@ -51,4 +52,22 @@ class Release(
it.name.endsWith(releaseSuffix) it.name.endsWith(releaseSuffix)
} }
} }
fun getLauncherDownload(): Asset? {
val platform = LaunchUtils.platformName
val use32BitPlatform = platform == "android32"
return assets.find { asset ->
/* you know it's good when you pull out the truth table
* u32bp | contains | found
* 1 | 1 | 1
* 0 | 1 | 0
* 1 | 0 | 0
* 0 | 0 | 1
* surprise! it's an xnor
*/
(asset.name.contains("android32")) == use32BitPlatform
}
}
} }
@@ -1,9 +1,11 @@
package com.geode.launcher.utils package com.geode.launcher.updater
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.geode.launcher.api.Release import com.geode.launcher.BuildConfig
import com.geode.launcher.api.ReleaseRepository import com.geode.launcher.utils.DownloadUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -15,6 +17,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okio.buffer
import okio.source
import java.io.File import java.io.File
import java.io.InterruptedIOException import java.io.InterruptedIOException
@@ -30,7 +34,7 @@ class ReleaseManager private constructor(
private lateinit var managerInstance: ReleaseManager private lateinit var managerInstance: ReleaseManager
fun get(context: Context): ReleaseManager { fun get(context: Context): ReleaseManager {
if (!::managerInstance.isInitialized) { if (!Companion::managerInstance.isInitialized) {
val applicationContext = context.applicationContext val applicationContext = context.applicationContext
val httpClient = OkHttpClient.Builder() val httpClient = OkHttpClient.Builder()
@@ -74,6 +78,9 @@ class ReleaseManager private constructor(
val isInUpdate: Boolean val isInUpdate: Boolean
get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished
private val _availableLauncherUpdate = MutableStateFlow<Release?>(null)
val availableLauncherUpdate = _availableLauncherUpdate.asStateFlow()
private fun sendError(e: Exception) { private fun sendError(e: Exception) {
_uiState.value = ReleaseManagerState.Failure(e) _uiState.value = ReleaseManagerState.Failure(e)
@@ -84,21 +91,24 @@ class ReleaseManager private constructor(
} }
} }
fun dismissLauncherUpdate() {
val update = availableLauncherUpdate.value ?: return
val sharedPreferences = PreferenceUtils.get(applicationContext)
sharedPreferences.setString(PreferenceUtils.Key.LAST_DISMISSED_UPDATE, update.tagName)
_availableLauncherUpdate.value = null
}
private suspend fun getLatestRelease(): Release? { private suspend fun getLatestRelease(): Release? {
val sharedPreferences = PreferenceUtils.get(applicationContext) val sharedPreferences = PreferenceUtils.get(applicationContext)
val useNightly = sharedPreferences.getBoolean(PreferenceUtils.Key.RELEASE_CHANNEL) val useNightly = sharedPreferences.getBoolean(PreferenceUtils.Key.RELEASE_CHANNEL)
val latestRelease = if (useNightly) { return releaseRepository.getLatestGeodeRelease(useNightly)
releaseRepository.getLatestNightlyRelease()
} else {
releaseRepository.getLatestRelease()
}
return latestRelease
} }
private suspend fun performUpdate(release: Release) { private suspend fun performUpdate(release: Release) {
val releaseAsset = release.getAndroidDownload() val releaseAsset = release.getGeodeDownload()
if (releaseAsset == null) { if (releaseAsset == null) {
val noAssetException = Exception("missing Android download") val noAssetException = Exception("missing Android download")
_uiState.value = ReleaseManagerState.Failure(noAssetException) _uiState.value = ReleaseManagerState.Failure(noAssetException)
@@ -110,7 +120,10 @@ class ReleaseManager private constructor(
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size.toLong()) _uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size.toLong())
try { try {
val fileStream = DownloadUtils.downloadStream(httpClient, releaseAsset.browserDownloadUrl) { progress, outOf -> val fileStream = DownloadUtils.downloadStream(
httpClient,
releaseAsset.browserDownloadUrl
) { progress, outOf ->
_uiState.value = ReleaseManagerState.InDownload(progress, outOf) _uiState.value = ReleaseManagerState.InDownload(progress, outOf)
} }
@@ -145,13 +158,25 @@ class ReleaseManager private constructor(
} }
val sharedPreferences = PreferenceUtils.get(applicationContext) val sharedPreferences = PreferenceUtils.get(applicationContext)
val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED)
?: return false
val fileLastModified = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED) val currentFileHash = computeFileHash(geodeFile)
if (fileLastModified == 0L) {
return false return originalFileHash != currentFileHash
} }
return fileLastModified != geodeFile.lastModified() private fun checkLauncherUpdate(launcherUpdate: Release) {
val sharedPreferences = PreferenceUtils.get(applicationContext)
val lastDismissedVersion = sharedPreferences.getString(PreferenceUtils.Key.LAST_DISMISSED_UPDATE)
if (!lastDismissedVersion.isNullOrEmpty() && launcherUpdate.tagName == lastDismissedVersion) {
return
}
if (launcherUpdate.tagName != BuildConfig.VERSION_NAME) {
_availableLauncherUpdate.value = launcherUpdate
}
} }
private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) { private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) {
@@ -162,6 +187,17 @@ class ReleaseManager private constructor(
return return
} }
val launcherRelease = try {
releaseRepository.getLatestLauncherRelease()
} catch (e: Exception) {
sendError(e)
return
}
if (launcherRelease != null) {
checkLauncherUpdate(launcherRelease)
}
if (release == null) { if (release == null) {
_uiState.value = ReleaseManagerState.Finished() _uiState.value = ReleaseManagerState.Finished()
return return
@@ -176,7 +212,7 @@ class ReleaseManager private constructor(
val geodeFile = getGeodeOutputPath() val geodeFile = getGeodeOutputPath()
// check if an update is needed // check if an update is needed
if (latestVersion <= currentVersion && geodeFile.exists()) { if (latestVersion == currentVersion && geodeFile.exists()) {
_uiState.value = ReleaseManagerState.Finished() _uiState.value = ReleaseManagerState.Finished()
return return
} }
@@ -203,13 +239,16 @@ class ReleaseManager private constructor(
release.getDescriptor() release.getDescriptor()
) )
// store hash as modified time is unreliable (don't know why)
val outputFile = getGeodeOutputPath() val outputFile = getGeodeOutputPath()
sharedPreferences.setLong( val fileHash = computeFileHash(outputFile)
PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED,
outputFile.lastModified() sharedPreferences.setString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED, fileHash)
)
} }
private fun computeFileHash(file: File): String =
file.source().buffer().readByteString().md5().hex()
private fun getGeodeOutputPath(): File { private fun getGeodeOutputPath(): File {
val geodeName = LaunchUtils.geodeFilename val geodeName = LaunchUtils.geodeFilename
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext) val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
@@ -0,0 +1,104 @@
package com.geode.launcher.updater
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.Instant
import kotlinx.datetime.until
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import kotlinx.serialization.json.okio.decodeFromBufferedSource
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
import java.net.URL
class ReleaseRepository(private val httpClient: OkHttpClient) {
companion object {
private const val GITHUB_API_BASE = "https://api.github.com"
private const val GITHUB_API_HEADER = "X-GitHub-Api-Version"
private const val GITHUB_API_VERSION = "2022-11-28"
private const val GITHUB_RATELIMIT_REMAINING = "x-ratelimit-remaining"
private const val GITHUB_RATELIMIT_RESET = "x-ratelimit-reset"
}
suspend fun getLatestLauncherRelease(): Release? {
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
val url = URL(releasePath)
return getReleaseByUrl(url)
}
suspend fun getLatestGeodeRelease(isNightly: Boolean = false): Release? {
val geodeBaseUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases"
val releasePath = if (isNightly) "$geodeBaseUrl/tags/nightly"
else "$geodeBaseUrl/latest"
val url = URL(releasePath)
return getReleaseByUrl(url)
}
private fun generateRateLimitMessage(resetTime: Instant): String {
val currentTime = Clock.System.now()
val resetDelay = currentTime.until(resetTime, DateTimeUnit.SECOND)
val formattedWait = "${resetDelay / 60}m"
return "api ratelimit reached, try again in ${formattedWait}"
}
@OptIn(ExperimentalSerializationApi::class)
private suspend fun getReleaseByUrl(url: URL): Release? {
val request = Request.Builder()
.url(url)
.addHeader("Accept", "application/json")
.addHeader(GITHUB_API_HEADER, GITHUB_API_VERSION)
.build()
val call = httpClient.newCall(request)
val response = call.executeCoroutine()
return when (response.code) {
200 -> {
val format = Json {
namingStrategy = JsonNamingStrategy.SnakeCase
ignoreUnknownKeys = true
}
val release = format.decodeFromBufferedSource<Release>(
response.body!!.source()
)
release
}
403 -> {
// determine if the error code is a ratelimit
// (github docs say it sends 429 too, but haven't seen that)
val limitRemaining = response.headers.get(GITHUB_RATELIMIT_REMAINING)?.toInt()
val limitReset = response.headers.get(GITHUB_RATELIMIT_RESET)?.toLong()
if (limitRemaining == 0 && limitReset != null) {
// handle ratelimit with a custom error
// there's also a retry-after header but again, haven't seen
val resetTime = Instant.fromEpochSeconds(limitReset, 0L)
val msg = generateRateLimitMessage(resetTime)
throw IOException(msg)
}
throw IOException("response 403: ${response.body!!.string()}")
}
404 -> {
null
}
else -> {
throw IOException("unknown response ${response.code}")
}
}
}
}
@@ -1,4 +1,4 @@
package com.geode.launcher.api package com.geode.launcher.updater
import android.app.Application import android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -7,7 +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.utils.ReleaseManager import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -53,6 +53,8 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
val isInUpdate val isInUpdate
get() = ReleaseManager.get(application).isInUpdate get() = ReleaseManager.get(application).isInUpdate
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
var hasPerformedCheck = false var hasPerformedCheck = false
private set private set
@@ -78,7 +80,7 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
fun useGlobalCheckState() { fun useGlobalCheckState() {
hasPerformedCheck = true hasPerformedCheck = true
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
val releaseFlow = ReleaseManager.get(application) val releaseFlow = ReleaseManager.get(application)
.uiState .uiState
@@ -89,11 +91,17 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
fun runReleaseCheck(isManual: Boolean = false) { fun runReleaseCheck(isManual: Boolean = false) {
hasPerformedCheck = true hasPerformedCheck = true
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
val releaseFlow = ReleaseManager.get(application) val releaseFlow = ReleaseManager.get(application)
.checkForUpdates(isManual) .checkForUpdates(isManual)
syncUiState(releaseFlow) syncUiState(releaseFlow)
} }
} }
fun dismissLauncherUpdate() {
viewModelScope.launch(Dispatchers.IO) {
ReleaseManager.get(application).dismissLauncherUpdate()
}
}
} }
@@ -13,10 +13,6 @@ object Constants {
@SuppressLint("SdCardPath") @SuppressLint("SdCardPath")
const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}" const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}"
const val SUPPORTED_VERSION_CODE = 37L const val SUPPORTED_VERSION_CODE = 38L
const val SUPPORTED_VERSION_STRING = "2.2.13" const val SUPPORTED_VERSION_STRING = "2.205"
const val LAUNCHER_KEY_RETURN_TITLE = "return_title"
const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message"
} }
@@ -0,0 +1,32 @@
package com.geode.launcher.utils
import android.content.Context
import android.widget.FrameLayout
class ConstrainedFrameLayout(context: Context) : FrameLayout(context) {
// avoid running the aspect ratio fix when we don't need it
var aspectRatio: Float? = null;
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
if (aspectRatio != null) {
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
if (width == 0 || height == 0) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
val calculatedWidth = height * (aspectRatio ?: 1.0f);
val padding = ((width - calculatedWidth) / 2).toInt()
// ignore paddings where the screen ends up looking odd
if (padding > 0) {
setPadding(padding, 0, padding, 0)
}
}
super.onMeasure(
widthMeasureSpec,
heightMeasureSpec
)
}
}
@@ -116,19 +116,13 @@ object DownloadUtils {
fun copyFile(inputStream: InputStream, outputStream: OutputStream) { fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
// gotta love copying // gotta love copying
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FileUtils.copy(inputStream, outputStream) inputStream.use { input -> outputStream.use { output ->
FileUtils.copy(input, output)
}}
} else { } else {
inputStream.use { input -> inputStream.use { input -> outputStream.use { output ->
outputStream.use { output -> input.copyTo(output)
val buffer = ByteArray(4 * 1024) }}
while (true) {
val byteCount = input.read(buffer)
if (byteCount < 0) break
output.write(buffer, 0, byteCount)
}
output.flush()
}
}
} }
} }
} }
@@ -0,0 +1,196 @@
package com.geode.launcher.utils
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import java.io.File
class FileUtils {
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
// i am actually very lazy to move this to a separate class
// hello zmx i moved it
companion object {
fun getRealPathFromURI(context: Context, uri: Uri): String? {
when {
// DocumentProvider
DocumentsContract.isDocumentUri(context, uri) -> {
when {
// ExternalStorageProvider
isExternalStorageDocument(uri) -> {
return handleExternalStorageDocument(context, uri)
}
isDownloadsDocument(uri) -> {
return handleDownloadsDocument(context, uri)
}
isMediaDocument(uri) -> {
return handleMediaDocument(context, uri)
}
// TODO: add the geode provider
}
}
"content".equals(uri.scheme, ignoreCase = true) -> {
// Return the remote address
if (isGooglePhotosUri(uri)) return uri.lastPathSegment
// This happened with a directory
if (isExternalStorageDocument(uri)) {
return handleExternalStorageDocument(context, uri)
}
return getDataColumn(context, uri, null, null)
}
"file".equals(uri.scheme, ignoreCase = true) -> {
return uri.path
}
}
return null
}
private fun handleMediaDocument(context: Context, uri: Uri): String? {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(
context,
contentUri,
selection,
selectionArgs
)
}
private fun handleDownloadsDocument(context: Context, uri: Uri): String? {
val fileName = getFilePath(context, uri)
if (fileName != null) {
return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
}
var id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
id = id.replaceFirst("raw:".toRegex(), "")
val file = File(id)
if (file.exists()) return id
}
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
}
private fun handleExternalStorageDocument(context: Context, uri: Uri): String {
val docId: String = if (DocumentsContract.isDocumentUri(context, uri)) {
DocumentsContract.getDocumentId(uri)
} else {
// please please please PLEASE let this be the only other case
DocumentsContract.getTreeDocumentId(uri)
}
val split = docId.split(":").toTypedArray()
val type = split[0]
// This is for checking Main Memory
return if ("primary".equals(type, ignoreCase = true)) {
if (split.size > 1) {
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} else {
Environment.getExternalStorageDirectory().toString() + "/"
}
// This is for checking SD Card
} else {
"storage" + "/" + docId.replace(":", "/")
}
}
private fun getFilePath(context: Context, uri: Uri?): String? {
var cursor: Cursor? = null
val projection = arrayOf(
MediaStore.MediaColumns.DISPLAY_NAME
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(
uri, projection, null, null,
null
)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
val data = cursor.getString(index)
cursor.close()
return data
}
} catch (e: Exception) {
cursor?.close()
} finally {
cursor?.close()
}
return null
}
private fun getDataColumn(context: Context, uri: Uri?, selection: String?,
selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(
column
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(
uri, projection, selection, selectionArgs,
null
)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
val data = cursor.getString(index)
cursor.close()
return data
}
} catch (e: Exception) {
cursor?.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
}
}
@@ -0,0 +1,134 @@
package com.geode.launcher.utils
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.os.Build
import android.util.Log
import okio.ByteString.Companion.toByteString
object GamePackageUtils {
fun isGameInstalled(packageManager: PackageManager): Boolean {
return try {
packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
true
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
fun getGameVersionCode(packageManager: PackageManager): Long {
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
game.longVersionCode
} else {
@Suppress("DEPRECATION")
game.versionCode.toLong()
}
}
fun getGameVersionString(packageManager: PackageManager): String {
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
return game.versionName
}
private val gameVersionMap = mapOf(
37L to "2.200",
38L to "2.205"
)
/**
* Gets the version name that more closely aligns to the Windows/macOS releases
*/
fun getUnifiedVersionName(packageManager: PackageManager): String {
val versionCode = getGameVersionCode(packageManager)
return gameVersionMap[versionCode] ?: getGameVersionString(packageManager)
}
fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean {
val abi = LaunchUtils.applicationArchitecture
val isExtracted =
packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS
val isSplit = (packageInfo.applicationInfo.splitSourceDirs?.size ?: 0) > 1
val metadata =
"Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi"
Log.i("GeodeLauncher", metadata)
// easiest check! these messages are hardcoded in bionic/linker/linker_phdr.cpp
if (loadException.message?.contains("32-bit instead of 64-bit") == true) {
return true
}
if (loadException.message?.contains("64-bit instead of 32-bit") == true) {
return true
}
// determine if it can find gd libraries for the opposite architecture
val gdBinaryName = "lib${Constants.COCOS_LIB_NAME}.so"
val oppositeArchitecture = if (abi == "arm64-v8a") "armeabi-v7a" else "arm64-v8a"
// detect issues using native library directory (only works on extracted libraries)
val nativeLibraryDir = packageInfo.applicationInfo.nativeLibraryDir
if (nativeLibraryDir.contains(oppositeArchitecture)) {
return true
}
// android has some odd behavior where 32bit libraries get installed to the arm directory instead of armeabi-v7a
// detect it, ig
if (oppositeArchitecture == "armeabi-v7a" && nativeLibraryDir.contains("/lib/arm/")) {
return true
}
if (!isExtracted) {
try {
context.assets.openNonAssetFd("lib/$oppositeArchitecture/$gdBinaryName")
return true
} catch (_: Exception) {
// this is good, actually!
}
}
return false
}
@SuppressLint("DiscouragedPrivateApi")
fun addAssetsFromPackage(assetManager: AssetManager, packageInfo: PackageInfo) {
// this method is officially marked as deprecated but it is the only method we are allowed to reflect
// (the source recommends replacing with AssetManager.setApkAssets(ApkAssets[], boolean) lol)
val clazz = assetManager.javaClass
val aspMethod = clazz.getDeclaredMethod("addAssetPath", String::class.java)
aspMethod.invoke(assetManager, packageInfo.applicationInfo.sourceDir)
packageInfo.applicationInfo.splitSourceDirs?.forEach {
aspMethod.invoke(assetManager, it)
}
}
private const val GAME_CERTIFICATE_HASH = "f5e7d8284d72c461a5f022d0cf755df101c3bb1e69cbe241bc1aef2cc5610a43"
private fun validateCertificate(certificate: ByteArray): Boolean =
certificate.toByteString().sha256().hex() == GAME_CERTIFICATE_HASH
fun identifyGameLegitimacy(packageManager: PackageManager): Boolean {
@Suppress("DEPRECATION")
val certificates = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, PackageManager.GET_SIGNING_CERTIFICATES)
val signingInfo = game.signingInfo
signingInfo.signingCertificateHistory
} else {
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, PackageManager.GET_SIGNATURES)
game.signatures
}
return certificates.any {
validateCertificate(it.toByteArray())
}
}
}
@@ -1,62 +1,96 @@
package com.geode.launcher.utils package com.geode.launcher.utils
import android.app.Activity import android.Manifest
import android.content.ActivityNotFoundException
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.database.Cursor import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
import android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.Keep
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.geode.launcher.BuildConfig
import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
import java.io.File import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.net.URI
import kotlin.system.exitProcess import kotlin.system.exitProcess
@Suppress("unused") @Keep
@Suppress("unused", "KotlinJniMissingFunction")
object GeodeUtils { object GeodeUtils {
private lateinit var activity: WeakReference<AppCompatActivity> private lateinit var activity: WeakReference<AppCompatActivity>
private lateinit var fileResultLauncher: ActivityResultLauncher<Intent> private lateinit var openFileResultLauncher: ActivityResultLauncher<GeodeOpenFileActivityResult.OpenFileParams>
private lateinit var filesResultLauncher: ActivityResultLauncher<Intent> private lateinit var openDirectoryResultLauncher: ActivityResultLauncher<Uri?>
private lateinit var openFilesResultLauncher: ActivityResultLauncher<GeodeOpenFilesActivityResult.OpenFileParams>
private lateinit var saveFileResultLauncher: ActivityResultLauncher<GeodeSaveFileActivityResult.SaveFileParams>
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private lateinit var internalRequestPermissionsLauncher: ActivityResultLauncher<Array<String>>
private lateinit var internalRequestAllFilesLauncher: ActivityResultLauncher<Intent>
private var afterRequestPermissions: (() -> Unit)? = null
private var afterRequestPermissionsFailure: (() -> Unit)? = null
fun setContext(activity: AppCompatActivity) { fun setContext(activity: AppCompatActivity) {
this.activity = WeakReference(activity) this.activity = WeakReference(activity)
fileResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> openFileResultLauncher = activity.registerForActivityResult(GeodeOpenFileActivityResult()) { uri ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
if (data != null) {
val uri = data.data
if (uri != null) { if (uri != null) {
val path = getRealPathFromURI(activity, uri) val path = FileUtils.getRealPathFromURI(activity, uri)
if (path != null) { if (path != null) {
selectFileCallback(path) selectFileCallback(path)
return@registerForActivityResult return@registerForActivityResult
} }
Toast.makeText(activity, R.string.file_select_error, Toast.LENGTH_SHORT)
.show()
} else {
Toast.makeText(activity, R.string.no_file_selected, Toast.LENGTH_SHORT)
.show()
} }
failedCallback()
} }
openDirectoryResultLauncher = activity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
if (it != null) {
val path = FileUtils.getRealPathFromURI(activity, it)
if (path != null) {
selectFileCallback(path)
return@registerForActivityResult
}
Toast.makeText(activity, R.string.file_select_error, Toast.LENGTH_SHORT)
.show()
} else {
Toast.makeText(activity, R.string.no_file_selected, Toast.LENGTH_SHORT)
.show()
} }
failedCallback() failedCallback()
} }
filesResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> openFilesResultLauncher = activity.registerForActivityResult(GeodeOpenFilesActivityResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.isEmpty()) {
val data = result.data Toast.makeText(activity, R.string.no_file_selected, Toast.LENGTH_SHORT)
if (data != null) { .show()
val clipData = data.clipData
if (clipData != null) { failedCallback()
val paths = Array(clipData.itemCount) { "" } return@registerForActivityResult
for (i in 0 until clipData.itemCount) { }
val path = getRealPathFromURI(activity, clipData.getItemAt(i).uri) val paths: Array<String> = Array(result.size) {"n = $it"}
for (i in result.indices) {
val path = FileUtils.getRealPathFromURI(activity, result[i])
if (path != null) { if (path != null) {
paths[i] = path paths[i] = path
} }
@@ -64,24 +98,62 @@ object GeodeUtils {
selectFilesCallback(paths) selectFilesCallback(paths)
return@registerForActivityResult return@registerForActivityResult
} }
saveFileResultLauncher = activity.registerForActivityResult(GeodeSaveFileActivityResult()) { uri ->
if (uri != null) {
val path = FileUtils.getRealPathFromURI(activity, uri)
if (path != null) {
selectFileCallback(path)
return@registerForActivityResult
} }
Toast.makeText(activity, R.string.file_select_error, Toast.LENGTH_SHORT)
.show()
} else {
Toast.makeText(activity, R.string.no_file_selected, Toast.LENGTH_SHORT)
.show()
} }
failedCallback() failedCallback()
} }
requestPermissionLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
permissionCallback(isGranted)
}
internalRequestPermissionsLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val isGranted = permissions.values.all { it }
if (isGranted) {
afterRequestPermissions?.invoke()
} else {
Toast.makeText(activity, R.string.missing_permissions, Toast.LENGTH_SHORT)
.show()
afterRequestPermissionsFailure?.invoke()
}
}
// only necessary on newer android versions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
internalRequestAllFilesLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
if (Environment.isExternalStorageManager()) {
afterRequestPermissions?.invoke()
} else {
Toast.makeText(activity, R.string.missing_permissions, Toast.LENGTH_SHORT)
.show()
afterRequestPermissionsFailure?.invoke()
}
}
}
} }
@JvmStatic @JvmStatic
fun getLogcatCrashBuffer(): String { fun getLogcatCrashBuffer(): String {
try { return try {
val logcatProcess = Runtime.getRuntime().exec("logcat -v brief -b crash -d") val logcatProcess = Runtime.getRuntime().exec("logcat -v brief -b crash -d")
val logcatOutput = logcatProcess.inputStream.bufferedReader().readText() logcatProcess.inputStream.bufferedReader().readText()
} catch (e: Exception) {
return logcatOutput
}
catch (e: Exception) {
Log.e("Geode", "Failed to get logcat crash buffer", e) Log.e("Geode", "Failed to get logcat crash buffer", e)
return "" ""
} }
} }
@@ -111,116 +183,183 @@ object GeodeUtils {
activity.get()?.run { activity.get()?.run {
packageManager.getLaunchIntentForPackage(packageName)?.also { packageManager.getLaunchIntentForPackage(packageName)?.also {
val mainIntent = Intent.makeRestartActivityTask(it.component) val mainIntent = Intent.makeRestartActivityTask(it.component)
mainIntent.putExtra("restarted", true); mainIntent.putExtra("restarted", true)
startActivity(mainIntent) startActivity(mainIntent)
exitProcess(0) exitProcess(0)
} }
} }
} }
// TODO As of now this is unused
@JvmStatic @JvmStatic
fun openFolder(path: String): Boolean { fun openFolder(path: String): Boolean {
activity.get()?.run { val context = activity.get()!!
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
}
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) { val pathFile = File(path)
startActivity(intent) val baseDirectory = LaunchUtils.getBaseDirectory(context)
return true val isInternalPath = pathFile.startsWith(baseDirectory)
val intent = if (isInternalPath) {
// TODO: figure out how to get this to point to the path it should be pointing at
// (the best i got was pointing at a file)
// val relativePath = pathFile.relativeTo(baseDirectory)
Intent(Intent.ACTION_VIEW).apply {
data = DocumentsContract.buildRootUri(
"${context.packageName}.user", UserDirectoryProvider.ROOT
)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
)
} }
} } else {
return false Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addCategory(Intent.CATEGORY_OPENABLE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val documentFile = DocumentFile.fromFile(File(path))
putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.uri)
} }
external fun selectFileCallback(path: String) type = "*/*"
}
}
external fun selectFilesCallback(paths: Array<String>) return try {
context.startActivity(intent)
true
} catch (_: ActivityNotFoundException) {
false
}
}
external fun failedCallback() private external fun selectFileCallback(path: String)
private external fun selectFilesCallback(paths: Array<String>)
private external fun failedCallback()
private fun checkForFilePermissions(onSuccess: () -> Unit, onFailure: () -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Environment.isExternalStorageManager()) {
onSuccess()
} else {
val intent = Intent(
ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
)
internalRequestAllFilesLauncher.launch(intent)
afterRequestPermissions = onSuccess
afterRequestPermissionsFailure = onFailure
}
} else {
val permissions = listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
val context = activity.get()!!
val needsPermissions = permissions.filter {
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
}
if (needsPermissions.isNotEmpty()) {
internalRequestPermissionsLauncher.launch(needsPermissions.toTypedArray())
afterRequestPermissions = onSuccess
afterRequestPermissionsFailure = onFailure
} else {
onSuccess()
}
}
}
@JvmStatic @JvmStatic
fun selectFile(path: String): Boolean { fun selectFile(path: String): Boolean {
activity.get()?.run { var uri: Uri?
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also { DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri) uri = it.uri
} }
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) { return try {
print("Geode Selectfile") checkForFilePermissions(
fileResultLauncher.launch(intent) onSuccess = {
print("Geode launched ") openFileResultLauncher.launch(GeodeOpenFileActivityResult.OpenFileParams(arrayOf("*/*"), uri))
return true },
onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
} }
} }
return false
}
@JvmStatic @JvmStatic
fun selectFiles(path: String): Boolean { fun selectFiles(path: String): Boolean {
activity.get()?.run { var uri: Uri?
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also { DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri) uri = it.uri
} }
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) { return try {
filesResultLauncher.launch(intent) checkForFilePermissions(
return true onSuccess = {
openFilesResultLauncher.launch(GeodeOpenFilesActivityResult.OpenFileParams(arrayOf("*/*"), uri))
},
onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
} }
} }
return false
}
@JvmStatic @JvmStatic
fun selectFolder(path: String): Boolean { fun selectFolder(path: String): Boolean {
activity.get()?.run { var uri: Uri?
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also { DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri) uri = it.uri
} }
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) { return try {
fileResultLauncher.launch(intent) checkForFilePermissions(
return true onSuccess = {
openDirectoryResultLauncher.launch(uri)
},
onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
} }
}
return false
} }
@JvmStatic @JvmStatic
fun createFile(path: String): Boolean { fun createFile(path: String): Boolean {
activity.get()?.run { val initialPath = File(path)
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
}
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) { return try {
fileResultLauncher.launch(intent) checkForFilePermissions(
return true onSuccess = {
saveFileResultLauncher.launch(GeodeSaveFileActivityResult.SaveFileParams(null, initialPath))
},
onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
} }
} }
return false
}
@JvmStatic @JvmStatic
fun getBaseDirectory(): String { fun getBaseDirectory(): String {
@@ -235,169 +374,127 @@ object GeodeUtils {
} }
private val gameVersionMap = mapOf( private val gameVersionMap = mapOf(
37L to "2.200" 37L to "2.200",
38L to "2.205"
) )
@JvmStatic @JvmStatic
fun getGameVersion(): String { fun getGameVersion(): String {
// these versions should be aligned to windows releases, not what android says // these versions should be aligned to windows releases, not what android says
activity.get()?.run { activity.get()?.run {
val versionCode = LaunchUtils.getGeometryDashVersionCode(packageManager) return GamePackageUtils.getUnifiedVersionName(packageManager)
return gameVersionMap[versionCode] ?: LaunchUtils.getGeometryDashVersionString(packageManager)
} }
return "" return ""
} }
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
// i am actually very lazy to move this to a separate class
fun getRealPathFromURI(context: Context, uri: Uri): String? {
when {
// DocumentProvider
DocumentsContract.isDocumentUri(context, uri) -> {
when {
// ExternalStorageProvider
isExternalStorageDocument(uri) -> {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
// This is for checking Main Memory
return if ("primary".equals(type, ignoreCase = true)) {
if (split.size > 1) {
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} else {
Environment.getExternalStorageDirectory().toString() + "/"
}
// This is for checking SD Card
} else {
"storage" + "/" + docId.replace(":", "/")
}
}
isDownloadsDocument(uri) -> {
val fileName = getFilePath(context, uri)
if (fileName != null) {
return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
}
var id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
id = id.replaceFirst("raw:".toRegex(), "")
val file = File(id)
if (file.exists()) return id
}
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
}
isMediaDocument(uri) -> {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs)
}
// TODO: add the geode provider
}
}
"content".equals(uri.scheme, ignoreCase = true) -> {
// Return the remote address
if (isGooglePhotosUri(uri)) return uri.lastPathSegment
return getDataColumn(context, uri, null, null)
}
"file".equals(uri.scheme, ignoreCase = true) -> {
return uri.path
}
}
return null
}
fun getDataColumn(context: Context, uri: Uri?, selection: String?,
selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(
column
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs,
null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
fun getFilePath(context: Context, uri: Uri?): String? {
var cursor: Cursor? = null
val projection = arrayOf(
MediaStore.MediaColumns.DISPLAY_NAME
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, null, null,
null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
fun isGeodeUri(uri: Uri): Boolean { fun isGeodeUri(uri: Uri): Boolean {
return "com.geode.launcher.user" == uri.authority return "com.geode.launcher.user" == uri.authority
} }
private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal"
private const val MANAGE_ALL_FILES = "${INTERNAL_PERMISSION_PREFIX}.MANAGE_ALL_FILES"
@JvmStatic
fun getPermissionStatus(permission: String): Boolean {
val context = activity.get() ?: return false
return when (permission) {
MANAGE_ALL_FILES -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
val permissions = listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
return permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
}
else -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
@JvmStatic
fun requestPermission(permission: String) {
if (permission == MANAGE_ALL_FILES) {
// this function handles already having perms for us
checkForFilePermissions(onSuccess = {
permissionCallback(true)
}, onFailure = {
permissionCallback(false)
})
return
}
if (getPermissionStatus(permission)) {
permissionCallback(true)
return
}
try {
requestPermissionLauncher.launch(permission)
} catch (e: ActivityNotFoundException) {
permissionCallback(false)
}
}
private external fun permissionCallback(granted: Boolean)
const val ARGUMENT_SAFE_MODE = "--geode:safe-mode"
private var additionalLaunchArguments = arrayListOf<String>()
fun setAdditionalLaunchArguments(vararg args: String) {
additionalLaunchArguments.addAll(args)
}
fun clearLaunchArguments() = setAdditionalLaunchArguments()
@JvmStatic
fun getLaunchArguments(): String? {
activity.get()?.apply {
val preferences = PreferenceUtils.get(this)
val userArgs = preferences.getString(PreferenceUtils.Key.LAUNCH_ARGUMENTS)
val args = if (!userArgs.isNullOrEmpty()) {
listOf(userArgs) + additionalLaunchArguments
} else additionalLaunchArguments
return args.joinToString(" ")
}
return null
}
interface CapabilityListener {
fun onCapabilityAdded(capability: String): Boolean
}
const val CAPABILITY_EXTENDED_INPUT = "extended_input"
const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs"
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
fun setCapabilityListener(listener: CapabilityListener) {
capabilityListener = WeakReference(listener)
}
@JvmStatic
fun reportPlatformCapability(capability: String?): Boolean {
if (capability.isNullOrEmpty()) {
return false
}
return capabilityListener.get()?.onCapabilityAdded(capability) ?: false
}
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean)
external fun nativeActionScroll(scrollX: Float, scrollY: Float)
external fun resizeSurface(width: Int, height: Int)
// represents the timestamp of the next input callback, in nanoseconds (most events don't send it, but it's there)
external fun setNextInputTimestamp(timestamp: Long)
} }
@@ -1,91 +1,18 @@
package com.geode.launcher.utils package com.geode.launcher.utils
import android.annotation.SuppressLint
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Build import android.os.Build
import android.util.Log
import java.io.File import java.io.File
object LaunchUtils { object LaunchUtils {
enum class FailureReason {
NOT_FOUND, ABI_MISMATCH, UNKNOWN
}
fun isGeometryDashInstalled(packageManager: PackageManager): Boolean {
return try {
packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
true
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
fun getGeometryDashVersionCode(packageManager: PackageManager): Long {
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
game.longVersionCode
} else {
@Suppress("DEPRECATION")
game.versionCode.toLong()
}
}
fun getGeometryDashVersionString(packageManager: PackageManager): String {
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
return game.versionName
}
fun diagnoseLoadErrors(context: Context, packageInfo: PackageInfo): FailureReason {
val abi = applicationArchitecture
val isExtracted =
packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS
val isSplit = (packageInfo.applicationInfo.splitSourceDirs?.size ?: 0) > 1
val metadata =
"Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi"
Log.i("GeodeLauncher", metadata)
// determine if it can find gd libraries for the opposite architecture
val gdBinaryName = "lib${Constants.COCOS_LIB_NAME}.so"
val oppositeArchitecture = if (abi == "arm64-v8a") "armeabi-v7a" else "arm64-v8a"
if (packageInfo.applicationInfo.nativeLibraryDir.contains(oppositeArchitecture)) {
return FailureReason.ABI_MISMATCH
}
if (!isExtracted) {
try {
context.assets.openNonAssetFd("lib/$oppositeArchitecture/$gdBinaryName")
return FailureReason.ABI_MISMATCH
} catch (_: Exception) {
// this is good, actually!
}
}
// try fetching for its own libraries (only for uncompressed libraries)
if (!isExtracted) {
try {
context.assets.openNonAssetFd("lib/$abi/$gdBinaryName")
} catch (_: Exception) {
return FailureReason.NOT_FOUND
}
}
return FailureReason.UNKNOWN
}
// supposedly CPU_ABI returns the current arch for the running application // supposedly CPU_ABI returns the current arch for the running application
// despite being deprecated, this is also one of the few ways to get this information // despite being deprecated, this is also one of the few ways to get this information
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val applicationArchitecture: String = Build.CPU_ABI val applicationArchitecture: String = Build.CPU_ABI
val platformName: String = if (applicationArchitecture == "arm64-v8a") val is64bit = applicationArchitecture == "arm64-v8a"
"android64" else "android32"
val platformName: String = if (is64bit) "android64" else "android32"
val geodeFilename: String = "Geode.$platformName.so" val geodeFilename: String = "Geode.$platformName.so"
@@ -137,16 +64,47 @@ object LaunchUtils {
return File(getBaseDirectory(context), "save") return File(getBaseDirectory(context), "save")
} }
@SuppressLint("DiscouragedPrivateApi") private const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
fun addAssetsFromPackage(assetManager: AssetManager, packageInfo: PackageInfo) {
// this method is officially marked as deprecated but it is the only method we are allowed to reflect
// (the source recommends replacing with AssetManager.setApkAssets(ApkAssets[], boolean) lol)
val clazz = assetManager.javaClass
val aspMethod = clazz.getDeclaredMethod("addAssetPath", String::class.java)
aspMethod.invoke(assetManager, packageInfo.applicationInfo.sourceDir) private fun getCrashDirectory(context: Context): File {
packageInfo.applicationInfo.splitSourceDirs?.forEach { val base = getBaseDirectory(context)
aspMethod.invoke(assetManager, it) return File(base, "game/geode/crashlogs/")
}
fun getLastCrash(context: Context): File? {
val crashDirectory = getCrashDirectory(context)
if (!crashDirectory.exists()) {
return null
}
val children = crashDirectory.listFiles {
// ignore indicator files (including old file)
_, name -> name != CRASH_INDICATOR_NAME && name != "last-pid"
}
return children?.maxByOrNull { it.lastModified() }
}
fun lastSessionCrashed(context: Context): Boolean {
val base = getCrashDirectory(context)
val crashIndicatorFile = File(base, CRASH_INDICATOR_NAME)
return crashIndicatorFile.exists()
}
enum class LauncherError {
LINKER_NEEDS_64BIT,
LINKER_NEEDS_32BIT,
LINKER_FAILED,
GENERIC,
CRASHED;
fun isAbiFailure(): Boolean {
return this == LINKER_NEEDS_32BIT || this == LINKER_NEEDS_64BIT
} }
} }
const val LAUNCHER_KEY_RETURN_ERROR = "return_error"
const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message"
const val LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE = "return_extended_message"
} }
@@ -13,6 +13,7 @@ import androidx.core.content.edit
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.geode.launcher.BuildConfig
/** /**
* Extension object for SharedPreferences to add better key safety and default values. * Extension object for SharedPreferences to add better key safety and default values.
@@ -23,36 +24,69 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
@Composable @Composable
fun useBooleanPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> { fun useBooleanPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> {
return usePreference(preferenceKey, lifecycleOwner) { p, k -> return usePreference(
p.getBoolean(k) preferenceKey,
} lifecycleOwner,
preferenceGet = { p, k -> p.getBoolean(k) },
preferenceSet = { p, k, v -> p.setBoolean(k, v) }
)
} }
@Composable @Composable
fun useStringPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<String?> { fun useStringPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<String?> {
return usePreference(preferenceKey, lifecycleOwner) { p, k -> return usePreference(
p.getString(k) preferenceKey,
} lifecycleOwner,
preferenceGet = { p, k -> p.getString(k) },
preferenceSet = { p, k, v -> p.setString(k, v) }
)
} }
@Composable @Composable
fun useIntPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Int> { fun useIntPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Int> {
return usePreference(preferenceKey, lifecycleOwner) { p, k -> return usePreference(
p.getInt(k) preferenceKey,
} lifecycleOwner,
preferenceGet = { p, k -> p.getInt(k) },
preferenceSet = { p, k, v -> p.setInt(k, v) }
)
} }
@Composable @Composable
private fun <T> usePreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, preferenceGet: (PreferenceUtils, Key) -> T): MutableState<T> { fun useLongPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Long> {
return usePreference(
preferenceKey,
lifecycleOwner,
preferenceGet = { p, k -> p.getLong(k) },
preferenceSet = { p, k, v -> p.setLong(k, v) }
)
}
@Composable
private fun <T> usePreference(
preferenceKey: Key,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
preferenceGet: (PreferenceUtils, Key) -> T,
preferenceSet: (PreferenceUtils, Key, T) -> Unit
): MutableState<T> {
val context = LocalContext.current val context = LocalContext.current
val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE) val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE)
val preferences = get(sharedPreferences) val preferences = get(sharedPreferences)
val preferenceValue = remember { val preferenceValue = remember {
mutableStateOf( val state = mutableStateOf(preferenceGet(preferences, preferenceKey))
preferenceGet(preferences, preferenceKey) object : MutableState<T> by state {
) override var value: T
get() = state.value
set(value) {
// ignore setting the same thing (messes up default preferences)
if (state.value == value) { return }
state.value = value
preferenceSet(preferences, preferenceKey, value)
}
}
} }
val listener = SharedPreferences.OnSharedPreferenceChangeListener { val listener = SharedPreferences.OnSharedPreferenceChangeListener {
@@ -89,7 +123,6 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
} }
enum class Key { enum class Key {
LOAD_TESTING,
LOAD_AUTOMATICALLY, LOAD_AUTOMATICALLY,
UPDATE_AUTOMATICALLY, UPDATE_AUTOMATICALLY,
RELEASE_CHANNEL, RELEASE_CHANNEL,
@@ -98,18 +131,29 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
THEME, THEME,
BLACK_BACKGROUND, BLACK_BACKGROUND,
CURRENT_RELEASE_MODIFIED, CURRENT_RELEASE_MODIFIED,
LAST_DISMISSED_UPDATE,
DISMISSED_GJ_UPDATE,
LAUNCH_ARGUMENTS,
LIMIT_ASPECT_RATIO,
DISPLAY_MODE,
FORCE_HRR
} }
private fun defaultValueForBooleanKey(key: Key): Boolean { private fun defaultValueForBooleanKey(key: Key): Boolean {
return when (key) { return when (key) {
Key.UPDATE_AUTOMATICALLY -> true Key.UPDATE_AUTOMATICALLY, Key.FORCE_HRR -> true
Key.RELEASE_CHANNEL -> BuildConfig.DEBUG
else -> false else -> false
} }
} }
private fun defaultValueForIntKey(key: Key) = when (key) {
Key.DISPLAY_MODE -> if (this.getBoolean(Key.LIMIT_ASPECT_RATIO)) 1 else 0
else -> 0
}
private fun keyToName(key: Key): String { private fun keyToName(key: Key): String {
return when (key) { return when (key) {
Key.LOAD_TESTING -> "PreferenceLoadTesting"
Key.LOAD_AUTOMATICALLY -> "PreferenceLoadAutomatically" Key.LOAD_AUTOMATICALLY -> "PreferenceLoadAutomatically"
Key.UPDATE_AUTOMATICALLY -> "PreferenceUpdateAutomatically" Key.UPDATE_AUTOMATICALLY -> "PreferenceUpdateAutomatically"
Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel" Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel"
@@ -117,7 +161,13 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor" Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor"
Key.THEME -> "PreferenceTheme" Key.THEME -> "PreferenceTheme"
Key.BLACK_BACKGROUND -> "PreferenceBlackBackground" Key.BLACK_BACKGROUND -> "PreferenceBlackBackground"
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModified" Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash"
Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate"
Key.LAUNCH_ARGUMENTS -> "PreferenceLaunchArguments"
Key.LIMIT_ASPECT_RATIO -> "PreferenceLimitAspectRatio"
Key.DISPLAY_MODE -> "PreferenceDisplayMode"
Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
} }
} }
@@ -128,6 +178,13 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
return sharedPreferences.getBoolean(keyName, defaultValue) return sharedPreferences.getBoolean(keyName, defaultValue)
} }
fun setBoolean(key: Key, value: Boolean) {
val keyName = keyToName(key)
sharedPreferences.edit {
putBoolean(keyName, value)
}
}
fun toggleBoolean(key: Key): Boolean { fun toggleBoolean(key: Key): Boolean {
val currentValue = getBoolean(key) val currentValue = getBoolean(key)
val keyName = keyToName(key) val keyName = keyToName(key)
@@ -144,7 +201,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
return sharedPreferences.getString(keyName, null) return sharedPreferences.getString(keyName, null)
} }
fun setString(key: Key, value: String) { fun setString(key: Key, value: String?) {
val keyName = keyToName(key) val keyName = keyToName(key)
sharedPreferences.edit { sharedPreferences.edit {
putString(keyName, value) putString(keyName, value)
@@ -165,7 +222,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
fun getInt(key: Key): Int { fun getInt(key: Key): Int {
val keyName = keyToName(key) val keyName = keyToName(key)
return sharedPreferences.getInt(keyName, 0) return sharedPreferences.getInt(keyName, defaultValueForIntKey(key))
} }
fun setInt(key: Key, value: Int) { fun setInt(key: Key, value: Int) {
@@ -2,17 +2,22 @@ package org.cocos2dx.lib
import android.content.Context import android.content.Context
import android.opengl.GLSurfaceView import android.opengl.GLSurfaceView
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.Surface
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.annotation.RequiresApi
import com.customRobTop.BaseRobTopActivity import com.customRobTop.BaseRobTopActivity
import com.geode.launcher.utils.GeodeUtils
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
class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
companion object { companion object {
@@ -37,6 +42,13 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
private lateinit var cocos2dxRenderer: Cocos2dxRenderer private lateinit var cocos2dxRenderer: Cocos2dxRenderer
var useKeyboardEvents = false
set(value) {
field = value
cocos2dxRenderer.sendResizeEvents = value
}
var sendTimestampEvents = false
var cocos2dxEditText: Cocos2dxEditText? = null var cocos2dxEditText: Cocos2dxEditText? = null
set(value) { set(value) {
@@ -103,6 +115,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
} }
@RequiresApi(Build.VERSION_CODES.R)
fun updateRefreshRate() {
val maxRefreshRate = display.supportedModes.maxBy { it.refreshRate }
holder.surface.setFrameRate(
maxRefreshRate.refreshRate,
Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
)
}
override fun onPause() { override fun onPause() {
queueEvent { cocos2dxRenderer.handleOnPause() } queueEvent { cocos2dxRenderer.handleOnPause() }
super.onPause() super.onPause()
@@ -118,6 +139,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val ids = IntArray(pointerNumber) val ids = IntArray(pointerNumber)
val xs = FloatArray(pointerNumber) val xs = FloatArray(pointerNumber)
val ys = FloatArray(pointerNumber) val ys = FloatArray(pointerNumber)
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
motionEvent.eventTimeNanos else motionEvent.eventTime * MS_TO_NS
for (i in 0 until pointerNumber) { for (i in 0 until pointerNumber) {
ids[i] = motionEvent.getPointerId(i) ids[i] = motionEvent.getPointerId(i)
xs[i] = motionEvent.getX(i) xs[i] = motionEvent.getX(i)
@@ -129,11 +153,10 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val xDown = xs[0] val xDown = xs[0]
val f = ys[0] val f = ys[0]
queueEvent { queueEvent {
cocos2dxRenderer.handleActionDown( if (sendTimestampEvents)
idDown, GeodeUtils.setNextInputTimestamp(timestamp)
xDown,
f cocos2dxRenderer.handleActionDown(idDown, xDown, f)
)
} }
true true
} }
@@ -141,26 +164,29 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val idUp = motionEvent.getPointerId(0) val idUp = motionEvent.getPointerId(0)
val f2 = xs[0] val f2 = xs[0]
val f3 = ys[0] val f3 = ys[0]
queueEvent { cocos2dxRenderer.handleActionUp(idUp, f2, f3) } queueEvent {
if (sendTimestampEvents)
GeodeUtils.setNextInputTimestamp(timestamp)
cocos2dxRenderer.handleActionUp(idUp, f2, f3)
}
true true
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
queueEvent { queueEvent {
cocos2dxRenderer.handleActionMove( if (sendTimestampEvents)
ids, GeodeUtils.setNextInputTimestamp(timestamp)
xs,
ys cocos2dxRenderer.handleActionMove(ids, xs, ys)
)
} }
true true
} }
MotionEvent.ACTION_CANCEL -> { MotionEvent.ACTION_CANCEL -> {
queueEvent { queueEvent {
cocos2dxRenderer.handleActionCancel( if (sendTimestampEvents)
ids, GeodeUtils.setNextInputTimestamp(timestamp)
xs,
ys cocos2dxRenderer.handleActionCancel(ids, xs, ys)
)
} }
true true
} }
@@ -170,11 +196,10 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val xPointerDown = motionEvent.getX(indexPointerDown) val xPointerDown = motionEvent.getX(indexPointerDown)
val y = motionEvent.getY(indexPointerDown) val y = motionEvent.getY(indexPointerDown)
queueEvent { queueEvent {
cocos2dxRenderer.handleActionDown( if (sendTimestampEvents)
idPointerDown, GeodeUtils.setNextInputTimestamp(timestamp)
xPointerDown,
y cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y)
)
} }
true true
} }
@@ -184,11 +209,10 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val xPointerUp = motionEvent.getX(indexPointUp) val xPointerUp = motionEvent.getX(indexPointUp)
val y2 = motionEvent.getY(indexPointUp) val y2 = motionEvent.getY(indexPointUp)
queueEvent { queueEvent {
cocos2dxRenderer.handleActionUp( if (sendTimestampEvents)
idPointerUp, GeodeUtils.setNextInputTimestamp(timestamp)
xPointerUp,
y2 cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2)
)
} }
true true
} }
@@ -207,7 +231,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
} }
override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean { private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
return when (keyCode) { return when (keyCode) {
KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_MENU -> { KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_MENU -> {
if (keyEvent.repeatCount != 0 || BaseRobTopActivity.blockBackButton) { if (keyEvent.repeatCount != 0 || BaseRobTopActivity.blockBackButton) {
@@ -220,6 +244,82 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (!useKeyboardEvents) {
return legacyKeyDown(keyCode, event)
}
return when (keyCode) {
// ignore system keys
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> {
super.onKeyDown(keyCode, event)
}
else -> {
if (BaseRobTopActivity.blockBackButton) {
return true
}
queueEvent {
if (sendTimestampEvents)
GeodeUtils.setNextInputTimestamp(event.eventTime * MS_TO_NS)
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
}
true
}
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
if (!useKeyboardEvents) {
return super.onKeyUp(keyCode, event)
}
return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> {
super.onKeyUp(keyCode, event)
}
else -> {
if (event.repeatCount != 0 || BaseRobTopActivity.blockBackButton) {
return true
}
queueEvent {
if (sendTimestampEvents)
GeodeUtils.setNextInputTimestamp(event.eventTime * MS_TO_NS)
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
}
true
}
}
}
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
if (!useKeyboardEvents) {
return super.onGenericMotionEvent(event)
}
if (event?.action == MotionEvent.ACTION_SCROLL) {
val scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL)
val scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
event.eventTimeNanos else event.eventTime * MS_TO_NS
queueEvent {
if (sendTimestampEvents)
GeodeUtils.setNextInputTimestamp(timestamp)
GeodeUtils.nativeActionScroll(scrollX, scrollY)
}
return true
}
return super.onGenericMotionEvent(event)
}
fun setCocos2dxRenderer(renderer: Cocos2dxRenderer) { fun setCocos2dxRenderer(renderer: Cocos2dxRenderer) {
this.cocos2dxRenderer = renderer this.cocos2dxRenderer = renderer
setRenderer(this.cocos2dxRenderer) setRenderer(this.cocos2dxRenderer)
@@ -1,11 +1,12 @@
package org.cocos2dx.lib package org.cocos2dx.lib
import android.opengl.GLSurfaceView import android.opengl.GLSurfaceView
import android.os.Build
import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10 import javax.microedition.khronos.opengles.GL10
@Suppress("unused", "KotlinJniMissingFunction") @Suppress("unused", "KotlinJniMissingFunction")
class Cocos2dxRenderer : GLSurfaceView.Renderer { class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceView.Renderer {
companion object { companion object {
@JvmStatic @JvmStatic
fun setAnimationInterval(@Suppress("UNUSED_PARAMETER") animationInterval: Double) { fun setAnimationInterval(@Suppress("UNUSED_PARAMETER") animationInterval: Double) {
@@ -59,6 +60,8 @@ class Cocos2dxRenderer : GLSurfaceView.Renderer {
private var lastTickInNanoSeconds: Long = 0 private var lastTickInNanoSeconds: Long = 0
private var screenWidth = 0 private var screenWidth = 0
private var screenHeight = 0 private var screenHeight = 0
var sendResizeEvents = false
var setFrameRate = false
fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) { fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) {
screenWidth = surfaceWidth screenWidth = surfaceWidth
@@ -70,7 +73,11 @@ class Cocos2dxRenderer : GLSurfaceView.Renderer {
lastTickInNanoSeconds = System.nanoTime() lastTickInNanoSeconds = System.nanoTime()
} }
override fun onSurfaceChanged(gl10: GL10?, width: Int, height: Int) {} override fun onSurfaceChanged(gl10: GL10?, width: Int, height: Int) {
if (setFrameRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
handler.updateRefreshRate()
}
}
override fun onDrawFrame(gl: GL10?) { override fun onDrawFrame(gl: GL10?) {
nativeRender() nativeRender()
+10 -4
View File
@@ -11,18 +11,24 @@ import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
class MediaCodec { class MediaCodec {
private var channelCount = 0 // these variables require getX functions for ndk access
var channelCount = 0
private set
var mCodecPtr = 0L var length = 0L
private set
var sampleRate = 0
private set
private var mCodecPtr = 0L
private var mCurrentOutputBufferIndex = -1 private var mCurrentOutputBufferIndex = -1
private var mDecoder: MediaCodec? = null private var mDecoder: MediaCodec? = null
private var mExtractor: MediaExtractor? = null private var mExtractor: MediaExtractor? = null
private var mInputBuffers: Array<ByteBuffer>? = null private var mInputBuffers: Array<ByteBuffer>? = null
private var mInputFinished = false private var mInputFinished = false
private var length = 0L
private var mOutputBuffers: Array<ByteBuffer>? = null private var mOutputBuffers: Array<ByteBuffer>? = null
private var mOutputFinished = false private var mOutputFinished = false
private var sampleRate = 0
fun init(codecPtr: Long): Boolean { fun init(codecPtr: Long): Boolean {
mCodecPtr = codecPtr mCodecPtr = codecPtr
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,760Q546,760 593,713Q640,666 640,600L640,440Q640,374 593,327Q546,280 480,280Q414,280 367,327Q320,374 320,440L320,600Q320,666 367,713Q414,760 480,760ZM400,640L560,640L560,560L400,560L400,640ZM400,480L560,480L560,400L400,400L400,480ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520ZM480,840Q415,840 359.5,808Q304,776 272,720L160,720L160,640L244,640Q241,620 240.5,600Q240,580 240,560L160,560L160,480L240,480Q240,460 240.5,440Q241,420 244,400L160,400L160,320L272,320Q286,297 303.5,277Q321,257 344,242L280,176L336,120L422,206Q450,197 479,197Q508,197 536,206L624,120L680,176L614,242Q637,257 655.5,276.5Q674,296 688,320L800,320L800,400L716,400Q719,420 719.5,440Q720,460 720,480L800,480L800,560L720,560Q720,580 719.5,600Q719,620 716,640L800,640L800,720L688,720Q656,776 600.5,808Q545,840 480,840Z"/>
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M360,720Q327,720 303.5,696.5Q280,673 280,640L280,160Q280,127 303.5,103.5Q327,80 360,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,640Q800,673 776.5,696.5Q753,720 720,720L360,720ZM360,640L720,640Q720,640 720,640Q720,640 720,640L720,160Q720,160 720,160Q720,160 720,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640ZM200,880Q167,880 143.5,856.5Q120,833 120,800L120,240L200,240L200,800Q200,800 200,800Q200,800 200,800L640,800L640,880L200,880ZM360,640Q360,640 360,640Q360,640 360,640L360,160Q360,160 360,160Q360,160 360,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640L360,640Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M320,720L640,720L640,640L320,640L320,720ZM320,560L640,560L640,480L320,480L320,560ZM240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L560,80L800,320L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM520,360L520,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800L720,800Q720,800 720,800Q720,800 720,800L720,360L520,360ZM240,160L240,160L240,360L240,360L240,160L240,360L240,360L240,800Q240,800 240,800Q240,800 240,800L240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M400,720L400,640L560,640L560,720L400,720ZM240,520L240,440L720,440L720,520L240,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M840,280L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L680,120L840,280ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM240,400L600,400L600,240L240,240L240,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/>
</vector>
+65 -8
View File
@@ -11,8 +11,13 @@
<string name="launcher_logo_alt">geode logo</string> <string name="launcher_logo_alt">geode logo</string>
<string name="launcher_launch_icon_alt">play icon</string> <string name="launcher_launch_icon_alt">play icon</string>
<string name="launcher_download_icon_alt">download icon</string> <string name="launcher_download_icon_alt">download icon</string>
<string name="launcher_download_game">Download Game</string>
<string name="launcher_download_32bit">Download the 32-bit version of launcher.</string>
<string name="launcher_download_universal">Download the non 32-bit version of launcher.</string>
<string name="launcher_settings_icon_alt">settings icon</string> <string name="launcher_settings_icon_alt">settings icon</string>
<string name="launcher_retry_icon_alt">retry icon</string>
<string name="geode_download_title">Geode is not installed.</string> <string name="geode_download_title">Geode is not installed.</string>
<string name="game_outdated">A Geometry Dash update is required.\nYou currently have update %1$s.</string>
<string name="geode_download_description">Geode is downloading. Please wait.</string> <string name="geode_download_description">Geode is downloading. Please wait.</string>
<plurals name="automatically_load_countdown"> <plurals name="automatically_load_countdown">
<item quantity="one">Automatically launching game in %1$d second.</item> <item quantity="one">Automatically launching game in %1$d second.</item>
@@ -21,9 +26,26 @@
<string name="launcher_warning_icon_alt">warning</string> <string name="launcher_warning_icon_alt">warning</string>
<string name="launcher_error_icon_alt">error</string> <string name="launcher_error_icon_alt">error</string>
<string name="launcher_unsupported_version_title">Unsupported version</string> <string name="launcher_unsupported_version_title">Unsupported version</string>
<string name="launcher_unsupported_version_description"> <string name="launcher_outdated_version_description">
The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s).\nLaunch will continue, but crashes may occur! The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s). You must update Geometry Dash to fix this.\nLaunch will continue, but crashes may occur!
</string> </string>
<string name="launcher_unsupported_version_description">
The current version of Geometry Dash is not compatible with the version supported by the launcher. A launcher update is required to fix this.\nLaunch will continue, but crashes may occur!
</string>
<string name="launcher_update_available">A new launcher update is available!</string>
<string name="launcher_update_dismiss">Dismiss</string>
<string name="safe_mode_enable_title">Enable safe mode?</string>
<string name="safe_mode_enable_description">Safe mode disables loading of all mods, allowing one to remove problematic mods or diagnose crashes. If you\'re not experiencing issues with Geode, you should probably not be using safe mode.</string>
<string name="launcher_failed_to_load">Geode failed to load.</string>
<string name="launcher_crashed">Geode crashed.</string>
<string name="launcher_error_more">Help</string>
<string name="launcher_error_details">Error details</string>
<string name="launcher_error_view_logs">View logs</string>
<string name="launcher_error_copy_error_alt">Copy</string>
<string name="launcher_error_copy">Copy details</string>
<string name="launcher_error_export_crash">Share crash</string>
<string name="launcher_error_export_missing">No crash dump was found!</string>
<string name="launcher_error_device_info">Android %2$s, %1$s</string>
<!-- Updater strings --> <!-- Updater strings -->
<string name="release_fetch_failed">Update failed.\n%1$s</string> <string name="release_fetch_failed">Update failed.\n%1$s</string>
@@ -36,15 +58,30 @@
<string name="release_fetch_cancelled">Update cancelled.</string> <string name="release_fetch_cancelled">Update cancelled.</string>
<string name="release_fetch_button_retry">Retry</string> <string name="release_fetch_button_retry">Retry</string>
<string name="release_fetch_button_cancel">Cancel</string> <string name="release_fetch_button_cancel">Cancel</string>
<string name="release_fetch_try_later">Please try again later.</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_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string>
<!-- GeometryDashActivity strings --> <!-- GeometryDashActivity strings -->
<string name="load_failed_link_error">Failed to link libraries</string> <string name="load_failed_link_error">Failed to link libraries</string>
<string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture of the launcher and Geometry Dash do not match, or from a system bug.\nReported issue: %1$s.\nCheck application logs for more details.</string> <string name="load_failed_link_error_description">Game libraries could not be found. Try to:</string>
<string name="load_failed_generic_error">Load failed</string> <string name="load_failed_abi_error">The architecture of the launcher and Geometry Dash do not match. Try to:</string>
<string name="load_failed_generic_error_description">Uncaught error during game load: `%1$s`.\nCheck application logs for more details. Please report this issue!</string> <string name="load_failed_generic_error">Failed to load Geode</string>
<string name="load_failed_generic_error_description">Uncaught error during game load. Try to:</string>
<string name="load_failed_crashed">Geode crashed</string>
<string name="load_failed_crashed_description">A crash was reported during the last session. Try to:</string>
<string name="load_failed_recommendation_reinstall">Reinstall Geometry Dash.</string>
<string name="load_failed_recommendation_switch_32bit_abi">Install the 32-bit version of the launcher.</string>
<string name="load_failed_recommendation_switch_64bit_abi">Install the non 32-bit version of the launcher.</string>
<string name="load_failed_recommendation_update">Verify that the latest versions of the launcher and Geometry Dash are installed.</string>
<string name="load_failed_recommendation_report">Report this issue to the developers.</string>
<string name="load_failed_recommendation_safe_mode">Hold the Launch button to launch in safe mode.</string>
<string name="message_box_cancel">Cancel</string> <string name="message_box_cancel">Cancel</string>
<string name="message_box_accept">OK</string> <string name="message_box_accept">OK</string>
<string name="message_box_continue">Continue</string>
<string name="missing_permissions">Missing required permissions for this action!</string>
<string name="no_file_selected">File selection cancelled.</string>
<string name="file_select_error">Failed to get file.</string>
<!-- SettingsActivity strings --> <!-- SettingsActivity strings -->
<string name="title_activity_settings">Settings</string> <string name="title_activity_settings">Settings</string>
@@ -54,7 +91,6 @@
<string name="text_copied">Copied to clipboard.</string> <string name="text_copied">Copied to clipboard.</string>
<string name="preference_category_testing">General</string> <string name="preference_category_testing">General</string>
<string name="preference_load_testing_name">Load files from /test</string>
<string name="preference_load_automatically_name">Automatically launch</string> <string name="preference_load_automatically_name">Automatically launch</string>
<string name="preference_load_automatically_description">Launches the game after a short delay.</string> <string name="preference_load_automatically_description">Launches the game after a short delay.</string>
<string name="preferences_copy_external_button">Copy external files directory</string> <string name="preferences_copy_external_button">Copy external files directory</string>
@@ -67,13 +103,21 @@
<string name="preference_theme_light">Light</string> <string name="preference_theme_light">Light</string>
<string name="preference_theme_dark">Dark</string> <string name="preference_theme_dark">Dark</string>
<string name="preference_black_background_name">Black background in dark theme</string> <string name="preference_black_background_name">Black background in dark theme</string>
<string name="preference_force_hrr">Force maximum refresh rate</string>
<string name="preference_display_mode_name">Screen scale</string>
<string name="preference_display_mode_select">Select screen scale</string>
<string name="preference_display_mode_legacy">Limited (16:9)</string>
<string name="preference_display_mode_fullscreen">Edge-to-edge</string>
<string name="preference_display_mode_default">Default</string>
<string name="preference_category_updater">Updates</string> <string name="preference_category_updater">Updates</string>
<string name="preference_update_automatically_name">Enable automatic updates</string> <string name="preference_update_automatically_name">Enable automatic updates</string>
<string name="preference_release_channel_name">Use nightly release channel</string> <string name="preference_release_channel_name">Use nightly release channel</string>
<string name="preference_check_for_updates_button">Check for updates</string> <string name="preference_check_for_updates_button">Check for updates</string>
<string name="preference_check_for_updates_description">Current version: %1$s</string> <string name="preference_check_for_updates_description">Current loader version: %1$s</string>
<string name="preference_launcher_version">Geode Launcher v%1$s</string>
<string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string> <string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string>
<string name="preference_check_for_updates_none_found">No updates found.</string> <string name="preference_check_for_updates_none_found">No updates found.</string>
<string name="preference_check_for_updates_success">Geode has been updated!</string> <string name="preference_check_for_updates_success">Geode has been updated!</string>
@@ -81,10 +125,23 @@
<string name="preference_check_for_updates_no_internet">No internet found.</string> <string name="preference_check_for_updates_no_internet">No internet found.</string>
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string> <string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
<string name="preference_category_developer">Developer</string>
<string name="preference_launch_arguments_name">Launch arguments</string>
<string name="preference_launch_arguments_set_title">Set launch arguments</string>
<string name="preference_text_clear">Clear</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>
<string name="application_logs_delete">Clear</string> <string name="application_logs_delete">Clear All</string>
<string name="application_logs_share">Share</string> <string name="application_logs_share">Share</string>
<string name="application_log_copy_label">Log Message</string> <string name="application_log_copy_label">Log Message</string>
<string name="application_logs_more">More</string>
<string name="application_logs_export">Save To</string>
<string name="application_logs_level">Set Log Level</string>
<string name="application_logs_level_dialog_name">Set minimum log level</string>
<string name="application_logs_export_completed">Log export complete.</string>
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
<string name="application_logs_crash_only">Show Crash Buffer</string>
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
</resources> </resources>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.NoActionBar.Fullscreen" parent="@style/Theme.AppCompat.Light.NoActionBar"> <style name="Theme.NoActionBar.Fullscreen" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item> <item name="android:windowFullscreen">true</item>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<game-mode-config
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:targetApi="33"
android:supportsBatteryGameMode="false"
android:supportsPerformanceGameMode="false"
android:allowGameFpsOverride="false" />
-16
View File
@@ -1,16 +0,0 @@
buildscript {
ext {
compose_version = '1.5.4'
compose_compiler = '1.5.7'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.2.1' apply false
id 'com.android.library' version '8.2.1' apply false
id 'org.jetbrains.kotlin.android' version '1.9.21' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21' apply false
}
tasks.register('clean', Delete) {
delete rootProject.buildDir
}
+16
View File
@@ -0,0 +1,16 @@
buildscript {
val composeBOM by extra("2024.05.00")
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.4.1" apply false
id("com.android.library") version "8.4.1" apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -21,3 +21,5 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
# nice optimization
org.gradle.configuration-cache=true
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+7 -7
View File
@@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -202,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
Vendored
+10 -10
View File
@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
+4 -1
View File
@@ -5,6 +5,8 @@ pluginManagement {
mavenCentral() mavenCentral()
} }
} }
@Suppress("UnstableApiUsage")
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
@@ -12,5 +14,6 @@ dependencyResolutionManagement {
mavenCentral() mavenCentral()
} }
} }
rootProject.name = "launcher" rootProject.name = "launcher"
include ':app' include(":app")