├── .github
└── workflows
│ ├── build.yml
│ └── update-site.yml
├── .gitignore
├── LICENSE.txt
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── cpp
│ ├── CMakeLists.txt
│ └── launcher-fix.cpp
│ ├── ic_launcher-playstore.png
│ ├── java
│ ├── com
│ │ ├── customRobTop
│ │ │ ├── BaseRobTopActivity.kt
│ │ │ └── JniToCpp.kt
│ │ └── geode
│ │ │ └── launcher
│ │ │ ├── AltMainActivity.kt
│ │ │ ├── GeometryDashActivity.kt
│ │ │ ├── LauncherFix.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── UserDirectoryProvider.kt
│ │ │ ├── activityresult
│ │ │ ├── GeodeOpenFileActivityResult.kt
│ │ │ ├── GeodeOpenFilesActivityResult.kt
│ │ │ └── GeodeSaveFileActivityResult.kt
│ │ │ ├── log
│ │ │ ├── LogLine.kt
│ │ │ └── LogViewModel.kt
│ │ │ ├── main
│ │ │ ├── Components.kt
│ │ │ ├── ErrorComponents.kt
│ │ │ ├── LaunchComponents.kt
│ │ │ ├── LaunchNotification.kt
│ │ │ ├── LaunchViewModel.kt
│ │ │ └── UpdateComponents.kt
│ │ │ ├── preferences
│ │ │ ├── ApplicationLogsActivity.kt
│ │ │ ├── DeveloperSettingsActivity.kt
│ │ │ ├── SettingsActivity.kt
│ │ │ └── SettingsComponents.kt
│ │ │ ├── ui
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ ├── updater
│ │ │ ├── Release.kt
│ │ │ ├── ReleaseManager.kt
│ │ │ ├── ReleaseRepository.kt
│ │ │ └── ReleaseViewModel.kt
│ │ │ └── utils
│ │ │ ├── Components.kt
│ │ │ ├── Constants.kt
│ │ │ ├── ConstrainedFrameLayout.kt
│ │ │ ├── DownloadUtils.kt
│ │ │ ├── FileUtils.kt
│ │ │ ├── GamePackageUtils.kt
│ │ │ ├── GeodeUtils.kt
│ │ │ ├── LaunchUtils.kt
│ │ │ ├── PreferenceUtils.kt
│ │ │ ├── ProfileManager.kt
│ │ │ └── useCountdownTimer.kt
│ └── org
│ │ ├── cocos2dx
│ │ └── lib
│ │ │ ├── Cocos2dxAccelerometer.kt
│ │ │ ├── Cocos2dxActivity.kt
│ │ │ ├── Cocos2dxBitmap.kt
│ │ │ ├── Cocos2dxEditText.kt
│ │ │ ├── Cocos2dxGLSurfaceView.kt
│ │ │ ├── Cocos2dxHelper.kt
│ │ │ ├── Cocos2dxRenderer.kt
│ │ │ ├── Cocos2dxTextInputWrapper.kt
│ │ │ └── Cocos2dxTypefaces.kt
│ │ └── fmod
│ │ ├── AudioDevice.kt
│ │ ├── FMOD.kt
│ │ └── MediaCodec.kt
│ ├── jniLibs
│ └── .keep
│ └── res
│ ├── drawable-v24
│ ├── ic_launcher_foreground.xml
│ └── ic_launcher_monochrome.xml
│ ├── drawable
│ ├── geode_logo.xml
│ ├── geode_monochrome.xml
│ ├── google_play_badge.png
│ ├── icon_bug_report.xml
│ ├── icon_content_copy.xml
│ ├── icon_data_object.xml
│ ├── icon_delete.xml
│ ├── icon_description.xml
│ ├── icon_download.xml
│ ├── icon_error.xml
│ ├── icon_filter_list.xml
│ ├── icon_link.xml
│ ├── icon_person.xml
│ ├── icon_person_add.xml
│ ├── icon_question_mark.xml
│ ├── icon_remove.xml
│ ├── icon_resume.xml
│ ├── icon_save.xml
│ └── icon_undo.xml
│ ├── mipmap-anydpi-v26
│ └── ic_launcher.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_foreground.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_foreground.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_foreground.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_foreground.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_foreground.png
│ ├── values-v31
│ └── themes.xml
│ ├── values
│ ├── colors.xml
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ ├── data_extraction_rules.xml
│ ├── game_mode_config.xml
│ └── provider_paths.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build launcher
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: '*'
7 |
8 | jobs:
9 | build:
10 | strategy:
11 | matrix:
12 | config:
13 | - name: Release
14 | lower: release
15 |
16 | - name: Debug
17 | lower: debug
18 |
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 |
24 | - name: Setup Java JDK
25 | uses: actions/setup-java@v4
26 | with:
27 | java-version: 21
28 | distribution: temurin
29 | cache: gradle
30 |
31 | - name: Generate keystore file
32 | run: echo ${{secrets.KEYSTORE_FILE}} | base64 -d > ~/release.keystore
33 |
34 | - name: Fix gradle permissions
35 | run: chmod +x ./gradlew
36 |
37 | - name: Build project
38 | run: |
39 | ./gradlew app:assemble${{ matrix.config.name }} \
40 | -Pandroid.injected.signing.store.file="$HOME/release.keystore" \
41 | -Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \
42 | -Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \
43 | -Pandroid.injected.signing.key.password="${{secrets.KEY_PASSWORD}}"
44 |
45 | - name: Rename universal apk
46 | id: rename_apk_universal
47 | run: |
48 | SHORT_SHA=${GITHUB_SHA::7}
49 | APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-universal-${{ matrix.config.lower }}.apk
50 | OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.apk
51 | mv $APK_PATH $OUT_PATH
52 | echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
53 |
54 | - name: Rename apk
55 | id: rename_apk
56 | run: |
57 | SHORT_SHA=${GITHUB_SHA::7}
58 | APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-armeabi-v7a-${{ matrix.config.lower }}.apk
59 | OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-android32-$SHORT_SHA.apk
60 | mv $APK_PATH $OUT_PATH
61 | echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
62 |
63 | - name: Upload development build
64 | uses: actions/upload-artifact@v4
65 | with:
66 | name: geode-launcher-${{ matrix.config.lower }}
67 | path: |
68 | ${{ steps.rename_apk_universal.outputs.path }}
69 | ${{ steps.rename_apk.outputs.path }}
70 |
--------------------------------------------------------------------------------
/.github/workflows/update-site.yml:
--------------------------------------------------------------------------------
1 | name: Update Website
2 |
3 | on:
4 | # release:
5 | # types:
6 | # - released
7 |
8 | workflow_dispatch:
9 |
10 | jobs:
11 | trigger:
12 | name: Trigger site build
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/github-script@v7
16 | with:
17 | github-token: ${{ secrets.GEODE_BOT_PUSH_BIN_TOKEN }}
18 | script: |
19 | await github.rest.actions.createWorkflowDispatch({
20 | owner: 'geode-sdk',
21 | repo: 'website',
22 | workflow_id: 'build.yml',
23 | ref: 'main'
24 | })
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/*
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | app/release/
12 | .vscode/
13 | .kotlin/
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Boost Software License - Version 1.0 - August 17th, 2003
2 |
3 | Permission is hereby granted, free of charge, to any person or organization
4 | obtaining a copy of the software and accompanying documentation covered by
5 | this license (the "Software") to use, reproduce, display, distribute,
6 | execute, and transmit the Software, and to prepare derivative works of the
7 | Software, and to permit third-parties to whom the Software is furnished to
8 | do so, all subject to the following:
9 |
10 | The copyright notices in the Software and this entire statement, including
11 | the above license grant, this restriction and the following disclaimer,
12 | must be included in all copies of the Software, in whole or in part, and
13 | all derivative works of the Software, unless such copies or derivative
14 | works are solely in the form of machine-executable object code generated by
15 | a source language processor.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 | DEALINGS IN THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Geode Android Launcher
2 |
3 | Launches a vanilla copy of Geometry Dash with the Geode loader added.
4 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | val composeBOM: String by rootProject.extra
2 |
3 | kotlin {
4 | compilerOptions {
5 | extraWarnings.set(true)
6 | }
7 | }
8 |
9 | plugins {
10 | id("com.android.application")
11 | id("org.jetbrains.kotlin.android")
12 | id("org.jetbrains.kotlin.plugin.serialization")
13 | id("org.jetbrains.kotlin.plugin.compose")
14 | }
15 |
16 | android {
17 | compileSdk = 36
18 |
19 | defaultConfig {
20 | applicationId = "com.geode.launcher"
21 | minSdk = 23
22 | targetSdk = 35
23 | versionCode = 22
24 | versionName = "1.6.1"
25 |
26 | vectorDrawables {
27 | useSupportLibrary = true
28 | }
29 |
30 | @Suppress("UnstableApiUsage")
31 | externalNativeBuild {
32 | cmake {
33 | arguments("-DUSE_TULIPHOOK:BOOL=OFF", "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES:BOOL=OFF")
34 | }
35 | }
36 |
37 | //noinspection ChromeOsAbiSupport (not my fault)
38 | ndk.abiFilters += listOf("arm64-v8a", "armeabi-v7a")
39 | }
40 |
41 | splits {
42 | abi {
43 | isEnable = true
44 | reset()
45 |
46 | //noinspection ChromeOsAbiSupport. i'm sorry!
47 | include("arm64-v8a", "armeabi-v7a")
48 |
49 | isUniversalApk = true
50 | }
51 | }
52 |
53 | buildTypes {
54 | release {
55 | isMinifyEnabled = true
56 | isShrinkResources = true
57 |
58 | proguardFiles(
59 | getDefaultProguardFile("proguard-android-optimize.txt"),
60 | "proguard-rules.pro"
61 | )
62 | }
63 | }
64 | compileOptions {
65 | // enables a polyfill for java Instant on api levels < 26 (used for updater)
66 | isCoreLibraryDesugaringEnabled = true
67 |
68 | sourceCompatibility = JavaVersion.VERSION_1_8
69 | targetCompatibility = JavaVersion.VERSION_1_8
70 | }
71 | kotlinOptions {
72 | jvmTarget = "1.8"
73 | }
74 | buildFeatures {
75 | compose = true
76 | buildConfig = true
77 | }
78 | packaging {
79 | resources {
80 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
81 | }
82 | }
83 | externalNativeBuild {
84 | cmake {
85 | version = "3.25.0+"
86 | path = file("src/main/cpp/CMakeLists.txt")
87 | }
88 | }
89 | namespace = "com.geode.launcher"
90 | ndkVersion = "28.1.13356709"
91 | }
92 |
93 | dependencies {
94 | implementation (platform("androidx.compose:compose-bom:$composeBOM"))
95 | implementation ("androidx.core:core-ktx:1.16.0")
96 | implementation ("androidx.compose.ui:ui")
97 | implementation ("androidx.compose.material3:material3")
98 | implementation ("androidx.compose.ui:ui-tooling-preview")
99 | implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
100 | implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
101 | implementation ("androidx.activity:activity-compose:1.10.1")
102 | implementation ("androidx.activity:activity-ktx:1.10.1")
103 | implementation ("androidx.appcompat:appcompat:1.7.0")
104 | implementation ("androidx.documentfile:documentfile:1.0.1")
105 | implementation ("com.squareup.okio:okio:3.11.0")
106 | implementation ("com.squareup.okhttp3:okhttp:4.12.0")
107 | implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
108 | implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.8.1")
109 | implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
110 | implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.33.0")
111 | implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.33.0")
112 | implementation ("androidx.browser:browser:1.8.0")
113 | debugImplementation ("androidx.compose.ui:ui-tooling")
114 | coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
115 | }
116 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add wildcard keep rules to all of the library code in use
2 | -keep class org.fmod.** { *; }
3 | -keep class org.cocos2dx.lib.** { *; }
4 | -keep class com.customRobTop.** { *; }
5 |
6 | # note: if you're going to add more rules, consider the @Keep annotation
7 | # this should really only be kept to non-custom code
8 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
32 |
33 |
45 |
49 |
54 |
59 |
64 |
67 |
68 |
76 |
79 |
80 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
97 |
98 |
99 |
100 |
101 |
106 |
109 |
110 |
111 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/app/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.18.1)
2 |
3 | project(launcherfix)
4 |
5 | add_library(launcherfix SHARED launcher-fix.cpp)
6 |
7 | target_compile_features(launcherfix PUBLIC cxx_std_20)
8 | set_target_properties(launcherfix PROPERTIES CXX_EXTENSIONS OFF)
9 |
10 | include(FetchContent)
11 |
12 | option(USE_TULIPHOOK "enables choice of hooking library" OFF)
13 | if (USE_TULIPHOOK)
14 | FetchContent_Declare(TulipHook
15 | GIT_REPOSITORY https://github.com/geode-sdk/TulipHook.git
16 | GIT_TAG 9aa1f95091ebac18c657d57b755f59d00fe6f1f8
17 | )
18 |
19 | set(TULIP_LINK_SOURCE ON CACHE INTERNAL "")
20 | FetchContent_MakeAvailable(TulipHook)
21 |
22 | target_compile_definitions(launcherfix PRIVATE -DUSE_TULIPHOOK=1)
23 |
24 | target_link_libraries(launcherfix PRIVATE TulipHook TulipHookInclude)
25 | else()
26 | FetchContent_Declare(dobby
27 | # this is the last version that builds on android at all
28 | GIT_REPOSITORY https://github.com/jmpews/Dobby
29 | GIT_TAG 0932d69c320e786672361ab53825ba8f4245e9d3
30 | )
31 |
32 | FetchContent_GetProperties(dobby)
33 | if(NOT dobby_POPULATED)
34 | FetchContent_Populate(dobby)
35 |
36 | # fixes Dobby build
37 | # add_compile_definitions(typeof=__typeof__)
38 | set(DOBBY_DEBUG OFF CACHE INTERNAL "" FORCE)
39 | set(DOBBY_GENERATE_SHARED OFF CACHE INTERNAL "" FORCE)
40 |
41 | add_subdirectory("${dobby_SOURCE_DIR}" ${dobby_BINARY_DIR} EXCLUDE_FROM_ALL)
42 | target_include_directories(launcherfix PRIVATE ${dobby_SOURCE_DIR}/include)
43 | target_link_libraries(launcherfix PRIVATE dobby)
44 | endif()
45 | endif()
46 |
47 | target_link_libraries(launcherfix PRIVATE log)
48 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/customRobTop/BaseRobTopActivity.kt:
--------------------------------------------------------------------------------
1 | package com.customRobTop
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.content.*
6 | import android.net.ConnectivityManager
7 | import android.net.NetworkCapabilities
8 | import android.net.Uri
9 | import android.os.Build
10 | import android.provider.Settings
11 | import android.util.Log
12 | import android.widget.Toast
13 | import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard
14 | import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard
15 | import java.lang.ref.WeakReference
16 | import java.util.*
17 |
18 |
19 | @Suppress("unused", "UNUSED_PARAMETER")
20 | object BaseRobTopActivity {
21 | private var isLoaded = false
22 | var blockBackButton = false
23 | private var keyboardActive = false
24 |
25 | var isPaused = false
26 |
27 | lateinit var me: WeakReference
28 | private var shouldResumeSound = true
29 |
30 | fun setCurrentActivity(currentActivity: Activity) {
31 | me = WeakReference(currentActivity)
32 | }
33 |
34 | @SuppressLint("HardwareIds")
35 | @JvmStatic
36 | fun getUserID(): String {
37 | // this is how RobTop does it in 2.2, based on the meltdown leaks
38 | val androidId = Settings.Secure.getString(me.get()?.contentResolver, Settings.Secure.ANDROID_ID)
39 | return if ("9774d56d682e549c" != androidId) {
40 | UUID.nameUUIDFromBytes(androidId.toByteArray()).toString()
41 | } else return UUID.randomUUID().toString()
42 | }
43 |
44 | @JvmStatic
45 | fun isNetworkAvailable(): Boolean {
46 | val connectivityManager = me.get()?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
47 |
48 | val activeNetwork = connectivityManager.activeNetwork
49 | val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
50 |
51 | if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
52 | return true
53 | }
54 |
55 | if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
56 | return true
57 | }
58 |
59 | return false
60 | }
61 |
62 | @JvmStatic
63 | fun openURL(url: String) {
64 | Log.d("MAIN", "Open URL")
65 | me.get()?.runOnUiThread {
66 | try {
67 | me.get()?.startActivity(
68 | Intent(
69 | "android.intent.action.VIEW",
70 | Uri.parse(url)
71 | )
72 | )
73 | } catch (e: ActivityNotFoundException) {
74 | Toast.makeText(
75 | me.get(),
76 | "No activity found to open this URL.",
77 | Toast.LENGTH_SHORT,
78 | ).show()
79 | }
80 | }
81 | }
82 |
83 | @JvmStatic
84 | fun sendMail(subject: String, body: String, to: String) {
85 | me.get()?.runOnUiThread {
86 | val i = Intent("android.intent.action.SEND")
87 | i.type = "message/rfc822"
88 | i.putExtra("android.intent.extra.EMAIL", arrayOf(to))
89 | i.putExtra("android.intent.extra.SUBJECT", subject)
90 | i.putExtra("android.intent.extra.TEXT", body)
91 | try {
92 | me.get()?.startActivity(Intent.createChooser(i, "Send mail..."))
93 | } catch (e: ActivityNotFoundException) {
94 | Toast.makeText(
95 | me.get(),
96 | "There are no email clients installed.",
97 | Toast.LENGTH_SHORT,
98 | ).show()
99 | }
100 | }
101 | }
102 |
103 | @JvmStatic
104 | fun shouldResumeSound(): Boolean {
105 | return shouldResumeSound
106 | }
107 |
108 | @JvmStatic
109 | fun setKeyboardState(value: Boolean) {
110 | keyboardActive = value
111 | }
112 |
113 | @JvmStatic
114 | fun onToggleKeyboard() {
115 | me.get()?.runOnUiThread {
116 | if (keyboardActive) {
117 | openIMEKeyboard()
118 | } else {
119 | closeIMEKeyboard()
120 | }
121 | }
122 | }
123 |
124 | @JvmStatic
125 | fun loadingFinished() {
126 | isLoaded = true
127 | }
128 |
129 | @JvmStatic
130 | fun getDeviceRefreshRate(): Float {
131 | val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
132 | me.get()?.display
133 | } else {
134 | @Suppress("DEPRECATION")
135 | me.get()?.windowManager?.defaultDisplay
136 | }
137 |
138 | return display!!.refreshRate
139 | }
140 |
141 | // Everyplay doesn't even exist anymore lol
142 | @JvmStatic
143 | fun setupEveryplay() {}
144 |
145 | @JvmStatic
146 | fun isEveryplaySupported(): Boolean {
147 | return false
148 | }
149 |
150 | @JvmStatic
151 | fun isRecordingSupported(): Boolean {
152 | return false
153 | }
154 |
155 | @JvmStatic
156 | fun isRecordingPaused() : Boolean {
157 | return false
158 | }
159 |
160 | @JvmStatic
161 | fun isRecording() : Boolean {
162 | return false
163 | }
164 |
165 | @JvmStatic
166 | fun startRecording() {}
167 |
168 | @JvmStatic
169 | fun stopRecording() {}
170 |
171 | @JvmStatic
172 | fun resumeRecording() {}
173 |
174 | @JvmStatic
175 | fun pauseRecording() {}
176 |
177 | @JvmStatic
178 | fun playLastRecording() {}
179 |
180 | @JvmStatic
181 | fun showEveryplay() {}
182 |
183 | @JvmStatic
184 | fun setEveryplayMetadata(levelID: String, levelName: String) {}
185 |
186 | @JvmStatic
187 | fun onNativePause() {}
188 |
189 | @JvmStatic
190 | fun onNativeResume() {}
191 |
192 | // Google Play Games methods
193 | // for some reason the button is hidden in game
194 | @JvmStatic
195 | fun gameServicesIsSignedIn(): Boolean {
196 | return false
197 | }
198 |
199 | @JvmStatic
200 | fun gameServicesSignIn() { }
201 |
202 | @JvmStatic
203 | fun gameServicesSignOut() { }
204 |
205 | @JvmStatic
206 | fun unlockAchievement(id: String) {}
207 |
208 | @JvmStatic
209 | fun showAchievements() {}
210 |
211 | // advertisements stuff, useless for full version
212 | @JvmStatic
213 | fun enableBanner() {}
214 |
215 | @JvmStatic
216 | fun disableBanner() {}
217 |
218 | @JvmStatic
219 | fun showInterstitial() {}
220 |
221 | @JvmStatic
222 | fun cacheInterstitial() {}
223 |
224 | @JvmStatic
225 | fun hasCachedInterstitial(): Boolean {
226 | return false
227 | }
228 |
229 | @JvmStatic
230 | fun showRewardedVideo() {}
231 |
232 | @JvmStatic
233 | fun cacheRewardedVideo() {}
234 |
235 | @JvmStatic
236 | fun hasCachedRewardedVideo(): Boolean {
237 | return false
238 | }
239 |
240 | @JvmStatic
241 | fun queueRefreshBanner() {}
242 |
243 | @JvmStatic
244 | fun enableBannerNoRefresh() {}
245 |
246 | // for in game rate buttons, may implement in the future depending on how lazy I am
247 | // (requires Play Store library)
248 | @JvmStatic
249 | fun tryShowRateDialog(appName: String) {}
250 |
251 | @JvmStatic
252 | fun openAppPage() {}
253 |
254 | @JvmStatic
255 | @Deprecated(
256 | message = "This method is not found on newer versions of the game.",
257 | level = DeprecationLevel.HIDDEN
258 | )
259 | fun logEvent(event: String) {}
260 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/customRobTop/JniToCpp.kt:
--------------------------------------------------------------------------------
1 | package com.customRobTop
2 |
3 | @Suppress("unused", "KotlinJniMissingFunction")
4 | object JniToCpp {
5 | @JvmStatic
6 | external fun didCacheInterstitial(str: String?)
7 |
8 | @JvmStatic
9 | external fun didClickInterstitial()
10 |
11 | @JvmStatic
12 | external fun didCloseInterstitial()
13 |
14 | @JvmStatic
15 | external fun didDismissInterstitial()
16 |
17 | @JvmStatic
18 | external fun everyplayRecordingStopped()
19 |
20 | @JvmStatic
21 | external fun googlePlaySignedIn()
22 |
23 | @JvmStatic
24 | external fun hideLoadingCircle()
25 |
26 | @JvmStatic
27 | external fun itemPurchased(str: String)
28 |
29 | @JvmStatic
30 | external fun itemRefunded(str: String)
31 |
32 | @JvmStatic
33 | external fun promoImageDownloaded()
34 |
35 | @JvmStatic
36 | external fun resumeSound()
37 |
38 | @JvmStatic
39 | external fun rewardedVideoAdFinished(i: Int)
40 |
41 | @JvmStatic
42 | external fun rewardedVideoAdHidden()
43 |
44 | @JvmStatic
45 | external fun setupHSSAssets(str: String?, str2: String?)
46 |
47 | @JvmStatic
48 | external fun showInterstitialFailed()
49 |
50 | @JvmStatic
51 | external fun userDidAttemptToRateApp()
52 |
53 | @JvmStatic
54 | external fun videoAdHidden()
55 |
56 | @JvmStatic
57 | external fun videoAdShowed()
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/LauncherFix.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher
2 |
3 | import androidx.annotation.Keep
4 | import com.geode.launcher.utils.Constants
5 |
6 | @Keep
7 | object LauncherFix {
8 | fun loadLibrary() {
9 | System.loadLibrary(Constants.LAUNCHER_FIX_LIB_NAME)
10 | }
11 |
12 | external fun setDataPath(dataPath: String)
13 |
14 | external fun setOriginalDataPath(dataPath: String)
15 |
16 | external fun performExceptionsRenaming()
17 |
18 | external fun enableCustomSymbolList(symbolsPath: String)
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/UserDirectoryProvider.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher
2 |
3 | import android.database.Cursor
4 | import android.database.MatrixCursor
5 | import android.net.Uri
6 | import android.os.Build
7 | import android.os.CancellationSignal
8 | import android.os.ParcelFileDescriptor
9 | import android.provider.DocumentsContract
10 | import android.provider.DocumentsProvider
11 | import android.webkit.MimeTypeMap
12 | import androidx.annotation.RequiresApi
13 | import com.geode.launcher.utils.LaunchUtils
14 | import java.io.File
15 |
16 | private val DEFAULT_ROOT_PROJECTION: Array = arrayOf(
17 | DocumentsContract.Root.COLUMN_ROOT_ID,
18 | DocumentsContract.Root.COLUMN_FLAGS,
19 | DocumentsContract.Root.COLUMN_ICON,
20 | DocumentsContract.Root.COLUMN_TITLE,
21 | DocumentsContract.Root.COLUMN_DOCUMENT_ID,
22 | )
23 |
24 | private val DEFAULT_DOCUMENT_PROJECTION: Array = arrayOf(
25 | DocumentsContract.Document.COLUMN_DOCUMENT_ID,
26 | DocumentsContract.Document.COLUMN_MIME_TYPE,
27 | DocumentsContract.Document.COLUMN_DISPLAY_NAME,
28 | DocumentsContract.Document.COLUMN_LAST_MODIFIED,
29 | DocumentsContract.Document.COLUMN_FLAGS,
30 | DocumentsContract.Document.COLUMN_SIZE
31 | )
32 |
33 | // a lot of this code is pulled from
34 | class UserDirectoryProvider : DocumentsProvider() {
35 | companion object {
36 | internal const val ROOT = "root"
37 | }
38 |
39 | private lateinit var rootDir: File
40 |
41 | override fun onCreate(): Boolean {
42 | val context = context ?: return false
43 | rootDir = LaunchUtils.getBaseDirectory(context, true)
44 |
45 | return true
46 | }
47 |
48 | override fun queryRoots(projection: Array?): Cursor {
49 | val result = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
50 |
51 | // assert context is nonnull
52 | val context = context ?: return result
53 |
54 | result.newRow().apply {
55 | add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)
56 | add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, ROOT)
57 | add(
58 | DocumentsContract.Root.COLUMN_FLAGS,
59 | DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
60 | DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
61 | DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
62 | )
63 | add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.app_name))
64 | add(DocumentsContract.Root.COLUMN_ICON, R.drawable.geode_logo)
65 | }
66 |
67 | return result
68 | }
69 |
70 | override fun queryDocument(documentId: String?, projection: Array?): Cursor {
71 | return MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply {
72 | val file = getFileForDocumentId(documentId ?: ROOT)
73 | appendDocument(this, file)
74 | }
75 | }
76 |
77 | override fun queryChildDocuments(
78 | parentDocumentId: String?,
79 | projection: Array?,
80 | sortOrder: String?
81 | ): Cursor {
82 | return MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply {
83 | val parent = getFileForDocumentId(parentDocumentId ?: ROOT)
84 | parent.listFiles()?.forEach { file -> appendDocument(this, file) }
85 |
86 | context?.let {
87 | setNotificationUri(it.contentResolver, getDocumentUri(parentDocumentId))
88 | }
89 | }
90 | }
91 |
92 | override fun openDocument(
93 | documentId: String?,
94 | mode: String?,
95 | signal: CancellationSignal?
96 | ): ParcelFileDescriptor {
97 | val file = getFileForDocumentId(documentId ?: ROOT)
98 | return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode))
99 | }
100 |
101 | private fun appendDocument(cursor: MatrixCursor, file: File) {
102 | var flags = 0
103 | if (file.canWrite()) {
104 | flags = if (file.isDirectory) {
105 | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
106 | } else {
107 | DocumentsContract.Document.FLAG_SUPPORTS_WRITE
108 | }
109 | flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
110 | flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
111 | // The system will handle copy + move for us
112 | }
113 |
114 | val name = if (file == rootDir) {
115 | context!!.getString(R.string.app_name)
116 | } else {
117 | file.name
118 | }
119 | cursor.newRow().apply {
120 | add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, getDocumentIdForFile(file))
121 | add(DocumentsContract.Document.COLUMN_MIME_TYPE, typeForFile(file))
122 | add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name)
123 | add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified())
124 | add(DocumentsContract.Document.COLUMN_FLAGS, flags)
125 | add(DocumentsContract.Document.COLUMN_SIZE, file.length())
126 | if (file == rootDir) {
127 | add(DocumentsContract.Document.COLUMN_ICON, R.drawable.geode_logo)
128 | }
129 | }
130 | }
131 |
132 | override fun createDocument(
133 | parentDocumentId: String,
134 | mimeType: String,
135 | displayName: String
136 | ): String {
137 | val folder = getFileForDocumentId(parentDocumentId)
138 | val file = findFileNameForNewFile(File(folder, displayName))
139 | if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
140 | file.mkdirs()
141 | } else {
142 | file.createNewFile()
143 | }
144 |
145 | notifyFileChange(file)
146 | return getDocumentIdForFile(file)
147 | }
148 |
149 | override fun deleteDocument(documentId: String) {
150 | val file = getFileForDocumentId(documentId)
151 | file.deleteRecursively()
152 | notifyFileChange(file)
153 | }
154 |
155 | override fun renameDocument(documentId: String, displayName: String): String {
156 | val file = getFileForDocumentId(documentId)
157 | val dest = findFileNameForNewFile(File(file.parentFile, displayName))
158 | file.renameTo(dest)
159 |
160 | notifyFileChange(file)
161 | return getDocumentIdForFile(dest)
162 | }
163 |
164 | private fun typeForFile(file: File): String {
165 | if (file.isDirectory) {
166 | return DocumentsContract.Document.MIME_TYPE_DIR
167 | }
168 |
169 | return MimeTypeMap.getSingleton()
170 | .getMimeTypeFromExtension(file.extension) ?: "application/octet-stream"
171 | }
172 |
173 | private fun getFileForDocumentId(documentId: String): File {
174 | return File(rootDir, documentId.removePrefix(ROOT))
175 | }
176 |
177 | private fun getDocumentIdForFile(file: File): String {
178 | return ROOT + file.toRelativeString(rootDir)
179 | }
180 |
181 | private fun findFileNameForNewFile(file: File): File {
182 | var unusedFile = file
183 | var i = 1
184 | while (unusedFile.exists()) {
185 | val pathWithoutExtension = unusedFile.absolutePath.substringBeforeLast('.')
186 | val extension = unusedFile.absolutePath.substringAfterLast('.')
187 | unusedFile = File("$pathWithoutExtension.$i.$extension")
188 | i++
189 | }
190 | return unusedFile
191 | }
192 |
193 | private fun getDocumentUri(parentDocumentId: String?): Uri {
194 | return DocumentsContract.buildChildDocumentsUri(
195 | "${context!!.packageName}.user",
196 | parentDocumentId
197 | )
198 | }
199 |
200 | private fun notifyFileChange(file: File) {
201 | notifyChange(getDocumentIdForFile(file.parentFile!!))
202 | }
203 |
204 | private fun notifyChange(parentDocumentId: String?) {
205 | val uri = getDocumentUri(parentDocumentId)
206 | context!!.contentResolver.notifyChange(uri, null)
207 | }
208 |
209 | @RequiresApi(Build.VERSION_CODES.O)
210 | override fun findDocumentPath(
211 | parentDocumentId: String?,
212 | childDocumentId: String
213 | ): DocumentsContract.Path {
214 | if (!parentDocumentId.isNullOrEmpty()) {
215 | // not implementing this for now...
216 | return super.findDocumentPath(parentDocumentId, childDocumentId)
217 | }
218 |
219 | val nonRootPath = listOf(ROOT) + childDocumentId.removePrefix(ROOT).split("/")
220 |
221 | return DocumentsContract.Path(ROOT, nonRootPath)
222 | }
223 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/activityresult/GeodeOpenFileActivityResult.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.activityresult
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.os.Build
8 | import android.provider.DocumentsContract
9 | import androidx.activity.result.contract.ActivityResultContract
10 |
11 | open class GeodeOpenFileActivityResult : ActivityResultContract() {
12 | class OpenFileParams(val extraMimes: Array, val defaultPath: Uri?) {}
13 | override fun createIntent(context: Context, input: OpenFileParams): Intent {
14 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
15 | .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
16 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
17 | .putExtra(Intent.EXTRA_MIME_TYPES, input.extraMimes)
18 | .setType("*/*")
19 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
20 | if (input.defaultPath != null) {
21 | intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
22 | }
23 | }
24 | return intent
25 | }
26 |
27 | final override fun getSynchronousResult(
28 | context: Context,
29 | input: OpenFileParams
30 | ): SynchronousResult? = null
31 |
32 | final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
33 | return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/activityresult/GeodeOpenFilesActivityResult.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.activityresult
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.os.Build
8 | import android.provider.DocumentsContract
9 | import androidx.activity.result.contract.ActivityResultContract
10 |
11 | open class GeodeOpenFilesActivityResult : ActivityResultContract>() {
12 | class OpenFileParams(val extraMimes: Array, val defaultPath: Uri?) {}
13 | override fun createIntent(context: Context, input: OpenFileParams): Intent {
14 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
15 | .putExtra(Intent.EXTRA_MIME_TYPES, input.extraMimes)
16 | .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
17 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
18 | .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
19 | .setType("*/*")
20 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
21 | if (input.defaultPath != null) {
22 | intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
23 | }
24 | }
25 | return intent
26 | }
27 |
28 | final override fun getSynchronousResult(
29 | context: Context,
30 | input: OpenFileParams
31 | ): SynchronousResult>? = null
32 |
33 | final override fun parseResult(resultCode: Int, intent: Intent?): List {
34 | // Gracefully stolen from ActivityResultContracts.Intent.getclipDataUris
35 | val clipData = intent.takeIf {
36 | resultCode == Activity.RESULT_OK
37 | }?.clipData
38 | val resultSet = LinkedHashSet()
39 | intent?.data?.let { data ->
40 | resultSet.add(data)
41 | }
42 | if (clipData == null && resultSet.isEmpty()) {
43 | return emptyList()
44 | } else if (clipData != null) {
45 | for (i in 0 until clipData.itemCount) {
46 | val uri = clipData.getItemAt(i).uri
47 | if (uri != null) {
48 | resultSet.add(uri)
49 | }
50 | }
51 | }
52 | return ArrayList(resultSet)
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/activityresult/GeodeSaveFileActivityResult.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.activityresult
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.os.Build
8 | import android.provider.DocumentsContract
9 | import androidx.activity.result.contract.ActivityResultContract
10 | import androidx.documentfile.provider.DocumentFile
11 | import java.io.File
12 |
13 | open class GeodeSaveFileActivityResult : ActivityResultContract() {
14 | class SaveFileParams(val mimeType: String?, val initialPath: File?)
15 | override fun createIntent(context: Context, input: SaveFileParams): Intent {
16 | val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
17 | .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
18 | .setType(input.mimeType ?: "*/*")
19 |
20 | if (input.initialPath != null) {
21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
22 | val uri = DocumentFile.fromFile(input.initialPath).uri
23 | intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
24 | }
25 |
26 | intent.putExtra(Intent.EXTRA_TITLE, input.initialPath.name)
27 | }
28 |
29 | return intent
30 | }
31 |
32 | final override fun getSynchronousResult(
33 | context: Context,
34 | input: SaveFileParams
35 | ): SynchronousResult? = null
36 |
37 | final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
38 | return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/log/LogLine.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.log
2 |
3 | import kotlinx.datetime.Clock
4 | import kotlinx.datetime.Instant
5 | import kotlinx.datetime.TimeZone
6 | import kotlinx.datetime.toJavaInstant
7 | import kotlinx.datetime.toLocalDateTime
8 | import okio.Buffer
9 | import okio.BufferedSource
10 | import java.io.IOException
11 |
12 | fun BufferedSource.readCChar(): Char {
13 | return this.readUtf8CodePoint().toChar()
14 | }
15 |
16 | fun BufferedSource.readCString(): String {
17 | val buffer = StringBuilder()
18 |
19 | var lastByte = this.readCChar()
20 | while (lastByte != '\u0000') {
21 | buffer.append(lastByte)
22 | lastByte = this.readCChar()
23 | }
24 |
25 | return buffer.toString()
26 | }
27 |
28 | data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
29 |
30 | enum class LogPriority {
31 | UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
32 |
33 | class LogPriorityIterator(
34 | start: LogPriority,
35 | private val end: LogPriority,
36 | private val step: Int
37 | ) : Iterator {
38 | private var hasNext: Boolean = if (step > 0) start <= end else start >= end
39 | private var next = if (hasNext) start else end
40 |
41 | override fun hasNext() = hasNext
42 |
43 | override fun next(): LogPriority {
44 | val current = next
45 | if (current == end) {
46 | if (!hasNext) {
47 | throw NoSuchElementException()
48 | }
49 |
50 | hasNext = false
51 | } else {
52 | next = LogPriority.fromInt(next.toInt() + step)
53 | }
54 |
55 | return current
56 | }
57 | }
58 |
59 | class LogPriorityProgression(
60 | override val start: LogPriority,
61 | override val endInclusive: LogPriority,
62 | private val step: Int = 1
63 | ) : Iterable, ClosedRange {
64 | override fun iterator(): Iterator {
65 | return LogPriorityIterator(start, endInclusive, step)
66 | }
67 | }
68 |
69 | infix fun downTo(to: LogPriority) = LogPriorityProgression(this, to, -1)
70 |
71 | operator fun rangeTo(to: LogPriority) = LogPriorityProgression(this, to)
72 |
73 | companion object {
74 | fun fromByte(byte: Byte): LogPriority = fromInt(byte.toInt())
75 |
76 | fun fromInt(int: Int) = when (int) {
77 | 0x1 -> DEFAULT
78 | 0x2 -> VERBOSE
79 | 0x3 -> DEBUG
80 | 0x4 -> INFO
81 | 0x5 -> WARN
82 | 0x6 -> ERROR
83 | 0x7 -> FATAL
84 | 0x8 -> SILENT
85 | else -> UNKNOWN
86 | }
87 | }
88 |
89 | fun toInt() = when (this) {
90 | DEFAULT -> 0x1
91 | VERBOSE -> 0x2
92 | DEBUG -> 0x3
93 | INFO -> 0x4
94 | WARN -> 0x5
95 | ERROR -> 0x6
96 | FATAL -> 0x7
97 | SILENT -> 0x8
98 | else -> 0x0
99 | }
100 |
101 | fun toChar(): Char {
102 | return when (this) {
103 | VERBOSE -> 'V'
104 | DEBUG -> 'D'
105 | INFO -> 'I'
106 | WARN -> 'W'
107 | ERROR -> 'E'
108 | FATAL -> 'F'
109 | SILENT -> 'S'
110 | else -> '?'
111 | }
112 | }
113 | }
114 |
115 | /**
116 | * Represents a log entry from logcat.
117 | */
118 | data class LogLine(
119 | val process: ProcessInformation,
120 | val time: Instant,
121 | val logId: Int,
122 | val priority: LogPriority,
123 | val tag: String,
124 | val message: String
125 | ) {
126 | companion object {
127 | private enum class EntryVersion {
128 | V3, V4
129 | }
130 |
131 | private fun headerSizeToVersion(size: UShort) = when (size.toInt()) {
132 | 0x18 -> EntryVersion.V3
133 | 0x1c -> EntryVersion.V4
134 | else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
135 | }
136 |
137 | fun fromBufferedSource(source: BufferedSource): LogLine {
138 | /*
139 | // from android
140 | // there are multiple logger entry formats
141 | // use the header_size to determine the one you have
142 |
143 | struct logger_entry_v3 {
144 | uint16_t len; // length of the payload
145 | uint16_t hdr_size; // sizeof(struct logger_entry_v3)
146 | int32_t pid; // generating process's pid
147 | int32_t tid; // generating process's tid
148 | int32_t sec; // seconds since Epoch
149 | int32_t nsec; // nanoseconds
150 | uint32_t lid; // log id of the payload
151 | }
152 |
153 | struct logger_entry_v4 {
154 | uint16_t len; // length of the payload
155 | uint16_t hdr_size; // sizeof(struct logger_entry_v4 = 28)
156 | int32_t pid; // generating process's pid
157 | uint32_t tid; // generating process's tid
158 | uint32_t sec; // seconds since Epoch
159 | uint32_t nsec; // nanoseconds
160 | uint32_t lid; // log id of the payload, bottom 4 bits currently
161 | uint32_t uid; // generating process's uid
162 | };
163 | */
164 |
165 | val payloadLength = source.readShortLe().toLong()
166 | val headerSize = source.readShortLe().toUShort()
167 |
168 | val entryVersion = headerSizeToVersion(headerSize)
169 |
170 | val pid = source.readIntLe()
171 | val tid = source.readIntLe()
172 | val sec = source.readIntLe().toLong()
173 | val nSec = source.readIntLe()
174 | val lid = source.readIntLe()
175 | val uid = if (entryVersion == EntryVersion.V4)
176 | source.readIntLe() else 0
177 |
178 | val processInformation = ProcessInformation(pid, tid, uid)
179 | val time = Instant.fromEpochSeconds(sec, nSec)
180 |
181 | // the payload is split into three parts
182 | // initial priority byte -> null terminated tag -> non null terminated message
183 |
184 | val packetBuffer = Buffer()
185 | source.readFully(packetBuffer, payloadLength)
186 |
187 | val priorityByte = packetBuffer.readByte()
188 | val priority = LogPriority.fromByte(priorityByte)
189 |
190 | val tag = packetBuffer.readCString()
191 | val message = packetBuffer.readUtf8()
192 |
193 | return LogLine(
194 | process = processInformation,
195 | priority = priority,
196 | time = time,
197 | logId = lid,
198 | tag = tag,
199 | message = message
200 | )
201 | }
202 |
203 | fun showException(exception: Exception) = LogLine(
204 | process = ProcessInformation(0, 0, 0),
205 | time = Clock.System.now(),
206 | logId = 0,
207 | priority = LogPriority.FATAL,
208 | tag = "GeodeLauncher",
209 | message = "Failed to parse log entry with ${exception.stackTraceToString()}"
210 | )
211 | }
212 |
213 | val identifier = time.toJavaInstant()
214 |
215 | val messageTrimmed by lazy {
216 | this.message.trim { it <= ' ' }
217 | }
218 |
219 | val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
220 | val asSimpleString by lazy {
221 | "$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
222 | }
223 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/log/LogViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.log
2 |
3 | import androidx.compose.runtime.mutableStateListOf
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.Job
8 | import kotlinx.coroutines.coroutineScope
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.asStateFlow
11 | import kotlinx.coroutines.launch
12 | import kotlinx.coroutines.withContext
13 | import kotlinx.coroutines.yield
14 | import okio.Buffer
15 | import okio.buffer
16 | import okio.source
17 | import java.io.EOFException
18 | import java.io.IOException
19 |
20 | /**
21 | * ViewModel to manage log output from logcat.
22 | * Currently, it loads the list of logs at init with no ability to read additional logs.
23 | */
24 | class LogViewModel: ViewModel() {
25 | // the original version of this code did support streaming btw
26 | // the states were way too annoying to manage across multiple threads
27 | // yay!
28 |
29 | private val _lineState = mutableStateListOf()
30 | val lineState = _lineState
31 |
32 | private val _isLoading = MutableStateFlow(false)
33 | val isLoading = _isLoading.asStateFlow()
34 |
35 | private var failedToLoad = false
36 |
37 | private var logJob: Job? = null
38 |
39 | var filterCrashes = false
40 | private set
41 |
42 | // skip debug/verbose lines by default
43 | var logLevel = LogPriority.INFO
44 | set(value) {
45 | field = value
46 |
47 | _lineState.clear()
48 | loadLogs()
49 | }
50 |
51 | fun toggleCrashBuffer() {
52 | filterCrashes = !filterCrashes
53 |
54 | _lineState.clear()
55 | loadLogs()
56 | }
57 |
58 | private suspend fun logOutput(): List {
59 | val logLines = ArrayList()
60 |
61 | // -B = binary format, -d = dump logs
62 | val logCommand = if (filterCrashes) "logcat -b crash -B -d"
63 | else "logcat -B -d"
64 |
65 | val logProcess = try {
66 | withContext(Dispatchers.IO) {
67 | Runtime.getRuntime().exec(logCommand)
68 | }
69 | } catch (ioe: IOException) {
70 | ioe.printStackTrace()
71 | logLines += LogLine.showException(ioe)
72 |
73 | failedToLoad = true
74 |
75 | return logLines
76 | }
77 |
78 | // read entire log into a buffer so no logs are added to the buffer during processing
79 | val logBuffer = Buffer()
80 | logProcess.inputStream.source().buffer().readAll(logBuffer)
81 |
82 | try {
83 | coroutineScope {
84 | // this runs until the stream is exhausted
85 | while (true) {
86 | val line = LogLine.fromBufferedSource(logBuffer)
87 |
88 | if (line.priority >= logLevel) {
89 | logLines += line
90 | }
91 |
92 | yield()
93 | }
94 | }
95 | } catch (_: EOFException) {
96 | // ignore, end of file reached
97 | } catch (e: Exception) {
98 | e.printStackTrace()
99 | logLines += LogLine.showException(e)
100 |
101 | // technically it maybe didn't completely fail...
102 | failedToLoad = true
103 | }
104 |
105 | return logLines
106 | }
107 |
108 | fun clearLogs() {
109 | logJob?.cancel()
110 |
111 | viewModelScope.launch(Dispatchers.IO) {
112 | // -c = clear
113 | Runtime.getRuntime().exec("logcat -c")
114 | .waitFor()
115 | }
116 |
117 | _lineState.clear()
118 | }
119 |
120 | private fun dumpLogcatText(): String {
121 | val logCommand = if (filterCrashes) "logcat -b crash -d"
122 | else "logcat -d"
123 |
124 | val logProcess = try {
125 | Runtime.getRuntime().exec(logCommand)
126 | } catch (ioe: IOException) {
127 | ioe.printStackTrace()
128 | return ioe.stackTraceToString()
129 | }
130 |
131 | return logProcess.inputStream.bufferedReader().readText()
132 | }
133 |
134 | fun getLogData(): String = if (failedToLoad) {
135 | dumpLogcatText()
136 | } else {
137 | _lineState.joinToString("\n") { it.asSimpleString }
138 | }
139 |
140 | private fun loadLogs() {
141 | _isLoading.value = true
142 |
143 | logJob?.cancel()
144 | logJob = viewModelScope.launch(Dispatchers.IO) {
145 | val lines = logOutput()
146 | _lineState.addAll(lines)
147 | _isLoading.value = false
148 |
149 | logJob = null
150 | }
151 | }
152 |
153 | init {
154 | loadLogs()
155 | }
156 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/main/Components.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.main
2 |
3 | import android.content.ActivityNotFoundException
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.widget.Toast
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.clickable
10 | import androidx.compose.foundation.layout.Arrangement
11 | import androidx.compose.foundation.layout.Column
12 | import androidx.compose.foundation.layout.Row
13 | import androidx.compose.foundation.layout.Spacer
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.size
16 | import androidx.compose.foundation.layout.sizeIn
17 | import androidx.compose.foundation.layout.width
18 | import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
19 | import androidx.compose.material3.ElevatedCard
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.ui.Alignment
22 | import androidx.compose.ui.Modifier
23 | import androidx.compose.ui.draw.clip
24 | import androidx.compose.ui.platform.LocalContext
25 | import androidx.compose.ui.res.painterResource
26 | import androidx.compose.ui.res.stringResource
27 | import androidx.compose.ui.unit.dp
28 | import com.geode.launcher.R
29 | import com.geode.launcher.utils.Constants
30 |
31 | fun onDownloadGame(context: Context) {
32 | val appUrl = "https://play.google.com/store/apps/details?id=${Constants.PACKAGE_NAME}"
33 | try {
34 | val intent = Intent(Intent.ACTION_VIEW).apply {
35 | data = Uri.parse(appUrl)
36 | setPackage("com.android.vending")
37 | }
38 | context.startActivity(intent)
39 | } catch (_: ActivityNotFoundException) {
40 | Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
41 | }
42 | }
43 |
44 | @Composable
45 | fun GooglePlayBadge(modifier: Modifier = Modifier) {
46 | val context = LocalContext.current
47 |
48 | Image(
49 | painter = painterResource(id = R.drawable.google_play_badge),
50 | contentDescription = stringResource(R.string.launcher_download_game),
51 | modifier = modifier
52 | .width(196.dp)
53 | .clip(AbsoluteRoundedCornerShape(4.dp))
54 | .clickable { onDownloadGame(context) }
55 | )
56 | }
57 |
58 | @Composable
59 | fun StatusIndicator(
60 | icon: @Composable () -> Unit,
61 | text: @Composable () -> Unit,
62 | onClick: () -> Unit,
63 | modifier: Modifier = Modifier
64 | ) {
65 | ElevatedCard(modifier = modifier, onClick = onClick) {
66 | Row(modifier = Modifier.padding(8.dp)) {
67 | icon()
68 | Spacer(Modifier.size(8.dp))
69 | text()
70 | }
71 | }
72 | }
73 |
74 | @Composable
75 | fun InlineDialog(headline: @Composable () -> Unit, body: @Composable () -> Unit, modifier: Modifier = Modifier) {
76 | ElevatedCard(modifier = modifier) {
77 | Column(modifier = Modifier.padding(18.dp).sizeIn(maxWidth = 512.dp)) {
78 | Row(
79 | verticalAlignment = Alignment.CenterVertically,
80 | horizontalArrangement = Arrangement.spacedBy(8.dp),
81 | modifier = Modifier.padding(bottom = 8.dp)
82 | ) {
83 | headline()
84 | }
85 |
86 | body()
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/main/LaunchNotification.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.main
2 |
3 | import android.content.Context
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.core.MutableTransitionState
6 | import androidx.compose.animation.expandHorizontally
7 | import androidx.compose.animation.fadeIn
8 | import androidx.compose.animation.fadeOut
9 | import androidx.compose.animation.shrinkHorizontally
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.IntrinsicSize
12 | import androidx.compose.foundation.layout.displayCutoutPadding
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.size
15 | import androidx.compose.foundation.layout.width
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.filled.Info
18 | import androidx.compose.material.icons.filled.Warning
19 | import androidx.compose.material3.Icon
20 | import androidx.compose.material3.ListItem
21 | import androidx.compose.material3.LocalContentColor
22 | import androidx.compose.material3.MaterialTheme
23 | import androidx.compose.material3.OutlinedCard
24 | import androidx.compose.material3.Text
25 | import androidx.compose.runtime.Composable
26 | import androidx.compose.runtime.CompositionLocalProvider
27 | import androidx.compose.runtime.LaunchedEffect
28 | import androidx.compose.runtime.getValue
29 | import androidx.compose.runtime.mutableStateOf
30 | import androidx.compose.runtime.remember
31 | import androidx.compose.runtime.setValue
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.platform.LocalContext
34 | import androidx.compose.ui.res.painterResource
35 | import androidx.compose.ui.res.stringResource
36 | import androidx.compose.ui.unit.dp
37 | import com.geode.launcher.R
38 | import com.geode.launcher.ui.theme.GeodeLauncherTheme
39 | import com.geode.launcher.ui.theme.LocalTheme
40 | import com.geode.launcher.ui.theme.Theme
41 | import com.geode.launcher.updater.ReleaseManager
42 | import com.geode.launcher.utils.Constants
43 | import com.geode.launcher.utils.GamePackageUtils
44 | import com.geode.launcher.utils.PreferenceUtils
45 | import kotlinx.coroutines.delay
46 |
47 | enum class LaunchNotificationType {
48 | GEODE_UPDATED, LAUNCHER_UPDATE_AVAILABLE, UPDATE_FAILED, UNSUPPORTED_VERSION;
49 | }
50 |
51 | fun determineDisplayedCards(context: Context): List {
52 | val cards = mutableListOf()
53 |
54 | if (GamePackageUtils.getGameVersionCode(context.packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
55 | cards.add(LaunchNotificationType.UNSUPPORTED_VERSION)
56 | }
57 |
58 | val releaseManager = ReleaseManager.get(context)
59 | val loadAutomatically = PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
60 |
61 | val availableUpdate = releaseManager.availableLauncherUpdate.value
62 | if (loadAutomatically && availableUpdate != null) {
63 | cards.add(LaunchNotificationType.LAUNCHER_UPDATE_AVAILABLE)
64 | }
65 |
66 | val releaseState = releaseManager.uiState.value
67 |
68 | if (releaseState is ReleaseManager.ReleaseManagerState.Finished && releaseState.hasUpdated) {
69 | cards.add(LaunchNotificationType.GEODE_UPDATED)
70 | }
71 |
72 | if (releaseState is ReleaseManager.ReleaseManagerState.Failure) {
73 | cards.add(LaunchNotificationType.UPDATE_FAILED)
74 | }
75 |
76 | return cards
77 | }
78 |
79 | @Composable
80 | fun NotificationCardFromType(type: LaunchNotificationType) {
81 | when (type) {
82 | LaunchNotificationType.LAUNCHER_UPDATE_AVAILABLE -> {
83 | var showInfoDialog by remember { mutableStateOf(false) }
84 |
85 | if (showInfoDialog) {
86 | LauncherUpdateInformation {
87 | showInfoDialog = false
88 | }
89 | }
90 |
91 | AnimatedNotificationCard(
92 | displayLength = 5000L,
93 | onClick = {
94 | showInfoDialog = true
95 | }
96 | ) {
97 | LauncherUpdateContent()
98 | }
99 | }
100 | LaunchNotificationType.UPDATE_FAILED -> {
101 | AnimatedNotificationCard {
102 | UpdateFailedContent()
103 | }
104 | }
105 | LaunchNotificationType.UNSUPPORTED_VERSION -> {
106 | AnimatedNotificationCard {
107 | OutdatedVersionContent()
108 | }
109 | }
110 | else -> {}
111 | }
112 | }
113 |
114 | @Composable
115 | fun LaunchNotification() {
116 | val context = LocalContext.current
117 |
118 | val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
119 | val theme = Theme.fromInt(themeOption)
120 |
121 | val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
122 |
123 | val cards = determineDisplayedCards(context)
124 |
125 | CompositionLocalProvider(LocalTheme provides theme) {
126 | GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
127 | // surface is not in use, so this is unfortunately not provided
128 | CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
129 | Column(
130 | modifier = Modifier.displayCutoutPadding()
131 | ) {
132 | cards.forEach {
133 | NotificationCardFromType(it)
134 | }
135 | }
136 | }
137 | }
138 | }
139 | }
140 |
141 | @Composable
142 | fun AnimatedNotificationCard(modifier: Modifier = Modifier, visibilityDelay: Long = 0L, displayLength: Long = 3000L, onClick: (() -> Unit)? = null, contents: @Composable () -> Unit) {
143 | val state = remember {
144 | MutableTransitionState(false).apply {
145 | targetState = visibilityDelay <= 0
146 | }
147 | }
148 |
149 | LaunchedEffect(true) {
150 | if (visibilityDelay > 0) {
151 | delay(visibilityDelay)
152 | state.targetState = true
153 | }
154 |
155 | delay(displayLength)
156 | state.targetState = false
157 | }
158 |
159 | AnimatedVisibility(
160 | visibleState = state,
161 | enter = expandHorizontally() + fadeIn(),
162 | exit = shrinkHorizontally() + fadeOut(),
163 | modifier = modifier
164 | ) {
165 | CardView(onClick, modifier = Modifier.padding(8.dp)) {
166 | contents()
167 | }
168 | }
169 | }
170 |
171 | @Composable
172 | fun CardView(onClick: (() -> Unit)?, modifier: Modifier = Modifier, contents: @Composable () -> Unit) {
173 | // val surfaceColor = MaterialTheme.colorScheme.background.copy(alpha = 0.75f)
174 |
175 | if (onClick != null) {
176 | OutlinedCard(
177 | onClick = onClick,
178 | modifier = modifier
179 | ) {
180 | contents()
181 | }
182 | } else {
183 | OutlinedCard(
184 | modifier = modifier
185 | ) {
186 | contents()
187 | }
188 | }
189 | }
190 |
191 | @Composable
192 | fun LauncherUpdateContent(modifier: Modifier = Modifier) {
193 | ListItem(
194 | headlineContent = {
195 | Text(text = stringResource(id = R.string.launcher_update_available))
196 | },
197 | supportingContent = {
198 | Text(text = stringResource(id = R.string.launcher_notification_update_cta))
199 | },
200 | leadingContent = {
201 | Icon(
202 | Icons.Filled.Info,
203 | contentDescription = null,
204 | )
205 | },
206 | modifier = modifier.width(IntrinsicSize.Max)
207 | )
208 | }
209 |
210 | @Composable
211 | fun UpdateNotificationContent(modifier: Modifier = Modifier) {
212 | ListItem(
213 | headlineContent = {
214 | Text(text = stringResource(id = R.string.launcher_notification_update_success))
215 | },
216 | leadingContent = {
217 | Icon(
218 | painterResource(R.drawable.geode_monochrome),
219 | contentDescription = null,
220 | modifier = Modifier.size(24.dp)
221 | )
222 | },
223 | modifier = modifier.width(IntrinsicSize.Max)
224 | )
225 | }
226 |
227 | @Composable
228 | fun OutdatedVersionContent(modifier: Modifier = Modifier) {
229 | ListItem(
230 | headlineContent = {
231 | Text(text = stringResource(id = R.string.launcher_notification_compatibility))
232 | },
233 | leadingContent = {
234 | Icon(
235 | Icons.Filled.Warning,
236 | contentDescription = null,
237 | )
238 | },
239 | supportingContent = {
240 | Text(text = stringResource(id = R.string.launcher_notification_compatibility_description))
241 | },
242 | modifier = modifier.width(IntrinsicSize.Max)
243 | )
244 | }
245 |
246 | @Composable
247 | fun UpdateFailedContent(modifier: Modifier = Modifier) {
248 | ListItem(
249 | headlineContent = {
250 | Text(text = stringResource(id = R.string.launcher_notification_update_failed))
251 | },
252 | leadingContent = {
253 | Icon(
254 | Icons.Filled.Warning,
255 | contentDescription = null,
256 | )
257 | },
258 | modifier = modifier.width(IntrinsicSize.Max)
259 | )
260 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/main/LaunchViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.main
2 |
3 | import android.app.Application
4 | import android.os.CountDownTimer
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.ViewModelProvider
7 | import androidx.lifecycle.viewModelScope
8 | import androidx.lifecycle.viewmodel.initializer
9 | import androidx.lifecycle.viewmodel.viewModelFactory
10 | import com.geode.launcher.updater.ReleaseManager
11 | import com.geode.launcher.utils.Constants
12 | import com.geode.launcher.utils.GamePackageUtils
13 | import com.geode.launcher.utils.LaunchUtils
14 | import com.geode.launcher.utils.PreferenceUtils
15 | import kotlinx.coroutines.flow.MutableStateFlow
16 | import kotlinx.coroutines.flow.asStateFlow
17 | import kotlinx.coroutines.flow.map
18 | import kotlinx.coroutines.flow.takeWhile
19 | import kotlinx.coroutines.launch
20 |
21 | private const val COOLDOWN_LENGTH_MS = 3000L
22 |
23 | class LaunchViewModel(private val application: Application): ViewModel() {
24 | companion object {
25 | val Factory: ViewModelProvider.Factory = viewModelFactory {
26 | initializer {
27 | val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
28 |
29 | LaunchViewModel(
30 | application = application
31 | )
32 | }
33 | }
34 | }
35 |
36 | private var readyTimerPassed = false
37 | private var readyTimer: CountDownTimer? = null
38 |
39 | private fun initReadyTimer() {
40 | readyTimer = object : CountDownTimer(COOLDOWN_LENGTH_MS, 1000) {
41 | override fun onTick(millisUntilFinished: Long) {
42 | // no tick necessary
43 | }
44 |
45 | override fun onFinish() {
46 | readyTimerPassed = true
47 |
48 | if (_uiState.value is LaunchUIState.Working) {
49 | viewModelScope.launch {
50 | preReadyCheck()
51 | }
52 | }
53 | }
54 | }.start()
55 | }
56 |
57 | enum class LaunchCancelReason {
58 | MANUAL, AUTOMATIC, LAST_LAUNCH_CRASHED, GEODE_NOT_FOUND, GAME_MISSING, GAME_OUTDATED;
59 |
60 | fun allowsRetry() = when (this) {
61 | MANUAL,
62 | AUTOMATIC,
63 | LAST_LAUNCH_CRASHED,
64 | GEODE_NOT_FOUND -> true
65 | GAME_MISSING,
66 | GAME_OUTDATED -> false
67 | }
68 |
69 | fun isGameInstallIssue() = when (this) {
70 | GAME_OUTDATED,
71 | GAME_MISSING -> true
72 | else -> false
73 | }
74 | }
75 |
76 | sealed class LaunchUIState {
77 | data object Initial : LaunchUIState()
78 | data object UpdateCheck : LaunchUIState()
79 | data class Updating(val downloaded: Long, val outOf: Long?) : LaunchUIState()
80 | data class Cancelled(val reason: LaunchCancelReason, val inProgress: Boolean = false) : LaunchUIState()
81 | data object Working : LaunchUIState()
82 | data object Ready : LaunchUIState()
83 | }
84 |
85 | private val _uiState = MutableStateFlow(LaunchUIState.Initial)
86 | val uiState = _uiState.asStateFlow()
87 |
88 | val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
89 |
90 | var loadFailure: LoadFailureInfo? = null
91 | private var hasCheckedForUpdates = false
92 | private var isCancelling = false
93 | private var hasManuallyStarted = false
94 |
95 | private suspend fun determineUpdateStatus() {
96 | ReleaseManager.get(application).uiState.takeWhile {
97 | it is ReleaseManager.ReleaseManagerState.InUpdateCheck || it is ReleaseManager.ReleaseManagerState.InDownload
98 | }.map { when (it) {
99 | is ReleaseManager.ReleaseManagerState.InUpdateCheck -> LaunchUIState.UpdateCheck
100 | is ReleaseManager.ReleaseManagerState.InDownload -> LaunchUIState.Updating(it.downloaded, it.outOf)
101 | else -> LaunchUIState.UpdateCheck
102 | }
103 | }.collect {
104 | _uiState.emit(it)
105 | }
106 |
107 | // flow terminates once update check is finished
108 | hasCheckedForUpdates = true
109 | preReadyCheck()
110 | }
111 |
112 | fun currentCrashInfo(): LoadFailureInfo? {
113 | val currentState = _uiState.value
114 | if (currentState is LaunchUIState.Cancelled && currentState.reason == LaunchCancelReason.LAST_LAUNCH_CRASHED) {
115 | return loadFailure
116 | }
117 |
118 | return null
119 | }
120 |
121 | fun clearCrashInfo() {
122 | val currentState = _uiState.value
123 | if (currentState is LaunchUIState.Cancelled && currentState.reason == LaunchCancelReason.LAST_LAUNCH_CRASHED) {
124 | loadFailure = null
125 | }
126 | }
127 |
128 | private suspend fun preReadyCheck() {
129 | if (isCancelling) {
130 | // don't let the game start if cancelled
131 | return
132 | }
133 |
134 | val packageManager = application.packageManager
135 |
136 | if (!GamePackageUtils.isGameInstalled(packageManager)) {
137 | return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
138 | }
139 |
140 | if (ReleaseManager.get(application).isInUpdate) {
141 | viewModelScope.launch {
142 | determineUpdateStatus()
143 | }
144 | return
145 | }
146 |
147 | if (loadFailure != null) {
148 | // last launch crashed, so cancel
149 | return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.LAST_LAUNCH_CRASHED))
150 | }
151 |
152 | if (GamePackageUtils.getGameVersionCode(packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN) {
153 | return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
154 | }
155 |
156 | if (!LaunchUtils.isGeodeInstalled(application)) {
157 | return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
158 | }
159 |
160 | val loadAutomatically = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
161 | if (!loadAutomatically && !hasManuallyStarted) {
162 | return cancelLaunch(true)
163 | }
164 |
165 | if (!readyTimerPassed && !hasManuallyStarted) {
166 | return _uiState.emit(LaunchUIState.Working)
167 | }
168 |
169 | _uiState.emit(LaunchUIState.Ready)
170 | }
171 |
172 | suspend fun beginLaunchFlow(isRestart: Boolean = false) {
173 | if (isRestart) {
174 | hasManuallyStarted = true
175 | }
176 |
177 | if (_uiState.value !is LaunchUIState.Initial && _uiState.value !is LaunchUIState.Cancelled) {
178 | return
179 | }
180 |
181 | if (_uiState.value is LaunchUIState.Cancelled && !isRestart) {
182 | return
183 | }
184 |
185 | isCancelling = false
186 |
187 | if (!GamePackageUtils.isGameInstalled(application.packageManager)) {
188 | return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
189 | }
190 |
191 | if (readyTimer == null) {
192 | initReadyTimer()
193 | }
194 |
195 | val hasGeode = LaunchUtils.isGeodeInstalled(application)
196 | val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
197 | if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) {
198 | ReleaseManager.get(application).checkForUpdates(false)
199 | }
200 |
201 | preReadyCheck()
202 | }
203 |
204 | suspend fun cancelLaunch(isAutomatic: Boolean = false) {
205 | // no need to double cancel
206 | if (_uiState.value is LaunchUIState.Cancelled) {
207 | return
208 | }
209 |
210 | val reason = if (isAutomatic) LaunchCancelReason.AUTOMATIC else LaunchCancelReason.MANUAL
211 |
212 | isCancelling = true
213 | _uiState.emit(LaunchUIState.Cancelled(reason, true))
214 |
215 | val releaseManager = ReleaseManager.get(application)
216 | if (releaseManager.isInUpdate) {
217 | releaseManager.cancelUpdate()
218 | }
219 |
220 | _uiState.emit(LaunchUIState.Cancelled(reason, false))
221 | }
222 |
223 | override fun onCleared() {
224 | super.onCleared()
225 |
226 | readyTimer?.cancel()
227 | }
228 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/preferences/DeveloperSettingsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.preferences
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.OnBackPressedDispatcher
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.enableEdgeToEdge
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.rememberScrollState
15 | import androidx.compose.foundation.verticalScroll
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
18 | import androidx.compose.material.icons.filled.Info
19 | import androidx.compose.material3.ElevatedCard
20 | import androidx.compose.material3.ExperimentalMaterial3Api
21 | import androidx.compose.material3.HorizontalDivider
22 | import androidx.compose.material3.Icon
23 | import androidx.compose.material3.IconButton
24 | import androidx.compose.material3.MaterialTheme
25 | import androidx.compose.material3.Scaffold
26 | import androidx.compose.material3.Surface
27 | import androidx.compose.material3.Text
28 | import androidx.compose.material3.TopAppBar
29 | import androidx.compose.material3.TopAppBarDefaults
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.CompositionLocalProvider
32 | import androidx.compose.runtime.getValue
33 | import androidx.compose.ui.Modifier
34 | import androidx.compose.ui.input.nestedscroll.nestedScroll
35 | import androidx.compose.ui.res.stringResource
36 | import androidx.compose.ui.text.style.TextOverflow
37 | import androidx.compose.ui.unit.dp
38 | import com.geode.launcher.R
39 | import com.geode.launcher.ui.theme.GeodeLauncherTheme
40 | import com.geode.launcher.ui.theme.LocalTheme
41 | import com.geode.launcher.ui.theme.Theme
42 | import com.geode.launcher.utils.LabelledText
43 | import com.geode.launcher.utils.PreferenceUtils
44 |
45 | class DeveloperSettingsActivity : ComponentActivity() {
46 | override fun onCreate(savedInstanceState: Bundle?) {
47 | enableEdgeToEdge()
48 |
49 | super.onCreate(savedInstanceState)
50 | setContent {
51 | val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
52 | val theme = Theme.fromInt(themeOption)
53 |
54 | val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
55 |
56 | CompositionLocalProvider(LocalTheme provides theme) {
57 | GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
58 | Surface(
59 | modifier = Modifier.fillMaxSize(),
60 | color = MaterialTheme.colorScheme.background
61 | ) {
62 | DeveloperSettingsScreen(onBackPressedDispatcher)
63 | }
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
70 | @OptIn(ExperimentalMaterial3Api::class)
71 | @Composable
72 | fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
73 | val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
74 |
75 | Scaffold(
76 | modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
77 | topBar = {
78 | Column {
79 | TopAppBar(
80 | navigationIcon = {
81 | IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
82 | Icon(
83 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
84 | contentDescription = stringResource(R.string.back_icon_alt)
85 | )
86 | }
87 | },
88 | title = {
89 | Text(
90 | stringResource(R.string.title_activity_developer_settings),
91 | maxLines = 1,
92 | overflow = TextOverflow.Ellipsis
93 | )
94 | },
95 | scrollBehavior = scrollBehavior,
96 | )
97 | }
98 | }
99 | ) { innerPadding ->
100 | Column(
101 | Modifier
102 | .padding(innerPadding)
103 | .fillMaxWidth()
104 | .verticalScroll(rememberScrollState()),
105 | verticalArrangement = Arrangement.spacedBy(8.dp)
106 | ) {
107 |
108 | val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
109 |
110 | SettingsCard(
111 | title = stringResource(R.string.preference_developer_mode),
112 | preferenceKey = PreferenceUtils.Key.DEVELOPER_MODE,
113 | )
114 |
115 | InlineText(
116 | stringResource(R.string.preference_developer_mode_about)
117 | )
118 |
119 | HorizontalDivider()
120 |
121 | if (developerMode) {
122 | OptionsGroup(title = stringResource(R.string.preference_category_gameplay)) {
123 | SettingsStringCard(
124 | title = stringResource(R.string.preference_launch_arguments_name),
125 | dialogTitle = stringResource(R.string.preference_launch_arguments_set_title),
126 | preferenceKey = PreferenceUtils.Key.LAUNCH_ARGUMENTS,
127 | filterInput = { it.filter { c ->
128 | // if only there was a better way to define this!
129 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(){}<>[]?:;'\"~`-_+=\\| ".contains(c)
130 | }}
131 | )
132 |
133 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
134 | SettingsCard(
135 | title = stringResource(R.string.preference_force_hrr),
136 | preferenceKey = PreferenceUtils.Key.FORCE_HRR,
137 | )
138 | }
139 | /*
140 | SettingsCard(
141 | title = stringResource(R.string.preference_override_exceptions_name),
142 | description = stringResource(R.string.preference_override_exceptions_description),
143 | preferenceKey = PreferenceUtils.Key.CUSTOM_SYMBOL_LIST
144 | )
145 | */
146 | }
147 |
148 | OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
149 | SettingsCard(
150 | title = stringResource(R.string.preference_use_index_update_api),
151 | preferenceKey = PreferenceUtils.Key.USE_INDEX_API
152 | )
153 |
154 | SettingsSelectCard(
155 | title = stringResource(R.string.preference_release_channel_tag_name),
156 | dialogTitle = stringResource(R.string.preference_release_channel_select),
157 | preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL_TAG,
158 | options = linkedMapOf(
159 | 0 to stringResource(R.string.preference_release_channel_stable),
160 | 1 to stringResource(R.string.preference_release_channel_beta),
161 | 2 to stringResource(R.string.preference_release_channel_nightly),
162 | )
163 | )
164 |
165 | ElevatedCard(modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
166 | LabelledText(
167 | label = stringResource(R.string.preference_developer_update_notice),
168 | icon = {
169 | Icon(Icons.Filled.Info, null)
170 | },
171 | modifier = Modifier.padding(8.dp)
172 | )
173 | }
174 | }
175 |
176 | OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
177 | SettingsCard(
178 | title = stringResource(R.string.preference_enable_redesign),
179 | preferenceKey = PreferenceUtils.Key.ENABLE_REDESIGN,
180 | )
181 | }
182 |
183 | OptionsGroup(title = stringResource(R.string.preference_profiles)) {
184 | ProfileCreateCard()
185 | ProfileSelectCard()
186 | }
187 | }
188 | }
189 | }
190 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.runtime.compositionLocalOf
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.graphics.toArgb
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.platform.LocalView
18 | import androidx.core.view.ViewCompat
19 | import androidx.core.view.WindowCompat
20 |
21 | private val DarkColorScheme = darkColorScheme(
22 | primary = Purple80,
23 | secondary = PurpleGrey80,
24 | tertiary = Pink80
25 | )
26 |
27 | private val LightColorScheme = lightColorScheme(
28 | primary = Purple40,
29 | secondary = PurpleGrey40,
30 | tertiary = Pink40
31 |
32 | /* Other default colors to override
33 | background = Color(0xFFFFFBFE),
34 | surface = Color(0xFFFFFBFE),
35 | onPrimary = Color.White,
36 | onSecondary = Color.White,
37 | onTertiary = Color.White,
38 | onBackground = Color(0xFF1C1B1F),
39 | onSurface = Color(0xFF1C1B1F),
40 | */
41 | )
42 |
43 | enum class Theme {
44 | LIGHT, DARK;
45 |
46 | companion object {
47 | @Composable
48 | fun fromInt(value: Int) = when (value) {
49 | 1 -> LIGHT
50 | 2 -> DARK
51 | else -> if (isSystemInDarkTheme()) DARK else LIGHT
52 | }
53 | }
54 | }
55 |
56 | val LocalTheme = compositionLocalOf { Theme.LIGHT }
57 |
58 | @Composable
59 | fun GeodeLauncherTheme(
60 | theme: Theme = Theme.fromInt(0),
61 | // Dynamic color is available on Android 12+
62 | dynamicColor: Boolean = true,
63 | blackBackground: Boolean = false,
64 | content: @Composable () -> Unit
65 | ) {
66 | val colorScheme = when {
67 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
68 | val context = LocalContext.current
69 | when {
70 | theme == Theme.DARK && blackBackground ->
71 | dynamicDarkColorScheme(context).copy(surface = Color.Black, background = Color.Black)
72 | theme == Theme.DARK -> dynamicDarkColorScheme(context)
73 | else -> dynamicLightColorScheme(context)
74 | }
75 | }
76 | theme == Theme.DARK && blackBackground ->
77 | DarkColorScheme.copy(surface = Color.Black, background = Color.Black)
78 | theme == Theme.DARK -> DarkColorScheme
79 | else -> LightColorScheme
80 | }
81 |
82 | MaterialTheme(
83 | colorScheme = colorScheme,
84 | typography = Typography,
85 | content = content
86 | )
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | ),
18 | titleMedium = TextStyle(
19 | fontFamily = FontFamily.Default,
20 | fontWeight = FontWeight.SemiBold,
21 | fontSize = 18.sp,
22 | letterSpacing = 0.5.sp
23 | )
24 | /* Other default text styles to override
25 | titleLarge = TextStyle(
26 | fontFamily = FontFamily.Default,
27 | fontWeight = FontWeight.Normal,
28 | fontSize = 22.sp,
29 | lineHeight = 28.sp,
30 | letterSpacing = 0.sp
31 | ),
32 | labelSmall = TextStyle(
33 | fontFamily = FontFamily.Default,
34 | fontWeight = FontWeight.Medium,
35 | fontSize = 11.sp,
36 | lineHeight = 16.sp,
37 | letterSpacing = 0.5.sp
38 | )
39 | */
40 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/updater/Release.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.updater
2 |
3 | import com.geode.launcher.utils.LaunchUtils
4 | import kotlinx.datetime.Instant
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class Asset(
9 | val url: String,
10 | val id: Int,
11 | val name: String,
12 | val size: Int,
13 | val createdAt: Instant,
14 | val updatedAt: Instant,
15 | val browserDownloadUrl: String,
16 | )
17 |
18 | @Serializable
19 | data class Release(
20 | val url: String,
21 | val id: Int,
22 | val targetCommitish: String,
23 | val tagName: String,
24 | val createdAt: Instant,
25 | val publishedAt: Instant,
26 | val assets: List,
27 | val htmlUrl: String,
28 | val body: String?,
29 | val name: String?,
30 | )
31 |
32 | @Serializable
33 | data class LoaderVersion(
34 | val tag: String,
35 | val version: String,
36 | val createdAt: Instant,
37 | val commitHash: String,
38 | val prerelease: Boolean
39 | )
40 |
41 | @Serializable
42 | data class LoaderPayload(
43 | val payload: T?,
44 | val error: String
45 | )
46 |
47 | data class DownloadableAsset(val url: String, val filename: String, val size: Long? = null)
48 |
49 | abstract class Downloadable {
50 | abstract fun getDescription(): String
51 | abstract fun getDescriptor(): Long
52 | abstract fun getDownload(): DownloadableAsset?
53 | }
54 |
55 | class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadable() {
56 | override fun getDescription(): String {
57 | if (release.tagName == "nightly") {
58 | // get the commit from the assets
59 | // otherwise, a separate request is needed to get the hash (ew)
60 | val asset = release.assets.first()
61 | val commit = asset.name.substring(6..12)
62 |
63 | return "nightly-$commit"
64 | }
65 |
66 | return release.tagName
67 | }
68 |
69 | override fun getDescriptor(): Long {
70 | return release.createdAt.epochSeconds
71 | }
72 |
73 | private fun getGitHubDownload(): Asset? {
74 | // try to find an asset that matches the architecture first
75 | val platform = LaunchUtils.platformName
76 |
77 | val releaseSuffix = "$platform.zip"
78 | return release.assets.find {
79 | it.name.endsWith(releaseSuffix)
80 | }
81 | }
82 |
83 | override fun getDownload(): DownloadableAsset? {
84 | val download = getGitHubDownload() ?: return null
85 | return DownloadableAsset(
86 | url = download.browserDownloadUrl,
87 | filename = download.name,
88 | size = download.size.toLong()
89 | )
90 | }
91 | }
92 |
93 | class DownloadableLauncherRelease(val release: Release) : Downloadable() {
94 | override fun getDescription(): String {
95 | return release.tagName
96 | }
97 |
98 | override fun getDescriptor(): Long {
99 | return release.createdAt.epochSeconds
100 | }
101 |
102 | private fun getGitHubDownload(): Asset? {
103 | val platform = LaunchUtils.platformName
104 | val use32BitPlatform = platform == "android32"
105 |
106 | return release.assets.find { asset ->
107 | /* you know it's good when you pull out the truth table
108 | * u32bp | contains | found
109 | * 1 | 1 | 1
110 | * 0 | 1 | 0
111 | * 1 | 0 | 0
112 | * 0 | 0 | 1
113 | * surprise! it's an xnor
114 | */
115 |
116 | (asset.name.contains("android32")) == use32BitPlatform
117 | }
118 | }
119 |
120 | override fun getDownload(): DownloadableAsset? {
121 | val download = getGitHubDownload() ?: return null
122 | return DownloadableAsset(
123 | url = download.browserDownloadUrl,
124 | filename = download.name,
125 | size = download.size.toLong()
126 | )
127 | }
128 | }
129 |
130 | class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadable() {
131 | override fun getDescription(): String {
132 | return version.tag
133 | }
134 |
135 | override fun getDescriptor(): Long {
136 | return version.createdAt.epochSeconds
137 | }
138 |
139 | override fun getDownload(): DownloadableAsset? {
140 | val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
141 | return DownloadableAsset(
142 | url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename",
143 | filename = filename
144 | )
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/updater/ReleaseRepository.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.updater
2 |
3 | import com.geode.launcher.utils.DownloadUtils.executeCoroutine
4 | import kotlinx.datetime.Clock
5 | import kotlinx.datetime.DateTimeUnit
6 | import kotlinx.datetime.Instant
7 | import kotlinx.datetime.until
8 | import kotlinx.serialization.ExperimentalSerializationApi
9 | import kotlinx.serialization.json.Json
10 | import kotlinx.serialization.json.JsonNamingStrategy
11 | import kotlinx.serialization.json.okio.decodeFromBufferedSource
12 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
13 | import okhttp3.OkHttpClient
14 | import okhttp3.Request
15 | import java.io.IOException
16 | import java.net.URL
17 |
18 | class ReleaseRepository(private val httpClient: OkHttpClient) {
19 | companion object {
20 | private const val GITHUB_API_BASE = "https://api.github.com"
21 | private const val GITHUB_API_HEADER = "X-GitHub-Api-Version"
22 | private const val GITHUB_API_VERSION = "2022-11-28"
23 |
24 | private const val GITHUB_RATELIMIT_REMAINING = "x-ratelimit-remaining"
25 | private const val GITHUB_RATELIMIT_RESET = "x-ratelimit-reset"
26 |
27 | private const val GEODE_API_BASE = "https://api.geode-sdk.org/v1"
28 | }
29 |
30 | suspend fun getLatestLauncherRelease(): DownloadableLauncherRelease? {
31 | val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
32 |
33 | val url = URL(releasePath)
34 |
35 | return getReleaseByUrl(url)?.let(::DownloadableLauncherRelease)
36 | }
37 |
38 | suspend fun getLatestGeodeRelease(): DownloadableGitHubLoaderRelease? {
39 | val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases/latest"
40 | val url = URL(releasePath)
41 |
42 | return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
43 | }
44 |
45 | suspend fun getReleaseByTag(tag: String): DownloadableGitHubLoaderRelease? {
46 | val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases/tags/$tag"
47 | val url = URL(releasePath)
48 |
49 | return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
50 | }
51 |
52 | @OptIn(ExperimentalSerializationApi::class)
53 | suspend fun getLatestGeodePreRelease(): DownloadableGitHubLoaderRelease? {
54 | val releasesUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases?per_page=2"
55 | val url = URL(releasesUrl)
56 |
57 | val request = Request.Builder()
58 | .url(url)
59 | .addHeader("Accept", "application/json")
60 | .addHeader(GITHUB_API_HEADER, GITHUB_API_VERSION)
61 | .build()
62 |
63 | val call = httpClient.newCall(request)
64 | val response = call.executeCoroutine()
65 |
66 | return when (response.code) {
67 | 200 -> {
68 | val format = Json {
69 | namingStrategy = JsonNamingStrategy.SnakeCase
70 | ignoreUnknownKeys = true
71 | }
72 |
73 | val release = format.decodeFromBufferedSource>(
74 | response.body!!.source()
75 | )
76 |
77 | (release.find { it.tagName != "nightly" })
78 | ?.let(::DownloadableGitHubLoaderRelease)
79 | }
80 | 403 -> {
81 | // determine if the error code is a ratelimit
82 | // (github docs say it sends 429 too, but haven't seen that)
83 |
84 | val limitRemaining = response.headers.get(GITHUB_RATELIMIT_REMAINING)?.toInt()
85 | val limitReset = response.headers.get(GITHUB_RATELIMIT_RESET)?.toLong()
86 |
87 | if (limitRemaining == 0 && limitReset != null) {
88 | // handle ratelimit with a custom error
89 | // there's also a retry-after header but again, haven't seen
90 | val resetTime = Instant.fromEpochSeconds(limitReset, 0L)
91 | val msg = generateRateLimitMessage(resetTime)
92 |
93 | throw IOException(msg)
94 | }
95 |
96 | throw IOException("response 403: ${response.body!!.string()}")
97 | }
98 | 404 -> {
99 | null
100 | }
101 | else -> {
102 | throw IOException("unknown response ${response.code}")
103 | }
104 | }
105 | }
106 |
107 | private fun generateRateLimitMessage(resetTime: Instant): String {
108 | val currentTime = Clock.System.now()
109 | val resetDelay = currentTime.until(resetTime, DateTimeUnit.SECOND)
110 |
111 | val formattedWait = "${resetDelay / 60}m"
112 |
113 | return "api ratelimit reached, try again in ${formattedWait}"
114 | }
115 |
116 | @OptIn(ExperimentalSerializationApi::class)
117 | private suspend fun getReleaseByUrl(url: URL): Release? {
118 | val request = Request.Builder()
119 | .url(url)
120 | .addHeader("Accept", "application/json")
121 | .addHeader(GITHUB_API_HEADER, GITHUB_API_VERSION)
122 | .build()
123 |
124 | val call = httpClient.newCall(request)
125 | val response = call.executeCoroutine()
126 |
127 | return when (response.code) {
128 | 200 -> {
129 | val format = Json {
130 | namingStrategy = JsonNamingStrategy.SnakeCase
131 | ignoreUnknownKeys = true
132 | }
133 |
134 | val release = format.decodeFromBufferedSource(
135 | response.body!!.source()
136 | )
137 |
138 | release
139 | }
140 | 403 -> {
141 | // determine if the error code is a ratelimit
142 | // (github docs say it sends 429 too, but haven't seen that)
143 |
144 | val limitRemaining = response.headers[GITHUB_RATELIMIT_REMAINING]?.toInt()
145 | val limitReset = response.headers[GITHUB_RATELIMIT_RESET]?.toLong()
146 |
147 | if (limitRemaining == 0 && limitReset != null) {
148 | // handle ratelimit with a custom error
149 | // there's also a retry-after header but again, haven't seen
150 | val resetTime = Instant.fromEpochSeconds(limitReset, 0L)
151 | val msg = generateRateLimitMessage(resetTime)
152 |
153 | throw IOException(msg)
154 | }
155 |
156 | throw IOException("response 403: ${response.body!!.string()}")
157 | }
158 | 404 -> {
159 | null
160 | }
161 | else -> {
162 | throw IOException("unknown response ${response.code}")
163 | }
164 | }
165 | }
166 |
167 | @OptIn(ExperimentalSerializationApi::class)
168 | suspend fun getLatestIndexRelease(gd: Long, prerelease: Boolean = false): DownloadableLoaderRelease? {
169 | val url = "$GEODE_API_BASE/loader/versions/latest".toHttpUrlOrNull()!!
170 | .newBuilder()
171 | .addQueryParameter("gd", gd.toString())
172 | .addQueryParameter("platform", "android")
173 | .addQueryParameter("prerelease", prerelease.toString())
174 | .build()
175 |
176 | val request = Request.Builder()
177 | .url(url)
178 | .addHeader("Accept", "application/json")
179 | .build()
180 |
181 | val call = httpClient.newCall(request)
182 | val response = call.executeCoroutine()
183 |
184 | return when (response.code) {
185 | 200 -> {
186 | val format = Json {
187 | namingStrategy = JsonNamingStrategy.SnakeCase
188 | ignoreUnknownKeys = true
189 | }
190 |
191 | val body = format.decodeFromBufferedSource>(
192 | response.body!!.source()
193 | )
194 |
195 | val release = body.payload ?: throw IOException("index error: ${body.error}")
196 |
197 | DownloadableLoaderRelease(release)
198 | }
199 | 404 -> {
200 | null
201 | }
202 | else -> {
203 | throw IOException("unknown response ${response.code}")
204 | }
205 | }
206 | }
207 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/updater/ReleaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.updater
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.ViewModelProvider
6 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
7 | import androidx.lifecycle.viewModelScope
8 | import androidx.lifecycle.viewmodel.initializer
9 | import androidx.lifecycle.viewmodel.viewModelFactory
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.StateFlow
13 | import kotlinx.coroutines.flow.asStateFlow
14 | import kotlinx.coroutines.flow.map
15 | import kotlinx.coroutines.launch
16 |
17 | class ReleaseViewModel(private val application: Application): ViewModel() {
18 | companion object {
19 | val Factory: ViewModelProvider.Factory = viewModelFactory {
20 | initializer {
21 | val application = this[APPLICATION_KEY] as Application
22 |
23 | ReleaseViewModel(
24 | application = application
25 | )
26 | }
27 | }
28 | }
29 |
30 | sealed class ReleaseUIState {
31 | data object InUpdateCheck : ReleaseUIState()
32 | data class Failure(val exception: Exception) : ReleaseUIState()
33 | data class InDownload(val downloaded: Long, val outOf: Long?) : ReleaseUIState()
34 | data class Finished(val hasUpdated: Boolean = false) : ReleaseUIState()
35 | data class Cancelled(val isCancelling: Boolean = false) : ReleaseUIState()
36 |
37 | companion object {
38 | fun managerStateToUI(state: ReleaseManager.ReleaseManagerState): ReleaseUIState {
39 | return when (state) {
40 | is ReleaseManager.ReleaseManagerState.InUpdateCheck -> InUpdateCheck
41 | is ReleaseManager.ReleaseManagerState.Failure -> Failure(state.exception)
42 | is ReleaseManager.ReleaseManagerState.InDownload ->
43 | InDownload(state.downloaded, state.outOf)
44 | is ReleaseManager.ReleaseManagerState.Finished -> Finished(state.hasUpdated)
45 | }
46 | }
47 | }
48 | }
49 |
50 | private val _uiState = MutableStateFlow(ReleaseUIState.Finished())
51 | val uiState = _uiState.asStateFlow()
52 |
53 | val isInUpdate
54 | get() = ReleaseManager.get(application).isInUpdate
55 |
56 | val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
57 |
58 | var hasPerformedCheck = false
59 | private set
60 |
61 | fun cancelUpdate() {
62 | viewModelScope.launch {
63 | _uiState.value = ReleaseUIState.Cancelled(true)
64 |
65 | ReleaseManager.get(application).cancelUpdate()
66 |
67 | _uiState.value = ReleaseUIState.Cancelled()
68 | }
69 | }
70 |
71 | private suspend fun syncUiState(
72 | flow: StateFlow
73 | ) {
74 | flow.map(ReleaseUIState::managerStateToUI).collect {
75 | // send the mapped state to the ui
76 | _uiState.value = it
77 | }
78 | }
79 |
80 | fun useGlobalCheckState() {
81 | hasPerformedCheck = true
82 |
83 | viewModelScope.launch(Dispatchers.IO) {
84 | val releaseFlow = ReleaseManager.get(application)
85 | .uiState
86 |
87 | syncUiState(releaseFlow)
88 | }
89 | }
90 |
91 | fun runReleaseCheck(isManual: Boolean = false) {
92 | hasPerformedCheck = true
93 |
94 | viewModelScope.launch(Dispatchers.IO) {
95 | val releaseFlow = ReleaseManager.get(application)
96 | .checkForUpdates(isManual)
97 |
98 | syncUiState(releaseFlow)
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/utils/Components.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.utils
2 |
3 | import androidx.compose.foundation.layout.Spacer
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.text.InlineTextContent
6 | import androidx.compose.foundation.text.appendInlineContent
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.text.Placeholder
11 | import androidx.compose.ui.text.PlaceholderVerticalAlign
12 | import androidx.compose.ui.text.buildAnnotatedString
13 | import androidx.compose.ui.unit.sp
14 |
15 | @Composable
16 | fun LabelledText(label: String, icon: @Composable (() -> Unit)? = null, modifier: Modifier = Modifier) {
17 | if (icon != null) {
18 | val iconId = "inlineIcon"
19 | val paddingId = "paddingIcon"
20 |
21 | val labelText = buildAnnotatedString {
22 | appendInlineContent(iconId, "[x]")
23 | appendInlineContent(paddingId, " ")
24 | append(label)
25 | }
26 |
27 | val inlineContent = mapOf(
28 | Pair(iconId, InlineTextContent(
29 | Placeholder(
30 | width = 24.sp,
31 | height = 24.sp,
32 | placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter
33 | )
34 | ) {
35 | icon()
36 | }),
37 | Pair(paddingId, InlineTextContent(Placeholder(width = 6.sp, height = 24.sp, placeholderVerticalAlign = PlaceholderVerticalAlign.Center)) {
38 | Spacer(modifier = Modifier.fillMaxWidth())
39 | })
40 | )
41 |
42 | Text(text = labelText, inlineContent = inlineContent, modifier = modifier)
43 | } else {
44 | Text(label, modifier = modifier)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.utils
2 |
3 | import android.annotation.SuppressLint
4 |
5 | object Constants {
6 | const val PACKAGE_NAME = "com.robtopx.geometryjump"
7 |
8 | const val COCOS_LIB_NAME = "cocos2dcpp"
9 | const val FMOD_LIB_NAME = "fmod"
10 | const val LAUNCHER_FIX_LIB_NAME = "launcherfix"
11 |
12 | // this value is hardcoded into GD
13 | @SuppressLint("SdCardPath")
14 | const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}"
15 |
16 | // anything below this version code is blocked from opening the game
17 | const val SUPPORTED_VERSION_CODE_MIN = 37L
18 |
19 | // anything below this version code shows a warning on the main menu
20 | const val SUPPORTED_VERSION_CODE_MIN_WARNING = 40L
21 |
22 | // anything above this version code will show a warning when starting the game
23 | const val SUPPORTED_VERSION_CODE = 40L
24 |
25 | const val SUPPORTED_VERSION_STRING = "2.2.143"
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/utils/ConstrainedFrameLayout.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.utils
2 |
3 | import android.content.Context
4 | import android.widget.FrameLayout
5 | import kotlin.math.ceil
6 | import kotlin.math.floor
7 | import kotlin.math.min
8 |
9 | class ConstrainedFrameLayout(context: Context) : FrameLayout(context) {
10 | // avoid running the aspect ratio fix when we don't need it
11 | var aspectRatio: Float? = null
12 | var zoom: Float? = null
13 | var fitZoom: Boolean = false
14 |
15 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
16 | var paddingX = 0.0f
17 | var paddingY = 0.0f
18 |
19 | val currentZoom = zoom
20 | if (currentZoom != null) {
21 | val width = MeasureSpec.getSize(widthMeasureSpec)
22 | val height = MeasureSpec.getSize(heightMeasureSpec)
23 | if (width == 0 || height == 0) {
24 | super.onMeasure(widthMeasureSpec, heightMeasureSpec)
25 | return
26 | }
27 |
28 | val calculatedWidth = width * currentZoom
29 | val calculatedHeight = height * currentZoom
30 |
31 | paddingX = (width - calculatedWidth)/2
32 | paddingY = (height - calculatedHeight)/2
33 | }
34 |
35 | val currentAspectRatio = aspectRatio
36 | if (currentAspectRatio != null) {
37 | val width = MeasureSpec.getSize(widthMeasureSpec)
38 | val height = MeasureSpec.getSize(heightMeasureSpec)
39 | if (width == 0 || height == 0) {
40 | super.onMeasure(widthMeasureSpec, heightMeasureSpec)
41 | return
42 | }
43 |
44 | val calculatedWidth = (height - paddingY * 2) * currentAspectRatio
45 | val padding = (width - calculatedWidth) / 2
46 |
47 | paddingX = padding
48 | }
49 |
50 | if (paddingX > 0.0 || paddingY > 0.0) {
51 | if (fitZoom) {
52 | val width = MeasureSpec.getSize(widthMeasureSpec)
53 | val height = MeasureSpec.getSize(heightMeasureSpec)
54 |
55 | val calculatedHeight = (height - paddingY * 2)
56 | val calculatedWidth = (width - paddingX * 2)
57 |
58 | val zoomFactor = min(height / calculatedHeight, width / calculatedWidth)
59 |
60 | scaleX = zoomFactor
61 | scaleY = zoomFactor
62 | }
63 |
64 | setPadding(
65 | ceil(paddingX).toInt(),
66 | ceil(paddingY).toInt(),
67 | floor(paddingX).toInt(),
68 | floor(paddingY).toInt()
69 | )
70 | }
71 |
72 | super.onMeasure(
73 | widthMeasureSpec,
74 | heightMeasureSpec
75 | )
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/utils/DownloadUtils.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.utils
2 |
3 | import android.os.Build
4 | import android.os.FileUtils
5 | import kotlinx.coroutines.runInterruptible
6 | import kotlinx.coroutines.suspendCancellableCoroutine
7 | import okhttp3.Call
8 | import okhttp3.Callback
9 | import okhttp3.MediaType
10 | import okhttp3.OkHttpClient
11 | import okhttp3.Request
12 | import okhttp3.Response
13 | import okhttp3.ResponseBody
14 | import okio.Buffer
15 | import okio.BufferedSource
16 | import okio.ForwardingSource
17 | import okio.Source
18 | import okio.buffer
19 | import java.io.IOException
20 | import java.io.InputStream
21 | import java.io.OutputStream
22 | import java.util.concurrent.TimeUnit
23 | import java.util.zip.ZipInputStream
24 | import kotlin.coroutines.resume
25 | import kotlin.coroutines.resumeWithException
26 |
27 |
28 | typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit
29 |
30 | object DownloadUtils {
31 | suspend fun downloadStream(
32 | httpClient: OkHttpClient,
33 | url: String,
34 | onProgress: ProgressCallback? = null
35 | ): InputStream {
36 | val request = Request.Builder()
37 | .url(url)
38 | .build()
39 |
40 | // build a new client using the same pool as the old client
41 | // (more efficient)
42 | val progressClientBuilder = httpClient.newBuilder()
43 | .cache(null)
44 | // disable timeout
45 | .readTimeout(0, TimeUnit.SECONDS)
46 |
47 | // add progress listener
48 | if (onProgress != null) {
49 | progressClientBuilder.addNetworkInterceptor { chain ->
50 | val originalResponse = chain.proceed(chain.request())
51 | originalResponse.newBuilder()
52 | .body(ProgressResponseBody(
53 | originalResponse.body!!, onProgress
54 | ))
55 | .build()
56 | }
57 | }
58 |
59 | val progressClient = progressClientBuilder.build()
60 |
61 | val call = progressClient.newCall(request)
62 | val response = call.executeCoroutine()
63 |
64 | return response.body!!.byteStream()
65 | }
66 |
67 | suspend fun Call.executeCoroutine(): Response {
68 | return suspendCancellableCoroutine { continuation ->
69 | this.enqueue(object : Callback {
70 | override fun onFailure(call: Call, e: IOException) {
71 | if (continuation.isCancelled) {
72 | return
73 | }
74 |
75 | continuation.resumeWithException(e)
76 | }
77 |
78 | override fun onResponse(call: Call, response: Response) {
79 | continuation.resume(response)
80 | }
81 | })
82 |
83 | continuation.invokeOnCancellation {
84 | this.cancel()
85 | }
86 | }
87 | }
88 |
89 | suspend fun extractFileFromZipStream(inputStream: InputStream, outputStream: OutputStream, zipPath: String) {
90 | // note to self: ZipInputStreams are a little silly
91 | // (runInterruptible allows it to cancel, otherwise it waits for the stream to finish)
92 | runInterruptible {
93 | inputStream.use {
94 | outputStream.use {
95 | // nice indentation
96 | val zip = ZipInputStream(inputStream)
97 | zip.use {
98 | var entry = it.nextEntry
99 | while (entry != null) {
100 | if (entry.name == zipPath) {
101 | it.copyTo(outputStream)
102 | return@runInterruptible
103 | }
104 |
105 | entry = it.nextEntry
106 | }
107 |
108 | // no matching entries found, throw exception
109 | throw IOException("no file found for $zipPath")
110 | }
111 | }
112 | }
113 | }
114 | }
115 |
116 | fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
117 | // gotta love copying
118 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
119 | inputStream.use { input -> outputStream.use { output ->
120 | FileUtils.copy(input, output)
121 | }}
122 | } else {
123 | inputStream.use { input -> outputStream.use { output ->
124 | input.copyTo(output)
125 | }}
126 | }
127 | }
128 | }
129 |
130 | // based on
131 | // lazily ported to kotlin
132 | private class ProgressResponseBody (
133 | private val responseBody: ResponseBody,
134 | private val progressCallback: ProgressCallback
135 | ) : ResponseBody() {
136 | private val bufferedSource: BufferedSource by lazy {
137 | source(responseBody.source()).buffer()
138 | }
139 |
140 | override fun contentType(): MediaType? {
141 | return responseBody.contentType()
142 | }
143 |
144 | override fun contentLength(): Long {
145 | return responseBody.contentLength()
146 | }
147 |
148 | override fun source(): BufferedSource {
149 | return bufferedSource
150 | }
151 |
152 | private fun source(source: Source): Source {
153 | return object : ForwardingSource(source) {
154 | var totalBytesRead = 0L
155 |
156 | override fun read(sink: Buffer, byteCount: Long): Long {
157 | val bytesRead = super.read(sink, byteCount)
158 | totalBytesRead += if (bytesRead != -1L) bytesRead else 0
159 |
160 | progressCallback(
161 | totalBytesRead,
162 | responseBody.contentLength()
163 | )
164 |
165 | return bytesRead
166 | }
167 | }
168 | }
169 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/utils/FileUtils.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.utils
2 |
3 | import android.content.ContentUris
4 | import android.content.Context
5 | import android.database.Cursor
6 | import android.net.Uri
7 | import android.os.Environment
8 | import android.provider.DocumentsContract
9 | import android.provider.MediaStore
10 | import java.io.File
11 |
12 | class FileUtils {
13 | // copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
14 | // i am actually very lazy to move this to a separate class
15 | // hello zmx i moved it
16 | companion object {
17 | fun getRealPathFromURI(context: Context, uri: Uri): String? {
18 | when {
19 | // DocumentProvider
20 | DocumentsContract.isDocumentUri(context, uri) -> {
21 | when {
22 | // ExternalStorageProvider
23 | isExternalStorageDocument(uri) -> {
24 | return handleExternalStorageDocument(context, uri)
25 | }
26 | isDownloadsDocument(uri) -> {
27 | return handleDownloadsDocument(context, uri)
28 | }
29 | isMediaDocument(uri) -> {
30 | return handleMediaDocument(context, uri)
31 | }
32 |
33 | // TODO: add the geode provider
34 | }
35 | }
36 | "content".equals(uri.scheme, ignoreCase = true) -> {
37 | // Return the remote address
38 | if (isGooglePhotosUri(uri)) return uri.lastPathSegment
39 | // This happened with a directory
40 | if (isExternalStorageDocument(uri)) {
41 | return handleExternalStorageDocument(context, uri)
42 | }
43 | return getDataColumn(context, uri, null, null)
44 | }
45 | "file".equals(uri.scheme, ignoreCase = true) -> {
46 | return uri.path
47 | }
48 | }
49 | return null
50 | }
51 |
52 | private fun handleMediaDocument(context: Context, uri: Uri): String? {
53 | val docId = DocumentsContract.getDocumentId(uri)
54 | val split = docId.split(":").toTypedArray()
55 | val type = split[0]
56 | var contentUri: Uri? = null
57 | when (type) {
58 | "image" -> {
59 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
60 | }
61 | "video" -> {
62 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
63 | }
64 | "audio" -> {
65 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
66 | }
67 | }
68 | val selection = "_id=?"
69 | val selectionArgs = arrayOf(split[1])
70 | return getDataColumn(
71 | context,
72 | contentUri,
73 | selection,
74 | selectionArgs
75 | )
76 | }
77 |
78 | private fun handleDownloadsDocument(context: Context, uri: Uri): String? {
79 | val fileName = getFilePath(context, uri)
80 | if (fileName != null) {
81 | return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
82 | }
83 | var id = DocumentsContract.getDocumentId(uri)
84 | if (id.startsWith("raw:")) {
85 | id = id.replaceFirst("raw:".toRegex(), "")
86 | val file = File(id)
87 | if (file.exists()) return id
88 | }
89 | val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
90 | return getDataColumn(context, contentUri, null, null)
91 | }
92 |
93 | private fun handleExternalStorageDocument(context: Context, uri: Uri): String {
94 | val docId: String = if (DocumentsContract.isDocumentUri(context, uri)) {
95 | DocumentsContract.getDocumentId(uri)
96 | } else {
97 | // please please please PLEASE let this be the only other case
98 | DocumentsContract.getTreeDocumentId(uri)
99 | }
100 | val split = docId.split(":").toTypedArray()
101 | val type = split[0]
102 | // This is for checking Main Memory
103 | return if ("primary".equals(type, ignoreCase = true)) {
104 | if (split.size > 1) {
105 | Environment.getExternalStorageDirectory().toString() + "/" + split[1]
106 | } else {
107 | Environment.getExternalStorageDirectory().toString() + "/"
108 | }
109 | // This is for checking SD Card
110 | } else {
111 | "storage" + "/" + docId.replace(":", "/")
112 | }
113 | }
114 | private fun getFilePath(context: Context, uri: Uri?): String? {
115 | var cursor: Cursor? = null
116 | val projection = arrayOf(
117 | MediaStore.MediaColumns.DISPLAY_NAME
118 | )
119 | try {
120 | if (uri == null) return null
121 | cursor = context.contentResolver.query(
122 | uri, projection, null, null,
123 | null
124 | )
125 | if (cursor != null && cursor.moveToFirst()) {
126 | val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
127 | val data = cursor.getString(index)
128 | cursor.close()
129 | return data
130 | }
131 | } catch (e: Exception) {
132 | cursor?.close()
133 | } finally {
134 | cursor?.close()
135 | }
136 | return null
137 | }
138 |
139 | private fun getDataColumn(context: Context, uri: Uri?, selection: String?,
140 | selectionArgs: Array?): String? {
141 | var cursor: Cursor? = null
142 | val column = "_data"
143 | val projection = arrayOf(
144 | column
145 | )
146 | try {
147 | if (uri == null) return null
148 | cursor = context.contentResolver.query(
149 | uri, projection, selection, selectionArgs,
150 | null
151 | )
152 | if (cursor != null && cursor.moveToFirst()) {
153 | val index = cursor.getColumnIndexOrThrow(column)
154 | val data = cursor.getString(index)
155 | cursor.close()
156 | return data
157 | }
158 | } catch (e: Exception) {
159 | cursor?.close()
160 | }
161 | return null
162 | }
163 |
164 | /**
165 | * @param uri The Uri to check.
166 | * @return Whether the Uri authority is ExternalStorageProvider.
167 | */
168 | private fun isExternalStorageDocument(uri: Uri): Boolean {
169 | return "com.android.externalstorage.documents" == uri.authority
170 | }
171 |
172 | /**
173 | * @param uri The Uri to check.
174 | * @return Whether the Uri authority is DownloadsProvider.
175 | */
176 | private fun isDownloadsDocument(uri: Uri): Boolean {
177 | return "com.android.providers.downloads.documents" == uri.authority
178 | }
179 |
180 | /**
181 | * @param uri The Uri to check.
182 | * @return Whether the Uri authority is MediaProvider.
183 | */
184 | private fun isMediaDocument(uri: Uri): Boolean {
185 | return "com.android.providers.media.documents" == uri.authority
186 | }
187 |
188 | /**
189 | * @param uri The Uri to check.
190 | * @return Whether the Uri authority is Google Photos.
191 | */
192 | private fun isGooglePhotosUri(uri: Uri): Boolean {
193 | return "com.google.android.apps.photos.content" == uri.authority
194 | }
195 | }
196 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/utils/GamePackageUtils.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.utils
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.pm.ApplicationInfo
6 | import android.content.pm.PackageInfo
7 | import android.content.pm.PackageManager
8 | import android.content.res.AssetManager
9 | import android.os.Build
10 | import android.util.Log
11 | import okio.ByteString.Companion.toByteString
12 |
13 | object GamePackageUtils {
14 | fun isGameInstalled(packageManager: PackageManager): Boolean {
15 | return try {
16 | packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
17 | true
18 | } catch (e: PackageManager.NameNotFoundException) {
19 | false
20 | }
21 | }
22 |
23 | fun showDownloadBadge(packageManager: PackageManager) =
24 | !isGameInstalled(packageManager) || !identifyGameLegitimacy(packageManager)
25 |
26 | fun getGameVersionCode(packageManager: PackageManager): Long {
27 | val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
28 |
29 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
30 | game.longVersionCode
31 | } else {
32 | @Suppress("DEPRECATION")
33 | game.versionCode.toLong()
34 | }
35 | }
36 |
37 | fun getGameVersionCodeOrNull(packageManager: PackageManager): Long? {
38 | return try {
39 | getGameVersionCode(packageManager)
40 | } catch (e: PackageManager.NameNotFoundException) {
41 | null
42 | }
43 | }
44 |
45 | fun getGameVersionString(packageManager: PackageManager): String {
46 | val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
47 | return game.versionName ?: "0.000"
48 | }
49 |
50 | private val gameVersionMap = mapOf(
51 | 37L to "2.200",
52 | 38L to "2.205",
53 | 39L to "2.206",
54 | 40L to "2.2074" // ok
55 | )
56 |
57 | /**
58 | * Gets the version name that more closely aligns to the Windows/macOS releases
59 | */
60 | fun getUnifiedVersionName(packageManager: PackageManager): String {
61 | val versionCode = getGameVersionCode(packageManager)
62 | return gameVersionMap[versionCode] ?: getGameVersionString(packageManager)
63 | }
64 |
65 | fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean {
66 | val abi = LaunchUtils.applicationArchitecture
67 | val isExtracted =
68 | packageInfo.applicationInfo!!.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS
69 | val isSplit = (packageInfo.applicationInfo?.splitSourceDirs?.size ?: 0) > 1
70 |
71 | val metadata =
72 | "Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi"
73 | Log.i("GeodeLauncher", metadata)
74 |
75 | // easiest check! these messages are hardcoded in bionic/linker/linker_phdr.cpp
76 | if (loadException.message?.contains("32-bit instead of 64-bit") == true) {
77 | return true
78 | }
79 |
80 | if (loadException.message?.contains("64-bit instead of 32-bit") == true) {
81 | return true
82 | }
83 |
84 | // determine if it can find gd libraries for the opposite architecture
85 | val gdBinaryName = "lib${Constants.COCOS_LIB_NAME}.so"
86 | val oppositeArchitecture = if (abi == "arm64-v8a") "armeabi-v7a" else "arm64-v8a"
87 |
88 | // detect issues using native library directory (only works on extracted libraries)
89 | val nativeLibraryDir = packageInfo.applicationInfo!!.nativeLibraryDir
90 |
91 | if (nativeLibraryDir.contains(oppositeArchitecture)) {
92 | return true
93 | }
94 |
95 | // android has some odd behavior where 32bit libraries get installed to the arm directory instead of armeabi-v7a
96 | // detect it, ig
97 | if (oppositeArchitecture == "armeabi-v7a" && nativeLibraryDir.contains("/lib/arm/")) {
98 | return true
99 | }
100 |
101 | if (!isExtracted) {
102 | try {
103 | context.assets.openNonAssetFd("lib/$oppositeArchitecture/$gdBinaryName")
104 | return true
105 | } catch (_: Exception) {
106 | // this is good, actually!
107 | }
108 | }
109 |
110 | return false
111 | }
112 |
113 | @SuppressLint("DiscouragedPrivateApi")
114 | fun addAssetsFromPackage(assetManager: AssetManager, packageInfo: PackageInfo) {
115 | // this method is officially marked as deprecated but it is the only method we are allowed to reflect
116 | // (the source recommends replacing with AssetManager.setApkAssets(ApkAssets[], boolean) lol)
117 | val clazz = assetManager.javaClass
118 | val aspMethod = clazz.getDeclaredMethod("addAssetPath", String::class.java)
119 |
120 | aspMethod.invoke(assetManager, packageInfo.applicationInfo?.sourceDir)
121 | packageInfo.applicationInfo?.splitSourceDirs?.forEach {
122 | aspMethod.invoke(assetManager, it)
123 | }
124 | }
125 |
126 | private const val GAME_CERTIFICATE_HASH = "f5e7d8284d72c461a5f022d0cf755df101c3bb1e69cbe241bc1aef2cc5610a43"
127 | private const val AMAZON_CERTIFICATE_HASH = "1f228081e4d66e006887d4ba0f3d40e96a80c758b84231f11cd5c1c9aef6048f"
128 |
129 | private fun validateCertificate(certificate: ByteArray): GameSource = when (val hash = certificate.toByteString().sha256().hex()) {
130 | GAME_CERTIFICATE_HASH -> GameSource.GOOGLE
131 | AMAZON_CERTIFICATE_HASH -> GameSource.AMAZON
132 | else -> {
133 | println("Encountered certificate hash: $hash")
134 | GameSource.UNKNOWN
135 | }
136 | }
137 |
138 | enum class GameSource {
139 | UNKNOWN, GOOGLE, AMAZON
140 | }
141 |
142 | fun identifyGameSource(packageManager: PackageManager): GameSource {
143 | @Suppress("DEPRECATION")
144 | val certificates = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
145 | val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, PackageManager.GET_SIGNING_CERTIFICATES)
146 | val signingInfo = game.signingInfo
147 |
148 | signingInfo?.signingCertificateHistory ?: return GameSource.UNKNOWN
149 | } else {
150 | val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, PackageManager.GET_SIGNATURES)
151 | game.signatures
152 | }
153 |
154 | return certificates?.map {
155 | validateCertificate(it.toByteArray())
156 | }?.firstOrNull {
157 | it != GameSource.UNKNOWN
158 | } ?: GameSource.UNKNOWN
159 | }
160 |
161 | fun identifyGameLegitimacy(packageManager: PackageManager) =
162 | identifyGameSource(packageManager) != GameSource.UNKNOWN
163 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/utils/LaunchUtils.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.utils
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import java.io.File
6 | import java.io.Serializable
7 |
8 | object LaunchUtils {
9 | // supposedly CPU_ABI returns the current arch for the running application
10 | // despite being deprecated, this is also one of the few ways to get this information
11 | @Suppress("DEPRECATION")
12 | val applicationArchitecture: String = Build.CPU_ABI
13 |
14 | val is64bit = applicationArchitecture == "arm64-v8a"
15 |
16 | val platformName: String = if (is64bit) "android64" else "android32"
17 |
18 | val geodeFilename: String = "Geode.$platformName.so"
19 |
20 | fun getInstalledGeodePath(context: Context): File? {
21 | val geodeName = geodeFilename
22 |
23 | val internalGeodePath = File(context.filesDir, "launcher/$geodeName")
24 | if (internalGeodePath.exists()) {
25 | return internalGeodePath
26 | }
27 |
28 | val externalGeodeDir = getBaseDirectory(context)
29 |
30 | val updateGeodePath = File(externalGeodeDir, "launcher/$geodeName")
31 | if (updateGeodePath.exists()) {
32 | return updateGeodePath
33 | }
34 |
35 | val externalGeodePath = File(externalGeodeDir, geodeName)
36 | if (externalGeodePath.exists()) {
37 | return externalGeodePath
38 | }
39 |
40 | return null
41 | }
42 |
43 | fun isGeodeInstalled(context: Context): Boolean {
44 | return getInstalledGeodePath(context) != null
45 | }
46 |
47 | /**
48 | * Returns the directory that Geode/the game should base itself off of.
49 | */
50 | fun getBaseDirectory(context: Context, ignoreProfile: Boolean = false): File {
51 | // deprecated, but seems to be the best choice of directory (i forced mat to test it)
52 | // also, is getting the first item the correct choice here?? what do they mean
53 | @Suppress("DEPRECATION")
54 | val dir = context.externalMediaDirs.first()
55 |
56 | // prevent having resources added to system gallery
57 | // accessing this file every time the directory is read may be a little wasteful...
58 | val noMediaPath = File(dir, ".nomedia")
59 | noMediaPath.createNewFile()
60 |
61 | val currentProfile = ProfileManager.get(context).getCurrentProfile()
62 | if (currentProfile != null && !ignoreProfile) {
63 | val profile = File(dir, "profiles/$currentProfile/")
64 | profile.mkdirs()
65 |
66 | return profile
67 | }
68 |
69 | return dir
70 | }
71 |
72 | fun getSaveDirectory(context: Context): File {
73 | return File(getBaseDirectory(context), "save")
74 | }
75 |
76 | private const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
77 |
78 | private fun getCrashDirectory(context: Context): File {
79 | val base = getBaseDirectory(context)
80 | return File(base, "game/geode/crashlogs/")
81 | }
82 |
83 | fun getLastCrash(context: Context): File? {
84 | val crashDirectory = getCrashDirectory(context)
85 | if (!crashDirectory.exists()) {
86 | return null
87 | }
88 |
89 | val children = crashDirectory.listFiles {
90 | // ignore indicator files (including old file)
91 | _, name -> name != CRASH_INDICATOR_NAME && name != "last-pid"
92 | }
93 |
94 | return children?.maxByOrNull { it.lastModified() }
95 | }
96 |
97 | fun lastSessionCrashed(context: Context): Boolean {
98 | val base = getCrashDirectory(context)
99 | val crashIndicatorFile = File(base, CRASH_INDICATOR_NAME)
100 |
101 | return crashIndicatorFile.exists()
102 | }
103 |
104 | enum class LauncherError : Serializable {
105 | LINKER_NEEDS_64BIT,
106 | LINKER_NEEDS_32BIT,
107 | LINKER_FAILED_STL,
108 | LINKER_FAILED,
109 | GENERIC,
110 | CRASHED;
111 |
112 | fun isAbiFailure(): Boolean {
113 | return this == LINKER_NEEDS_32BIT || this == LINKER_NEEDS_64BIT
114 | }
115 | }
116 |
117 | const val LAUNCHER_KEY_RETURN_ERROR = "return_error"
118 | const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message"
119 | const val LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE = "return_extended_message"
120 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/utils/ProfileManager.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.utils
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import androidx.core.content.edit
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 | import kotlinx.serialization.Serializable
9 | import kotlinx.serialization.json.Json
10 |
11 | private const val FILE_KEY = "GeodeLauncherPreferencesFileKey"
12 | private const val PROFILES_FILE_KEY = "GeodeLauncherProfiles"
13 |
14 | private const val PROFILE_LIST_SAVE_KEY = "profiles"
15 | private const val PROFILE_CURRENT_SAVE_KEY = "currentProfile"
16 |
17 | @Serializable
18 | data class Profile(val path: String, val name: String)
19 |
20 | class ProfileManager {
21 | private val applicationContext: Context
22 | private val preferences: SharedPreferences
23 |
24 | companion object {
25 | private lateinit var managerInstance: ProfileManager
26 |
27 | fun get(context: Context): ProfileManager {
28 | if (!Companion::managerInstance.isInitialized) {
29 | val applicationContext = context.applicationContext
30 |
31 | managerInstance = ProfileManager(applicationContext)
32 | }
33 |
34 | return managerInstance
35 | }
36 | }
37 |
38 | constructor(applicationContext: Context) {
39 | this.applicationContext = applicationContext
40 | this.preferences = applicationContext.getSharedPreferences(PROFILES_FILE_KEY, Context.MODE_PRIVATE)
41 |
42 | _storedProfiles.value = getProfiles()
43 | }
44 |
45 | private val _storedProfiles = MutableStateFlow>(emptyList())
46 | val storedProfiles = _storedProfiles.asStateFlow()
47 |
48 | fun getCurrentProfile(): String? {
49 | return preferences.getString(PROFILE_CURRENT_SAVE_KEY, null)
50 | .takeUnless { it.isNullOrEmpty() }
51 | }
52 |
53 | fun getCurrentFileKey(): String {
54 | val currentProfile = getCurrentProfile()
55 | ?: return FILE_KEY
56 |
57 | return "${FILE_KEY}_${currentProfile}"
58 | }
59 |
60 | fun getProfile(path: String): Profile? {
61 | val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null)
62 | return profilesSet?.firstNotNullOfOrNull {
63 | Json.decodeFromString(it).takeIf { it.path == path }
64 | }
65 | }
66 |
67 | /**
68 | * Store a profile into the save, removing any duplicate profiles beforehand
69 | */
70 | fun storeProfile(profile: Profile) {
71 | val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet()
72 |
73 | val removedList = profilesSet.filterTo(LinkedHashSet()) {
74 | val saveProfile = Json.decodeFromString(it)
75 | saveProfile.path != profile.path
76 | }
77 | removedList.add(Json.encodeToString(profile))
78 |
79 | preferences.edit {
80 | putStringSet(PROFILE_LIST_SAVE_KEY, removedList)
81 | }
82 |
83 | _storedProfiles.value = getProfiles()
84 | }
85 |
86 | fun deleteProfile(path: String) {
87 | val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet()
88 |
89 | val removedList = profilesSet.filterTo(LinkedHashSet()) {
90 | val saveProfile = Json.decodeFromString(it)
91 | saveProfile.path != path
92 | }
93 |
94 | preferences.edit {
95 | putStringSet(PROFILE_LIST_SAVE_KEY, removedList)
96 | }
97 |
98 | _storedProfiles.value = getProfiles()
99 | }
100 |
101 | fun deleteProfiles(paths: List) {
102 | val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet()
103 |
104 | val removedList = profilesSet.filterTo(LinkedHashSet()) {
105 | val saveProfile = Json.decodeFromString(it)
106 | !paths.contains(saveProfile.path)
107 | }
108 |
109 | preferences.edit(commit = true) {
110 | putStringSet(PROFILE_LIST_SAVE_KEY, removedList)
111 | }
112 |
113 | _storedProfiles.value = getProfiles()
114 | }
115 |
116 | fun setCurrentProfile(profile: String?) {
117 | preferences.edit(commit = true) {
118 | if (profile.isNullOrEmpty()) {
119 | remove(PROFILE_CURRENT_SAVE_KEY)
120 | } else {
121 | putString(PROFILE_CURRENT_SAVE_KEY, profile)
122 | }
123 | }
124 | }
125 |
126 | fun getProfiles(): List {
127 | val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet()
128 |
129 | return profilesSet.map {
130 | Json.decodeFromString(it)
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/app/src/main/java/com/geode/launcher/utils/useCountdownTimer.kt:
--------------------------------------------------------------------------------
1 | package com.geode.launcher.utils
2 |
3 | import androidx.compose.runtime.*
4 | import androidx.lifecycle.compose.LocalLifecycleOwner
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.LifecycleEventObserver
7 | import androidx.lifecycle.LifecycleOwner
8 | import kotlinx.coroutines.delay
9 |
10 | const val MS_TO_SEC = 1000L
11 |
12 | @Composable
13 | fun useCountdownTimer(
14 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
15 | time: Long,
16 | onCountdownFinish: () -> Unit
17 | ): Long {
18 | var millisUntilFinished by remember {
19 | mutableLongStateOf(time)
20 | }
21 |
22 | var shouldBeCounting by remember {
23 | mutableStateOf(true)
24 | }
25 |
26 | LaunchedEffect(shouldBeCounting, millisUntilFinished) {
27 | if (!shouldBeCounting) {
28 | return@LaunchedEffect
29 | }
30 |
31 | if (millisUntilFinished > 0) {
32 | delay(MS_TO_SEC)
33 | millisUntilFinished -= MS_TO_SEC
34 | } else {
35 | onCountdownFinish()
36 | }
37 | }
38 |
39 | DisposableEffect(lifecycleOwner) {
40 | val observer = LifecycleEventObserver { _, event ->
41 | when (event) {
42 | Lifecycle.Event.ON_START -> {
43 | shouldBeCounting = true
44 | millisUntilFinished = time
45 | }
46 | Lifecycle.Event.ON_PAUSE,
47 | Lifecycle.Event.ON_STOP -> {
48 | shouldBeCounting = false
49 | }
50 | else -> {}
51 | }
52 | }
53 |
54 | lifecycleOwner.lifecycle.addObserver(observer)
55 |
56 | onDispose {
57 | lifecycleOwner.lifecycle.removeObserver(observer)
58 | }
59 | }
60 |
61 | return millisUntilFinished / MS_TO_SEC
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/cocos2dx/lib/Cocos2dxAccelerometer.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | Copyright (c) 2010-2011 cocos2d-x.org
3 |
4 | http://www.cocos2d-x.org
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 | ****************************************************************************/
24 | package org.cocos2dx.lib
25 |
26 | import android.content.Context
27 | import android.content.res.Configuration
28 | import android.hardware.Sensor
29 | import android.hardware.SensorEvent
30 | import android.hardware.SensorEventListener
31 | import android.hardware.SensorManager
32 | import android.os.Build
33 | import android.view.Surface
34 | import android.view.WindowManager
35 | import java.lang.ref.WeakReference
36 | import kotlin.math.roundToInt
37 |
38 | class Cocos2dxAccelerometer : SensorEventListener {
39 | companion object {
40 | @JvmStatic
41 | external fun onSensorChanged(pX: Float, pY: Float, pZ: Float, pTimestamp: Long)
42 | }
43 |
44 | constructor(context: Context) {
45 | mContext = WeakReference(context)
46 | mSensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
47 | mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
48 | val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
49 | context.display
50 | } else {
51 | @Suppress("DEPRECATION")
52 | (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
53 | }
54 | this.mNaturalOrientation = display.rotation
55 | }
56 |
57 | private val mContext: WeakReference
58 | private val mSensorManager: SensorManager
59 | private val mAccelerometer: Sensor?
60 | private val mNaturalOrientation: Int
61 |
62 | fun enable() {
63 | this.mSensorManager.registerListener(this, this.mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
64 | }
65 |
66 | fun setInterval(interval: Float) {
67 | this.mSensorManager.registerListener(this, this.mAccelerometer, (interval * 100000).roundToInt())
68 | }
69 |
70 | fun disable() {
71 | this.mSensorManager.unregisterListener(this)
72 | }
73 |
74 | override fun onSensorChanged(pSensorEvent: SensorEvent) {
75 | if (pSensorEvent.sensor.type != Sensor.TYPE_ACCELEROMETER) {
76 | return
77 | }
78 |
79 | var (x, y, z) = pSensorEvent.values
80 |
81 | /*
82 | * Because the axes are not swapped when the device's screen orientation
83 | * changes. So we should swap it here. In tablets such as Motorola Xoom,
84 | * the default orientation is landscape, so should consider this.
85 | */
86 | val orientation = this.mContext.get()!!.resources.configuration.orientation
87 |
88 | if ((orientation == Configuration.ORIENTATION_LANDSCAPE) && (this.mNaturalOrientation != Surface.ROTATION_0)) {
89 | val tmp = x
90 | x = -y
91 | y = tmp
92 | } else if ((orientation == Configuration.ORIENTATION_PORTRAIT) && (this.mNaturalOrientation != Surface.ROTATION_0)) {
93 | val tmp = x
94 | x = y
95 | y = -tmp
96 | }
97 |
98 | Cocos2dxGLSurfaceView.queueAccelerometer(x, y, z, pSensorEvent.timestamp)
99 | }
100 |
101 | override fun onAccuracyChanged(pSensor: Sensor, pAccuracy: Int) {}
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/cocos2dx/lib/Cocos2dxActivity.kt:
--------------------------------------------------------------------------------
1 | package org.cocos2dx.lib
2 |
3 | import com.customRobTop.BaseRobTopActivity
4 |
5 | @Suppress("unused")
6 | object Cocos2dxActivity {
7 | @JvmStatic
8 | fun openURL(url: String) {
9 | BaseRobTopActivity.openURL(url)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/cocos2dx/lib/Cocos2dxEditText.kt:
--------------------------------------------------------------------------------
1 | package org.cocos2dx.lib
2 |
3 | import android.content.Context
4 | import android.view.KeyEvent
5 | import androidx.appcompat.widget.AppCompatEditText
6 |
7 | class Cocos2dxEditText(context: Context) : AppCompatEditText(context) {
8 | var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView? = null
9 |
10 | override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
11 | super.onKeyDown(keyCode, keyEvent)
12 | if (keyCode != KeyEvent.KEYCODE_BACK) {
13 | return true
14 | }
15 | cocos2dxGLSurfaceView?.requestFocus()
16 | return true
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/cocos2dx/lib/Cocos2dxHelper.kt:
--------------------------------------------------------------------------------
1 | package org.cocos2dx.lib
2 |
3 | import android.content.Context
4 | import android.content.pm.ApplicationInfo
5 | import android.content.res.AssetManager
6 | import android.os.Process
7 | import com.customRobTop.BaseRobTopActivity
8 | import com.geode.launcher.utils.LaunchUtils
9 |
10 |
11 | @Suppress("unused", "KotlinJniMissingFunction")
12 | object Cocos2dxHelper {
13 | // private val sCocos2dMusic: Cocos2dxMusic? = null
14 | // private val sCocos2dSound: Cocos2dxSound? = null
15 | private var sAssetManager: AssetManager? = null
16 | private var sCocos2dxAccelerometer: Cocos2dxAccelerometer? = null
17 | private var sAccelerometerEnabled = false
18 | private var packageName: String? = null
19 | private var fileDirectory: String? = null
20 |
21 | // private var sContext: Context? = null
22 | private var cocos2dxHelperListener: Cocos2dxHelperListener? = null
23 |
24 | @JvmStatic
25 | external fun nativeSetApkPath(apkPath: String)
26 |
27 | @JvmStatic
28 | private external fun nativeSetEditTextDialogResult(bytes: ByteArray)
29 |
30 | @JvmStatic
31 | fun getCocos2dxWritablePath(): String? {
32 | return fileDirectory
33 | }
34 |
35 | @JvmStatic
36 | fun terminateProcess() {
37 | Process.killProcess(Process.myPid())
38 | }
39 |
40 | @JvmStatic
41 | fun getAssetManager(): AssetManager? {
42 | return sAssetManager
43 | }
44 |
45 | @JvmStatic
46 | fun enableAccelerometer() {
47 | sAccelerometerEnabled = true
48 | sCocos2dxAccelerometer?.enable()
49 | }
50 |
51 | @JvmStatic
52 | fun setAccelerometerInterval(interval: Float) {
53 | sCocos2dxAccelerometer?.setInterval(interval)
54 | }
55 |
56 | @JvmStatic
57 | fun disableAccelerometer() {
58 | sAccelerometerEnabled = false
59 | sCocos2dxAccelerometer?.disable()
60 | }
61 |
62 | fun onResume() {
63 | if (sAccelerometerEnabled) {
64 | sCocos2dxAccelerometer?.enable()
65 | }
66 | }
67 |
68 | fun onPause() {
69 | if (sAccelerometerEnabled) {
70 | sCocos2dxAccelerometer?.disable()
71 | }
72 | }
73 |
74 | @JvmStatic
75 | fun getDPI(): Int {
76 | return BaseRobTopActivity.me.get()?.resources?.configuration?.densityDpi ?: -1
77 | }
78 |
79 | @JvmStatic
80 | fun showDialog(title: String, message: String) {
81 | cocos2dxHelperListener?.showDialog(title, message)
82 | }
83 |
84 | fun init(context: Context, cocos2dxHelperListener: Cocos2dxHelperListener) {
85 | val applicationInfo: ApplicationInfo = context.applicationInfo
86 | // sContext = pContext
87 | this.cocos2dxHelperListener = cocos2dxHelperListener
88 | packageName = applicationInfo.packageName
89 | fileDirectory = LaunchUtils.getSaveDirectory(context).absolutePath
90 | sCocos2dxAccelerometer = Cocos2dxAccelerometer(context)
91 | // Cocos2dxHelper.sCocos2dMusic = Cocos2dxMusic(pContext)
92 | // var simultaneousStreams: Int = Cocos2dxSound.MAX_SIMULTANEOUS_STREAMS_DEFAULT
93 | // if (Cocos2dxHelper.getDeviceModel().indexOf("GT-I9100") !== -1) {
94 | // simultaneousStreams = Cocos2dxSound.MAX_SIMULTANEOUS_STREAMS_I9100
95 | // }
96 | // Cocos2dxHelper.sCocos2dSound = Cocos2dxSound(pContext, simultaneousStreams)
97 | sAssetManager = context.assets
98 | Cocos2dxBitmap.setContext(context)
99 | // Cocos2dxETCLoader.setContext(pContext)
100 | }
101 |
102 | interface Cocos2dxHelperListener {
103 | fun runOnGLThread(runnable: Runnable)
104 |
105 | fun showDialog(title: String, message: String)
106 |
107 | fun showEditTextDialog(
108 | title: String,
109 | message: String,
110 | inputMode: Int,
111 | inputFlag: Int,
112 | returnType: Int,
113 | maxLength: Int
114 | )
115 | }
116 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/cocos2dx/lib/Cocos2dxRenderer.kt:
--------------------------------------------------------------------------------
1 | package org.cocos2dx.lib
2 |
3 | import android.opengl.GLSurfaceView
4 | import android.os.Build
5 | import javax.microedition.khronos.egl.EGLConfig
6 | import javax.microedition.khronos.opengles.GL10
7 |
8 | private const val NANOSECONDS_PER_SECOND = 1000000000L
9 | private const val NANOSECONDS_PER_MICROSECOND = 1000000L
10 |
11 | @Suppress("unused", "KotlinJniMissingFunction")
12 | class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceView.Renderer {
13 | companion object {
14 | @JvmStatic
15 | fun setAnimationInterval(@Suppress("UNUSED_PARAMETER") animationInterval: Double) {
16 | // this function is useless for gd
17 | }
18 |
19 | @JvmStatic
20 | private external fun nativeDeleteBackward()
21 |
22 | @JvmStatic
23 | private external fun nativeGetContentText(): String
24 |
25 | @JvmStatic
26 | private external fun nativeInsertText(text: String)
27 |
28 | @JvmStatic
29 | private external fun nativeKeyDown(keyCode: Int): Boolean
30 |
31 | @JvmStatic
32 | private external fun nativeOnPause()
33 |
34 | @JvmStatic
35 | private external fun nativeOnResume()
36 |
37 | @JvmStatic
38 | private external fun nativeRender()
39 |
40 | @JvmStatic
41 | private external fun nativeTextClosed()
42 |
43 | @JvmStatic
44 | private external fun nativeTouchesBegin(id: Int, x: Float, y: Float)
45 |
46 | @JvmStatic
47 | private external fun nativeTouchesCancel(
48 | ids: IntArray,
49 | xs: FloatArray,
50 | ys: FloatArray
51 | )
52 |
53 | @JvmStatic
54 | private external fun nativeTouchesEnd(id: Int, x: Float, y: Float)
55 |
56 | @JvmStatic
57 | private external fun nativeTouchesMove(ids: IntArray, xs: FloatArray, ys: FloatArray)
58 |
59 | @JvmStatic
60 | private external fun nativeInit(width: Int, height: Int)
61 | }
62 |
63 | private var lastTickInNanoSeconds: Long = 0
64 | private var screenWidth = 0
65 | private var screenHeight = 0
66 | var sendResizeEvents = false
67 | var setFrameRate = false
68 | private var mAnimationInterval: Long? = null
69 |
70 | fun limitFrameRate(rate: Int) {
71 | mAnimationInterval = NANOSECONDS_PER_SECOND/rate
72 | }
73 |
74 | fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) {
75 | screenWidth = surfaceWidth
76 | screenHeight = surfaceHeight
77 | }
78 |
79 | override fun onSurfaceCreated(gl10: GL10?, eglConfig: EGLConfig?) {
80 | nativeInit(screenWidth, screenHeight)
81 | lastTickInNanoSeconds = System.nanoTime()
82 | }
83 |
84 | override fun onSurfaceChanged(gl10: GL10?, width: Int, height: Int) {
85 | if (setFrameRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
86 | handler.updateRefreshRate()
87 | }
88 |
89 | println("renderer surfaceChanged: ${width}x${height}@${handler.display.refreshRate}fps")
90 | }
91 |
92 | override fun onDrawFrame(gl: GL10?) {
93 | val animationInterval = mAnimationInterval
94 | if (animationInterval == null) {
95 | nativeRender()
96 | } else {
97 | nativeRender()
98 |
99 | val now = System.nanoTime()
100 | val interval = now - this.lastTickInNanoSeconds
101 |
102 | if (interval < animationInterval) {
103 | try {
104 | Thread.sleep((animationInterval - interval) / NANOSECONDS_PER_MICROSECOND)
105 | } catch (e: Exception) {}
106 | }
107 |
108 | lastTickInNanoSeconds = System.nanoTime()
109 | }
110 | }
111 |
112 | fun handleActionDown(id: Int, x: Float, y: Float) {
113 | nativeTouchesBegin(id, x, y)
114 | }
115 |
116 | fun handleActionUp(id: Int, x: Float, y: Float) {
117 | nativeTouchesEnd(id, x, y)
118 | }
119 |
120 | fun handleActionCancel(ids: IntArray, xs: FloatArray, ys: FloatArray) {
121 | nativeTouchesCancel(ids, xs, ys)
122 | }
123 |
124 | fun handleActionMove(ids: IntArray, xs: FloatArray, ys: FloatArray) {
125 | nativeTouchesMove(ids, xs, ys)
126 | }
127 |
128 | fun handleKeyDown(keyCode: Int) {
129 | nativeKeyDown(keyCode)
130 | }
131 |
132 | fun handleOnPause() {
133 | nativeOnPause()
134 | }
135 |
136 | fun handleOnResume() {
137 | nativeOnResume()
138 | }
139 |
140 | fun handleInsertText(text: String) {
141 | nativeInsertText(text)
142 | }
143 |
144 | fun handleDeleteBackward() {
145 | nativeDeleteBackward()
146 | }
147 |
148 | fun handleTextClosed() {
149 | nativeTextClosed()
150 | }
151 |
152 | fun getContentText(): String {
153 | return nativeGetContentText()
154 | }
155 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/cocos2dx/lib/Cocos2dxTextInputWrapper.kt:
--------------------------------------------------------------------------------
1 | package org.cocos2dx.lib
2 |
3 | import android.content.Context
4 | import android.text.TextWatcher
5 | import android.widget.TextView.OnEditorActionListener
6 | import android.text.Editable
7 | import android.view.KeyEvent
8 | import android.view.inputmethod.InputMethodManager
9 | import android.widget.TextView
10 |
11 | class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView) : TextWatcher, OnEditorActionListener {
12 | private var originText: String? = null
13 | private var text: String? = null
14 |
15 | private val isFullScreenEdit: Boolean
16 | get() = (cocos2dxGLSurfaceView.cocos2dxEditText?.context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).isFullscreenMode
17 |
18 | fun setOriginText(originText: String?) {
19 | this.originText = originText
20 | }
21 |
22 | override fun afterTextChanged(s: Editable) {
23 | if (!isFullScreenEdit) {
24 | var nModified = s.length - text!!.length
25 | if (nModified > 0) {
26 | cocos2dxGLSurfaceView.insertText(
27 | s.subSequence(text!!.length, s.length).toString()
28 | )
29 | } else {
30 | while (nModified < 0) {
31 | cocos2dxGLSurfaceView.deleteBackward()
32 | nModified++
33 | }
34 | }
35 | text = s.toString()
36 | }
37 | }
38 |
39 | override fun beforeTextChanged(
40 | charSequence: CharSequence,
41 | start: Int,
42 | count: Int,
43 | after: Int
44 | ) {
45 | text = charSequence.toString()
46 | }
47 |
48 | override fun onTextChanged(pCharSequence: CharSequence, start: Int, before: Int, count: Int) {}
49 | override fun onEditorAction(textView: TextView, actionID: Int, keyEvent: KeyEvent?): Boolean {
50 | if (cocos2dxGLSurfaceView.cocos2dxEditText == textView && isFullScreenEdit) {
51 | for (i in originText!!.length downTo 1) {
52 | cocos2dxGLSurfaceView.deleteBackward()
53 | }
54 | var text = textView.text.toString()
55 | if (text.compareTo("") == 0) {
56 | text = "\n"
57 | }
58 | if (10 != text[text.length - 1].code) {
59 | text += 10.toChar()
60 | }
61 | cocos2dxGLSurfaceView.insertText(text)
62 | }
63 | if (actionID != 6) {
64 | return false
65 | }
66 | cocos2dxGLSurfaceView.requestFocus()
67 | Cocos2dxGLSurfaceView.closeIMEKeyboard()
68 | return false
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/cocos2dx/lib/Cocos2dxTypefaces.kt:
--------------------------------------------------------------------------------
1 | package org.cocos2dx.lib
2 |
3 | import android.content.Context
4 | import android.graphics.Typeface
5 | import kotlin.jvm.Synchronized
6 | import java.util.HashMap
7 |
8 | object Cocos2dxTypefaces {
9 | private val typefaceCache = HashMap()
10 |
11 | @Synchronized
12 | operator fun get(pContext: Context, pAssetName: String): Typeface? {
13 | val typeface: Typeface?
14 | val typeface2: Typeface
15 |
16 | synchronized(Cocos2dxTypefaces::class.java) {
17 | if (!typefaceCache.containsKey(pAssetName)) {
18 | typeface2 = if (pAssetName.startsWith("/")) {
19 | Typeface.createFromFile(pAssetName)
20 | } else {
21 | Typeface.createFromAsset(pContext.assets, pAssetName)
22 | }
23 | typefaceCache[pAssetName] = typeface2
24 | }
25 | typeface = typefaceCache[pAssetName]
26 | }
27 | return typeface
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/fmod/AudioDevice.kt:
--------------------------------------------------------------------------------
1 | package org.fmod
2 |
3 | import android.media.AudioAttributes
4 | import android.media.AudioFormat
5 | import android.media.AudioManager
6 | import android.media.AudioTrack
7 | import android.os.Build
8 | import android.util.Log
9 |
10 | class AudioDevice {
11 | private var mTrack: AudioTrack? = null
12 | private fun fetchChannelConfigFromCount(speakerCount: Int): Int {
13 | return when (speakerCount) {
14 | 1 -> AudioFormat.CHANNEL_OUT_MONO
15 | 2 -> AudioFormat.CHANNEL_OUT_STEREO
16 | 6 -> AudioFormat.CHANNEL_OUT_5POINT1
17 | 8 -> AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
18 | else -> AudioFormat.CHANNEL_INVALID
19 | }
20 | }
21 |
22 | fun init(channelCount: Int, sampleRateInHz: Int, sampleSize: Int, sampleCount: Int): Boolean {
23 | val channelConfig = fetchChannelConfigFromCount(channelCount)
24 | val minBufferSize = AudioTrack.getMinBufferSize(
25 | sampleRateInHz,
26 | channelConfig,
27 | AudioFormat.ENCODING_PCM_16BIT
28 | )
29 |
30 | if (minBufferSize < 0) {
31 | Log.w(
32 | "fmod",
33 | "AudioDevice::init : Couldn't query minimum buffer size, possibly unsupported sample rate or channel count"
34 | )
35 | } else {
36 | Log.i("fmod", "AudioDevice::init : Min buffer size: $minBufferSize bytes")
37 | }
38 |
39 | val realBufferSize = sampleSize * sampleCount * channelCount * 2
40 | val bufferSize = minBufferSize.coerceAtLeast(realBufferSize)
41 | Log.i("fmod", "AudioDevice::init : Actual buffer size: $bufferSize bytes")
42 | return try {
43 | val attributes = AudioAttributes.Builder()
44 | .setUsage(AudioAttributes.USAGE_GAME)
45 | .build()
46 |
47 | val format = AudioFormat.Builder()
48 | .setChannelMask(channelConfig)
49 | .setSampleRate(sampleRateInHz)
50 | .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
51 | .build()
52 |
53 | val audioTrackBuilder = AudioTrack.Builder()
54 | .setAudioAttributes(attributes)
55 | .setAudioFormat(format)
56 | .setBufferSizeInBytes(bufferSize)
57 | .setTransferMode(AudioTrack.MODE_STREAM)
58 |
59 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
60 | audioTrackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
61 | }
62 |
63 | val audioTrack = audioTrackBuilder.build()
64 |
65 | mTrack = audioTrack
66 | try {
67 | audioTrack.play()
68 | true
69 | } catch (unused: IllegalStateException) {
70 | Log.e("fmod", "AudioDevice::init : AudioTrack play caused IllegalStateException")
71 | mTrack?.release()
72 | mTrack = null
73 | false
74 | }
75 | } catch (_: IllegalArgumentException) {
76 | Log.e("fmod", "AudioDevice::init : AudioTrack creation caused IllegalArgumentException")
77 | false
78 | }
79 | }
80 |
81 | fun close() {
82 | try {
83 | mTrack?.stop()
84 | } catch (unused: IllegalStateException) {
85 | Log.e("fmod", "AudioDevice::init : AudioTrack stop caused IllegalStateException")
86 | }
87 | mTrack?.release()
88 | mTrack = null
89 | }
90 |
91 | fun write(sArr: ShortArray, size: Int) {
92 | mTrack?.write(sArr, 0, size)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fmod/FMOD.kt:
--------------------------------------------------------------------------------
1 | package org.fmod
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.IntentFilter
8 | import android.content.pm.PackageManager
9 | import android.content.res.AssetManager
10 | import android.media.AudioDeviceInfo
11 | import android.media.AudioManager
12 | import android.net.Uri
13 | import android.os.Build
14 | import android.util.Log
15 | import java.io.FileNotFoundException
16 |
17 | // i never agreed to your licenses! take that
18 | @SuppressLint("StaticFieldLeak")
19 | object FMOD {
20 | private var gContext: Context? = null
21 | private val gPluginBroadcastReceiver = PluginBroadcastReceiver()
22 |
23 | external fun OutputAAudioHeadphonesChanged()
24 |
25 | @JvmStatic
26 | fun init(context: Context?) {
27 | gContext = context
28 | gContext?.registerReceiver(
29 | gPluginBroadcastReceiver,
30 | IntentFilter(Intent.ACTION_HEADSET_PLUG)
31 | )
32 | }
33 |
34 | @JvmStatic
35 | fun close() {
36 | val context = gContext
37 | context?.unregisterReceiver(gPluginBroadcastReceiver)
38 | gContext = null
39 | }
40 |
41 | @JvmStatic
42 | fun checkInit(): Boolean {
43 | return gContext != null
44 | }
45 |
46 | @JvmStatic
47 | fun getAssetManager(): AssetManager? {
48 | val context = gContext
49 | return context?.assets
50 | }
51 |
52 | @JvmStatic
53 | fun supportsLowLatency(): Boolean {
54 | val outputBlockSize = getOutputBlockSize()
55 | val lowLatencyFlag = lowLatencyFlag()
56 | val proAudioFlag = proAudioFlag()
57 | val z = outputBlockSize in 1..1024
58 | val isBluetoothOn = isBluetoothOn()
59 | Log.i(
60 | "fmod",
61 | "FMOD::supportsLowLatency : Low latency = $lowLatencyFlag, Pro Audio = $proAudioFlag, Bluetooth On = $isBluetoothOn, Acceptable Block Size = $z ($outputBlockSize)"
62 | )
63 | return z && lowLatencyFlag && !isBluetoothOn
64 | }
65 |
66 | @JvmStatic
67 | fun lowLatencyFlag(): Boolean {
68 | val context = gContext ?: return false
69 | return context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)
70 | }
71 |
72 | @JvmStatic
73 | fun proAudioFlag(): Boolean {
74 | val context = gContext ?: return false
75 | return context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO)
76 | }
77 |
78 | @JvmStatic
79 | fun supportsAAudio(): Boolean {
80 | return Build.VERSION.SDK_INT >= 27
81 | }
82 |
83 | @JvmStatic
84 | fun getOutputSampleRate(): Int {
85 | val context = gContext ?: return 0
86 | val audioService = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
87 | val property = audioService.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
88 |
89 | return property?.toInt() ?: 0
90 | }
91 |
92 | @JvmStatic
93 | fun getOutputBlockSize(): Int {
94 | val context = gContext ?: return 0
95 | val audioService = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
96 | val property = audioService.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
97 |
98 | return property?.toInt() ?: 0
99 | }
100 |
101 | @JvmStatic
102 | fun isBluetoothOn(): Boolean {
103 | val context = gContext ?: return false
104 | val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
105 |
106 | val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
107 | return devices.any {
108 | it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP || it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
109 | }
110 | }
111 |
112 | @JvmStatic
113 | fun fileDescriptorFromUri(str: String?): Int {
114 | gContext?.apply {
115 | try {
116 | contentResolver.openFileDescriptor(Uri.parse(str), "r")?.apply {
117 | return detachFd()
118 | }
119 | } catch (_: FileNotFoundException) { }
120 | }
121 |
122 | return -1
123 | }
124 |
125 | internal class PluginBroadcastReceiver : BroadcastReceiver() {
126 | override fun onReceive(context: Context, intent: Intent) {
127 | OutputAAudioHeadphonesChanged()
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fmod/MediaCodec.kt:
--------------------------------------------------------------------------------
1 | package org.fmod
2 |
3 | import android.media.MediaCodec
4 | import android.media.MediaDataSource
5 | import android.media.MediaExtractor
6 | import android.media.MediaCodec.BufferInfo
7 | import android.media.MediaFormat
8 | import android.os.Build
9 | import android.util.Log
10 | import java.io.IOException
11 | import java.nio.ByteBuffer
12 |
13 | class MediaCodec {
14 | // these variables require getX functions for ndk access
15 | var channelCount = 0
16 | private set
17 |
18 | var length = 0L
19 | private set
20 |
21 | var sampleRate = 0
22 | private set
23 |
24 | private var mCodecPtr = 0L
25 | private var mCurrentOutputBufferIndex = -1
26 | private var mDecoder: MediaCodec? = null
27 | private var mExtractor: MediaExtractor? = null
28 | private var mInputBuffers: Array? = null
29 | private var mInputFinished = false
30 | private var mOutputBuffers: Array? = null
31 | private var mOutputFinished = false
32 |
33 | fun init(codecPtr: Long): Boolean {
34 | mCodecPtr = codecPtr
35 |
36 | try {
37 | val mediaExtractor = MediaExtractor()
38 | mExtractor = mediaExtractor
39 | mediaExtractor.setDataSource(object : MediaDataSource() {
40 | override fun close() {}
41 | override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int {
42 | return fmodReadAt(mCodecPtr, position, buffer, offset, size)
43 | }
44 |
45 | override fun getSize(): Long {
46 | return fmodGetSize(mCodecPtr)
47 | }
48 | })
49 | } catch (e: IOException) {
50 | Log.w("fmod", "MediaCodec::init : $e")
51 | return false
52 | }
53 |
54 | val extractor = mExtractor!!
55 |
56 | val trackCount = extractor.trackCount
57 | var track = 0
58 |
59 | while (track < trackCount) {
60 | val trackFormat = extractor.getTrackFormat(track)
61 | val string = trackFormat.getString(MediaFormat.KEY_MIME)
62 | Log.d(
63 | "fmod",
64 | "MediaCodec::init : Format $track / $trackCount -- $trackFormat"
65 | )
66 | if (string == MediaFormat.MIMETYPE_AUDIO_AAC) {
67 | return try {
68 | val decoder = MediaCodec.createDecoderByType(string)
69 | extractor.selectTrack(track)
70 | decoder.configure(trackFormat, null, null, 0)
71 | decoder.start()
72 | mInputBuffers = decoder.inputBuffers
73 | mOutputBuffers = decoder.outputBuffers
74 | mDecoder = decoder
75 |
76 | val encoderDelay =
77 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && trackFormat.containsKey(MediaFormat.KEY_ENCODER_DELAY))
78 | trackFormat.getInteger(MediaFormat.KEY_ENCODER_DELAY) else 0
79 |
80 | val encoderPadding = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && trackFormat.containsKey(MediaFormat.KEY_ENCODER_PADDING))
81 | trackFormat.getInteger(MediaFormat.KEY_ENCODER_PADDING) else 0
82 |
83 | val trackDuration = trackFormat.getLong(MediaFormat.KEY_DURATION)
84 | channelCount = trackFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
85 | val trackSampleRate = trackFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
86 | sampleRate = trackSampleRate
87 | length =
88 | (((trackDuration * trackSampleRate.toLong() + 999999) / 1000000).toInt() - encoderDelay - encoderPadding).toLong()
89 |
90 | true
91 | } catch (e: IOException) {
92 | Log.e("fmod", "MediaCodec::init : $e")
93 | false
94 | }
95 | } else {
96 | track++
97 | }
98 | }
99 | return false
100 | }
101 |
102 | fun release() {
103 | mDecoder?.stop()
104 | mDecoder?.release()
105 | mDecoder = null
106 |
107 | mExtractor?.release()
108 | mExtractor = null
109 | }
110 |
111 | fun read(bArr: ByteArray, i: Int): Int {
112 | var dequeueInputBuffer = 0
113 | val i2 =
114 | if (!mInputFinished || !mOutputFinished || mCurrentOutputBufferIndex != -1) 0 else -1
115 | while (!mInputFinished && mDecoder!!.dequeueInputBuffer(0).also {
116 | dequeueInputBuffer = it
117 | } >= 0) {
118 | val readSampleData = mExtractor!!.readSampleData(mInputBuffers!![dequeueInputBuffer], 0)
119 | if (readSampleData >= 0) {
120 | mDecoder!!.queueInputBuffer(
121 | dequeueInputBuffer,
122 | 0,
123 | readSampleData,
124 | mExtractor!!.sampleTime,
125 | 0
126 | )
127 | mExtractor!!.advance()
128 | } else {
129 | mDecoder!!.queueInputBuffer(dequeueInputBuffer, 0, 0, 0, 4)
130 | mInputFinished = true
131 | }
132 | }
133 | if (!mOutputFinished && mCurrentOutputBufferIndex == -1) {
134 | val bufferInfo = BufferInfo()
135 | val dequeueOutputBuffer = mDecoder!!.dequeueOutputBuffer(bufferInfo, 10000)
136 | if (dequeueOutputBuffer >= 0) {
137 | mCurrentOutputBufferIndex = dequeueOutputBuffer
138 | mOutputBuffers!![dequeueOutputBuffer].limit(bufferInfo.size)
139 | mOutputBuffers!![dequeueOutputBuffer].position(bufferInfo.offset)
140 | } else if (dequeueOutputBuffer == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
141 | mOutputBuffers = mDecoder!!.outputBuffers
142 | } else if (dequeueOutputBuffer == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
143 | Log.d(
144 | "fmod",
145 | "MediaCodec::read : MediaCodec::dequeueOutputBuffer returned MediaCodec.INFO_OUTPUT_FORMAT_CHANGED " + mDecoder!!.outputFormat
146 | )
147 | } else if (dequeueOutputBuffer == MediaCodec.INFO_TRY_AGAIN_LATER) {
148 | Log.d(
149 | "fmod",
150 | "MediaCodec::read : MediaCodec::dequeueOutputBuffer returned MediaCodec.INFO_TRY_AGAIN_LATER."
151 | )
152 | } else {
153 | Log.w(
154 | "fmod",
155 | "MediaCodec::read : MediaCodec::dequeueOutputBuffer returned $dequeueOutputBuffer"
156 | )
157 | }
158 | if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
159 | mOutputFinished = true
160 | }
161 | }
162 | val i3 = mCurrentOutputBufferIndex
163 | if (i3 == -1) {
164 | return i2
165 | }
166 | val byteBuffer = mOutputBuffers!![i3]
167 | val min = byteBuffer.remaining().coerceAtMost(i)
168 | byteBuffer[bArr, 0, min]
169 | if (!byteBuffer.hasRemaining()) {
170 | byteBuffer.clear()
171 | mDecoder!!.releaseOutputBuffer(mCurrentOutputBufferIndex, false)
172 | mCurrentOutputBufferIndex = -1
173 | }
174 | return min
175 | }
176 |
177 | fun seek(i: Int) {
178 | val i2 = mCurrentOutputBufferIndex
179 | if (i2 != -1) {
180 | mOutputBuffers!![i2].clear()
181 | mCurrentOutputBufferIndex = -1
182 | }
183 | mInputFinished = false
184 | mOutputFinished = false
185 | mDecoder!!.flush()
186 | val j = i.toLong()
187 | mExtractor!!.seekTo(j * 1000000 / sampleRate.toLong(), MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
188 | val sampleTime = (mExtractor!!.sampleTime * sampleRate.toLong() + 999999) / 1000000
189 | var i3 = ((j - sampleTime) * channelCount.toLong() * 2).toInt()
190 | if (i3 < 0) {
191 | Log.w(
192 | "fmod",
193 | "MediaCodec::seek : Seek to $i resulted in position $sampleTime"
194 | )
195 | return
196 | }
197 | val bArr = ByteArray(1024)
198 | while (i3 > 0) {
199 | i3 -= read(bArr, 1024.coerceAtMost(i3))
200 | }
201 | }
202 |
203 | companion object {
204 | @JvmStatic
205 | external fun fmodGetSize(codecPtr: Long): Long
206 |
207 | @JvmStatic
208 | external fun fmodReadAt(codecPtr: Long, position: Long, buffer: ByteArray?, offset: Int, size: Int): Int
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/app/src/main/jniLibs/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/jniLibs/.keep
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/geode_logo.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
29 |
30 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/geode_monochrome.xml:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/google_play_badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/drawable/google_play_badge.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_bug_report.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_content_copy.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_data_object.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_delete.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_description.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_download.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_error.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_filter_list.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_link.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_person.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_person_add.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_question_mark.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_remove.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_resume.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_save.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_undo.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v31/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #282828
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/game_mode_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | buildscript {
2 | val composeBOM by extra("2025.04.01")
3 | }
4 |
5 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
6 | plugins {
7 | id("com.android.application") version "8.9.2" apply false
8 | id("com.android.library") version "8.9.2" apply false
9 | id("org.jetbrains.kotlin.android") version "2.1.20" apply false
10 | id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" apply false
11 | id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" apply false
12 | }
13 |
14 | tasks.register("clean") {
15 | delete(rootProject.layout.buildDirectory)
16 | }
17 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | # nice optimization
25 | org.gradle.configuration-cache=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geode-sdk/android-launcher/09bb28d66ffd0a60b6d5b7f3fcf647b0368134d0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Apr 24 10:54:01 MST 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 |
9 | @Suppress("UnstableApiUsage")
10 | dependencyResolutionManagement {
11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
12 | repositories {
13 | google()
14 | mavenCentral()
15 | }
16 | }
17 |
18 | rootProject.name = "launcher"
19 | include(":app")
20 |
--------------------------------------------------------------------------------