Compare commits

..
34 Commits
Author SHA1 Message Date
qimiko 642fdc36ee cleanup motionevent 2024-02-11 00:55:33 -07:00
qimiko c6155443a2 platformcapability changed to bool 2024-02-10 22:07:22 -07:00
qimiko b96561bf33 add keyboard input support 2024-02-10 18:07:06 -07:00
qimiko 61c018ee0d delay launch with safe mode dialog 2024-02-08 22:11:33 -07:00
qimiko 41e3e0a133 package maintenance 2024-02-07 22:19:10 -07:00
qimiko 3853a469cd add internal request all files perm 2024-02-04 12:46:25 -07:00
qimiko 62b31b8924 hide load failure option 2024-02-03 18:32:14 -07:00
qimiko ab1d0c96f4 have internal links open storage provider 2024-02-03 18:30:54 -07:00
qimiko 96354096e2 minor update 2024-02-03 18:08:31 -07:00
qimiko 25b686f183 fix update warning dismiss 2024-02-03 18:06:07 -07:00
qimiko 7694236434 disallow ignoring older gd version warning 2024-02-03 18:03:40 -07:00
qimiko 7c0f1da312 clear button for text inputs 2024-02-03 05:28:19 -07:00
qimiko 019d9747b5 add ui to launch in safe mode
closes #17
2024-02-02 02:06:43 -07:00
qimiko e806ec223a use all files permission on newer devices 2024-02-02 00:04:07 -07:00
qimiko 9f58e76f97 maintenance 2 2024-02-01 13:15:30 -07:00
qimiko 371750a184 enable gradle cache 2024-01-31 16:51:09 -07:00
qimiko 2494bda4e8 send vibration on log long press 2024-01-30 03:17:23 -07:00
qimiko b67be09cd3 update readme 2024-01-30 02:22:49 -07:00
qimiko 09b1c9bc3a forgot to set failure callback :( 2024-01-30 02:20:48 -07:00
qimiko 483fb56e25 feeling gradle 2024-01-30 02:20:48 -07:00
Chloe 7857fae68f appease github 2024-01-30 00:46:19 -07:00
qimiko 61b84cd223 accidentally left in an import 2024-01-30 00:40:08 -07:00
qimiko bd67332adc remove safe mode launch code
no sense having it in onLaunch parameters until i figure out how i want the ui to work
2024-01-30 00:39:04 -07:00
qimiko 84657fd6b3 add safe mode handling 2024-01-30 00:06:29 -07:00
qimiko f197b45f9e increment launcher version 2024-01-29 23:49:07 -07:00
qimiko d0dc8bec5d build file maintenance 2024-01-29 23:45:56 -07:00
qimiko c9e0408eb2 close dropdown on dialog open 2024-01-28 19:02:13 -07:00
qimiko b50a5b2b54 add option to filter by log level 2024-01-28 18:48:11 -07:00
qimiko c8e3d7404c allow downgrading geode 2024-01-28 17:55:01 -07:00
qimiko c4c41e324e move settings components into another file 2024-01-28 16:16:55 -07:00
qimiko 4d324913df promote load errors to log errors 2024-01-28 06:51:51 -07:00
qimiko 19bd549b1f request file permissions when necessary
fixes #8 by making it irrelevant
2024-01-28 00:20:18 -07:00
qimiko 9dc501c9ab make geode load failures a hard error 2024-01-27 23:16:33 -07:00
qimiko 5521869ac9 add launch arguments option 2024-01-27 22:55:34 -07:00
26 changed files with 1242 additions and 592 deletions

No files matched your search

+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
- name: Upload development build - name: Upload development build
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: geode-launcher-${{ matrix.config.lower }} name: geode-launcher-${{ matrix.config.lower }}
path: | path: |
-4
View File
@@ -1,7 +1,3 @@
# Geode Android Launcher # Geode Android Launcher
Launches a vanilla copy of Geometry Dash with the Geode loader added. Launches a vanilla copy of Geometry Dash with the Geode loader added.
## TODO:
* Figure out better way of packaging libgeode
-98
View File
@@ -1,98 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
}
android {
compileSdk 34
defaultConfig {
applicationId "com.geode.launcher"
minSdk 23
targetSdk 34
versionCode 9
versionName "1.1.1"
vectorDrawables {
useSupportLibrary true
}
externalNativeBuild {
cmake {
arguments "-DUSE_TULIPHOOK:BOOL=OFF"
}
}
ndkConfig.abiFilters "arm64-v8a", "armeabi-v7a"
}
splits {
abi {
enable true
reset()
//noinspection ChromeOsAbiSupport (not my fault)
include "arm64-v8a", "armeabi-v7a"
universalApk true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
// enables a polyfill for java Instant on api levels < 26 (used for updater)
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
buildConfig true
}
composeOptions {
kotlinCompilerExtensionVersion compose_compiler
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
}
}
namespace 'com.geode.launcher'
ndkVersion '26.1.10909125'
}
dependencies {
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.compose.ui:ui"
implementation 'androidx.compose.material3:material3'
implementation "androidx.compose.ui:ui-tooling-preview"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.activity:activity-ktx:1.8.2'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation "androidx.documentfile:documentfile:1.0.1"
implementation "com.squareup.okio:okio:3.7.0"
implementation "com.squareup.okhttp3:okhttp:4.12.0"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.0"
debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-test-manifest"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
}
+103
View File
@@ -0,0 +1,103 @@
val composeCompiler: String by rootProject.extra
val composeBOM: String by rootProject.extra
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.serialization")
}
android {
compileSdk = 34
defaultConfig {
applicationId = "com.geode.launcher"
minSdk = 23
targetSdk = 34
versionCode = 11
versionName = "1.2.1"
vectorDrawables {
useSupportLibrary = true
}
@Suppress("UnstableApiUsage")
externalNativeBuild {
cmake {
arguments("-DUSE_TULIPHOOK:BOOL=OFF")
}
}
//noinspection ChromeOsAbiSupport (not my fault)
ndk.abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
splits {
abi {
isEnable = true
reset()
//noinspection ChromeOsAbiSupport. i'm sorry!
include("arm64-v8a", "armeabi-v7a")
isUniversalApk = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
// enables a polyfill for java Instant on api levels < 26 (used for updater)
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = composeCompiler
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
namespace = "com.geode.launcher"
ndkVersion = "26.1.10909125"
}
dependencies {
implementation (platform("androidx.compose:compose-bom:$composeBOM"))
implementation ("androidx.core:core-ktx:1.12.0")
implementation ("androidx.compose.ui:ui")
implementation ("androidx.compose.material3:material3")
implementation ("androidx.compose.ui:ui-tooling-preview")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation ("androidx.activity:activity-compose:1.8.2")
implementation ("androidx.activity:activity-ktx:1.8.2")
implementation ("androidx.appcompat:appcompat:1.6.1")
implementation ("androidx.documentfile:documentfile:1.0.1")
implementation ("com.squareup.okio:okio:3.7.0")
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2")
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
debugImplementation ("androidx.compose.ui:ui-tooling")
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.0.4")
}
@@ -23,14 +23,14 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Divider
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
@@ -51,9 +51,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
@@ -104,16 +106,20 @@ fun getLogColor(priority: LogPriority): Color {
// i just copied these from my ide tbh // i just copied these from my ide tbh
return if (isDark) { return if (isDark) {
when (priority) { when (priority) {
LogPriority.ERROR -> Color(0xffdf5151) LogPriority.ERROR -> Color(0xff_df_51_51)
LogPriority.WARN -> Color(0xffbcb500) LogPriority.WARN -> Color(0xff_bc_b5_00)
LogPriority.FATAL -> Color(0xffb8b6fc) LogPriority.FATAL -> Color(0xff_b8_b6_fc)
LogPriority.DEBUG -> Color(0xff_aa_aa_aa)
LogPriority.VERBOSE -> Color(0xff_77_77_77)
else -> Color.Unspecified else -> Color.Unspecified
} }
} else { } else {
when (priority) { when (priority) {
LogPriority.ERROR -> Color(0xffdf5151) LogPriority.ERROR -> Color(0xff_df_51_51)
LogPriority.WARN -> Color(0xffeab700) LogPriority.WARN -> Color(0xff_ea_b7_00)
LogPriority.FATAL -> Color(0xffa142f4) LogPriority.FATAL -> Color(0xff_a1_42_f4)
LogPriority.DEBUG -> Color(0xff_77_77_77)
LogPriority.VERBOSE -> Color(0xff_aa_aa_aa)
else -> Color.Unspecified else -> Color.Unspecified
} }
} }
@@ -138,16 +144,39 @@ fun copyLogText(context: Context, logMessage: String) {
} }
} }
@Composable
fun SelectLogLevelDialog(
onDismissRequest: () -> Unit,
onSelect: (LogPriority) -> Unit,
initialValue: LogPriority
) {
SelectDialog(
title = stringResource(R.string.application_logs_level_dialog_name),
onDismissRequest = onDismissRequest,
onSelect = { onSelect(it) },
initialValue = initialValue,
toLabel = {
// i'm lazy
it.name.lowercase().replaceFirstChar { c -> c.uppercaseChar() }
},
optionsCount = LogPriority.FATAL downTo LogPriority.VERBOSE
)
}
@Composable @Composable
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) { fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
val context = LocalContext.current val context = LocalContext.current
val haptics = LocalHapticFeedback.current
Column( Column(
modifier = modifier modifier = modifier
.padding(8.dp) .padding(8.dp)
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onLongPress = { onLongPress = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
copyLogText(context, logLine.asSimpleString) copyLogText(context, logLine.asSimpleString)
} }
) )
@@ -205,6 +234,18 @@ fun ApplicationLogsScreen(
listState.scrollToItem(logLines.size) listState.scrollToItem(logLines.size)
} }
var showLogLevelSelect by remember { mutableStateOf(false) }
if (showLogLevelSelect) {
SelectLogLevelDialog(
onDismissRequest = { showLogLevelSelect = false },
onSelect = {
logViewModel.logLevel = it
showLogLevelSelect = false
},
initialValue = logViewModel.logLevel
)
}
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
@@ -213,7 +254,7 @@ fun ApplicationLogsScreen(
navigationIcon = { navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) { IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_icon_alt) contentDescription = stringResource(R.string.back_icon_alt)
) )
} }
@@ -238,7 +279,7 @@ fun ApplicationLogsScreen(
contentDescription = stringResource(R.string.application_logs_share) contentDescription = stringResource(R.string.application_logs_share)
) )
} }
IconButton(onClick = { showMoreOptions = !showMoreOptions },) { IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
Icon( Icon(
Icons.Filled.MoreVert, Icons.Filled.MoreVert,
contentDescription = stringResource(R.string.application_logs_more) contentDescription = stringResource(R.string.application_logs_more)
@@ -252,7 +293,7 @@ fun ApplicationLogsScreen(
DropdownMenuItem( DropdownMenuItem(
leadingIcon = { leadingIcon = {
Icon( Icon(
painterResource(R.drawable.icon_filter_list), painterResource(R.drawable.icon_bug_report),
contentDescription = null, contentDescription = null,
) )
}, },
@@ -272,6 +313,22 @@ fun ApplicationLogsScreen(
} }
) )
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_filter_list),
contentDescription = null,
)
},
text = {
Text(stringResource(R.string.application_logs_level))
},
onClick = {
showLogLevelSelect = true
showMoreOptions = false
}
)
DropdownMenuItem( DropdownMenuItem(
leadingIcon = { leadingIcon = {
Icon( Icon(
@@ -345,7 +402,7 @@ fun ApplicationLogsScreen(
logLines logLines
) { line -> ) { line ->
LogCard(line) LogCard(line)
Divider() HorizontalDivider()
} }
} }
} }
@@ -1,6 +1,5 @@
package com.geode.launcher package com.geode.launcher
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Intent import android.content.Intent
@@ -8,7 +7,6 @@ import android.content.IntentFilter
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
@@ -37,7 +35,7 @@ import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener { class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener {
private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null
private val sTag = GeometryDashActivity::class.simpleName private val sTag = GeometryDashActivity::class.simpleName
private var mIsRunning = false private var mIsRunning = false
@@ -59,7 +57,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
try { try {
tryLoadGame() tryLoadGame()
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
e.printStackTrace() Log.e("GeodeLauncher", "Library linkage failure", e)
// generates helpful information for use in debugging library load failures // generates helpful information for use in debugging library load failures
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0) val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
@@ -79,7 +77,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
return return
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e("GeodeLauncher", "Uncaught error during game load", e)
returnToMain( returnToMain(
getString(R.string.load_failed_generic_error), getString(R.string.load_failed_generic_error),
@@ -108,22 +106,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
setupRedirection(gdPackageInfo) setupRedirection(gdPackageInfo)
// request read and write permissions
val hasStoragePerms = (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) and
(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
if (!hasStoragePerms) {
requestPermissions(
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
0
)
}
Cocos2dxHelper.init(this, this) Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this) GeodeUtils.setContext(this)
GeodeUtils.setCapabilityListener(this)
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME) tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME) tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
@@ -136,8 +122,25 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
setupPostLibraryLoad(gdPackageInfo) setupPostLibraryLoad(gdPackageInfo)
if (!loadGeodeLibrary()) { try {
loadGeodeLibrary()
} catch (e: UnsatisfiedLinkError) {
handleGeodeException(e)
} catch (e: Exception) {
handleGeodeException(e)
}
}
private fun handleGeodeException(e: Throwable) {
e.printStackTrace()
val ignoreFailure = PreferenceUtils.get(this)
.getBoolean(PreferenceUtils.Key.IGNORE_LOAD_FAILURE)
if (ignoreFailure) {
Log.w("GeodeLauncher", "could not load Geode object!") Log.w("GeodeLauncher", "could not load Geode object!")
} else {
throw e
} }
} }
@@ -145,7 +148,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
try { try {
LaunchUtils.addAssetsFromPackage(assets, packageInfo) LaunchUtils.addAssetsFromPackage(assets, packageInfo)
} catch (e: NoSuchMethodException) { } catch (e: NoSuchMethodException) {
e.printStackTrace() Log.e("GeodeLauncher", "Failed to add asset redirection", e)
} }
try { try {
@@ -157,7 +160,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR) LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR)
LauncherFix.setDataPath(saveDir.path) LauncherFix.setDataPath(saveDir.path)
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
e.printStackTrace() Log.e("GeodeLauncher", "Failed to load LauncherFix", e)
} }
} }
@@ -209,26 +212,26 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadGeodeLibrary(): Boolean { private fun loadGeodeLibrary() {
// Load Geode if exists // Load Geode if exists
// bundling the object with the application allows for nicer backtraces // bundling the object with the application allows for nicer backtraces
try { try {
// put libgeode.so in jniLibs/armeabi-v7a to get this // put libgeode.so in jniLibs/armeabi-v7a to get this
System.loadLibrary("geode") System.loadLibrary("geode")
return true return
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
// but users may prefer it stored with data // but users may prefer it stored with data
val geodeFilename = LaunchUtils.geodeFilename val geodeFilename = LaunchUtils.geodeFilename
val geodePath = File(filesDir.path, "launcher/$geodeFilename") val geodePath = File(filesDir.path, "launcher/$geodeFilename")
if (geodePath.exists()) { if (geodePath.exists()) {
System.load(geodePath.path) System.load(geodePath.path)
return true return
} }
// you know zmx i have 0 clue what this does so im // you know zmx i have 0 clue what this does so im
// just gonna like copy the binary from external // just gonna like copy the binary from external
// also i get 20 million permission denied errors // also i get 20 million permission denied errors
val externalGeodePath = LaunchUtils.getInstalledGeodePath(this) ?: return false val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!!
val copiedPath = File(filesDir.path, "copied") val copiedPath = File(filesDir.path, "copied")
if (copiedPath.exists()) { if (copiedPath.exists()) {
@@ -245,17 +248,14 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
) )
if (copiedGeodePath.exists()) { if (copiedGeodePath.exists()) {
try { println("Loading Geode from ${externalGeodePath.name}")
println("Loading Geode from ${externalGeodePath.name}") System.load(copiedGeodePath.path)
System.load(copiedGeodePath.path) return
} catch (e: UnsatisfiedLinkError) {
e.printStackTrace()
}
} }
} }
}
return false throw e
}
} }
private fun createView(): FrameLayout { private fun createView(): FrameLayout {
@@ -450,4 +450,13 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
} }
} }
override fun onCapabilityAdded(capability: String): Boolean {
if (capability == GeodeUtils.CAPABILITY_EXTENDED_INPUT) {
mGLSurfaceView?.useKeyboardEvents = true
return true
}
return false
}
} }
@@ -8,6 +8,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -27,8 +29,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -46,7 +51,10 @@ import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import com.geode.launcher.updater.ReleaseManager import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.useCountdownTimer import com.geode.launcher.utils.useCountdownTimer
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import java.net.ConnectException import java.net.ConnectException
import java.net.UnknownHostException import java.net.UnknownHostException
@@ -122,7 +130,7 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String, onDis
Spacer(Modifier.size(4.dp)) Spacer(Modifier.size(4.dp))
TextButton(onClick = { uriHandler.openUri(openTo) },) { TextButton(onClick = { uriHandler.openUri(openTo) }) {
Text(stringResource(R.string.launcher_download)) Text(stringResource(R.string.launcher_download))
} }
} }
@@ -137,7 +145,7 @@ fun UpdateProgressIndicator(
message: String, message: String,
releaseViewModel: ReleaseViewModel, releaseViewModel: ReleaseViewModel,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
progress: Float? = null progress: (() -> Float)? = null
) { ) {
Column( Column(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
@@ -151,7 +159,7 @@ fun UpdateProgressIndicator(
if (progress == null) { if (progress == null) {
LinearProgressIndicator() LinearProgressIndicator()
} else { } else {
LinearProgressIndicator(progress) LinearProgressIndicator(progress = progress)
} }
TextButton( TextButton(
@@ -226,67 +234,91 @@ fun LoadFailedDialog(returnTitle: String, returnMessage: String) {
} }
@Composable @Composable
fun UpdateWarning() { fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val packageManager = context.packageManager val packageManager = context.packageManager
val gdVersionCode = remember { LaunchUtils.getGeometryDashVersionCode(packageManager) } val gdVersionCode = remember { LaunchUtils.getGeometryDashVersionCode(packageManager) }
val gdVersionString = remember { LaunchUtils.getGeometryDashVersionString(packageManager) } val gdVersionString = remember { LaunchUtils.getGeometryDashVersionString(packageManager) }
var showUpdateWarning by remember { mutableStateOf(true) }
var lastDismissedVersion by PreferenceUtils.useLongPreference( var lastDismissedVersion by PreferenceUtils.useLongPreference(
preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE
) )
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && gdVersionCode != lastDismissedVersion) { val canDismissRelease = gdVersionCode >= Constants.SUPPORTED_VERSION_CODE
if (showUpdateWarning) { val shouldDismiss = canDismissRelease && gdVersionCode == lastDismissedVersion
AlertDialog(
icon = {
Icon(
Icons.Filled.Warning,
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
)
},
title = {
Text(stringResource(R.string.launcher_unsupported_version_title))
},
text = {
val message = if (gdVersionCode >= Constants.SUPPORTED_VERSION_CODE) {
R.string.launcher_unsupported_version_description
} else {
R.string.launcher_outdated_version_description
}
Text(stringResource( if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && !shouldDismiss) {
message, gdVersionString, Constants.SUPPORTED_VERSION_STRING AlertDialog(
)) icon = {
}, Icon(
confirmButton = { Icons.Filled.Warning,
TextButton(onClick = { contentDescription = stringResource(R.string.launcher_warning_icon_alt)
showUpdateWarning = false )
lastDismissedVersion = gdVersionCode },
title = {
Text(stringResource(R.string.launcher_unsupported_version_title))
},
text = {
val message = if (gdVersionCode >= Constants.SUPPORTED_VERSION_CODE) {
R.string.launcher_unsupported_version_description
} else {
R.string.launcher_outdated_version_description
}
onLaunch(context) Text(stringResource(
}) { message, gdVersionString, Constants.SUPPORTED_VERSION_STRING
Text(stringResource(R.string.message_box_accept)) ))
} },
}, confirmButton = {
dismissButton = { TextButton(onClick = {
TextButton(onClick = { showUpdateWarning = false }) { onDismiss()
Text(stringResource(R.string.message_box_cancel)) lastDismissedVersion = gdVersionCode
}
}, onLaunch(context, inSafeMode)
onDismissRequest = { showUpdateWarning = false } }) {
) Text(stringResource(R.string.message_box_accept))
} }
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(R.string.message_box_cancel))
}
},
onDismissRequest = { onDismiss() }
)
} else { } else {
LaunchedEffect(gdVersionCode) { LaunchedEffect(gdVersionCode) {
onLaunch(context) onLaunch(context, inSafeMode)
} }
} }
} }
@Composable
fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
AlertDialog(
icon = {
Icon(
Icons.Filled.Warning,
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
)
},
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.message_box_cancel))
}
},
confirmButton = {
TextButton(onClick = onLaunch) {
Text(stringResource(R.string.message_box_continue))
}
},
onDismissRequest = onDismiss
)
}
@Composable @Composable
fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) { fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) {
val context = LocalContext.current val context = LocalContext.current
@@ -317,8 +349,6 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
) )
} }
is ReleaseViewModel.ReleaseUIState.InDownload -> { is ReleaseViewModel.ReleaseUIState.InDownload -> {
val progress = state.downloaded / state.outOf.toDouble()
val downloaded = remember(state.downloaded) { val downloaded = remember(state.downloaded) {
formatShortFileSize(context, state.downloaded) formatShortFileSize(context, state.downloaded)
} }
@@ -335,7 +365,10 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
), ),
modifier = modifier, modifier = modifier,
releaseViewModel = releaseViewModel, releaseViewModel = releaseViewModel,
progress = progress.toFloat() progress = {
val progress = state.downloaded / state.outOf.toDouble()
progress.toFloat()
}
) )
} }
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> { is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
@@ -389,7 +422,13 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
} }
} }
fun onLaunch(context: Context) { fun onLaunch(context: Context, safeMode: Boolean = false) {
if (safeMode) {
GeodeUtils.setAdditionalLaunchArguments(GeodeUtils.ARGUMENT_SAFE_MODE)
} else {
GeodeUtils.clearLaunchArguments()
}
val launchIntent = Intent(context, GeometryDashActivity::class.java) val launchIntent = Intent(context, GeometryDashActivity::class.java)
launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
@@ -410,7 +449,7 @@ fun MainScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val shouldAutomaticallyLaunch = PreferenceUtils.useBooleanPreference( val shouldAutomaticallyLaunch by PreferenceUtils.useBooleanPreference(
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
) )
@@ -423,6 +462,8 @@ fun MainScreen(
val geodeInstalled = geodePreinstalled || geodeJustInstalled val geodeInstalled = geodePreinstalled || geodeJustInstalled
var beginLaunch by remember { mutableStateOf(false) } var beginLaunch by remember { mutableStateOf(false) }
var showSafeModeDialog by remember { mutableStateOf(false) }
var launchInSafeMode by remember { mutableStateOf(false) }
LaunchedEffect(shouldUpdate) { LaunchedEffect(shouldUpdate) {
if (shouldUpdate && !releaseViewModel.hasPerformedCheck) { if (shouldUpdate && !releaseViewModel.hasPerformedCheck) {
@@ -451,15 +492,11 @@ fun MainScreen(
) )
if (gdInstalled && geodeInstalled) { if (gdInstalled && geodeInstalled) {
if (shouldAutomaticallyLaunch.value && !releaseViewModel.isInUpdate && !disableAutomaticLaunch) { val stopLaunch = releaseViewModel.isInUpdate || disableAutomaticLaunch || showSafeModeDialog
if (shouldAutomaticallyLaunch && !stopLaunch) {
val countdownTimer = useCountdownTimer( val countdownTimer = useCountdownTimer(
time = 3000, time = 3000,
onCountdownFinish = { onCountdownFinish = { beginLaunch = true }
// just in case this changed async
if (shouldAutomaticallyLaunch.value) {
beginLaunch = true
}
}
) )
if (countdownTimer != 0L) { if (countdownTimer != 0L) {
@@ -476,10 +513,42 @@ fun MainScreen(
} }
} }
// compose apis don't provide a good way of adding long press to a button
val interactionSource = remember { MutableInteractionSource() }
val viewConfiguration = LocalViewConfiguration.current
val haptics = LocalHapticFeedback.current
LaunchedEffect(interactionSource) {
interactionSource.interactions.collectLatest { interaction ->
when (interaction) {
is PressInteraction.Press -> {
launchInSafeMode = false
delay(viewConfiguration.longPressTimeoutMillis)
// perform a second delay to make the action more obvious
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
delay(viewConfiguration.longPressTimeoutMillis)
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
showSafeModeDialog = true
}
is PressInteraction.Release -> {
if (!showSafeModeDialog) {
beginLaunch = true
}
}
}
}
}
Row { Row {
Button( Button(
onClick = { beginLaunch = true }, onClick = { },
enabled = !releaseViewModel.isInUpdate enabled = !releaseViewModel.isInUpdate,
interactionSource = interactionSource
) { ) {
Icon( Icon(
Icons.Filled.PlayArrow, Icons.Filled.PlayArrow,
@@ -545,7 +614,24 @@ fun MainScreen(
} }
if (beginLaunch) { if (beginLaunch) {
UpdateWarning() UpdateWarning(launchInSafeMode) {
beginLaunch = false
launchInSafeMode = false
}
}
if (showSafeModeDialog) {
SafeModeDialog(
onDismiss = {
showSafeModeDialog = false
},
onLaunch = {
launchInSafeMode = true
beginLaunch = true
showSafeModeDialog = false
}
)
} }
} }
@@ -21,22 +21,18 @@ import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -45,7 +41,6 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.updater.ReleaseViewModel import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme import com.geode.launcher.ui.theme.GeodeLauncherTheme
@@ -84,16 +79,6 @@ class SettingsActivity : ComponentActivity() {
} }
} }
/*
fun onExportSaveData(context: Context) {
Toast.makeText(
context,
"This function is not implemented yet!",
Toast.LENGTH_SHORT
).show()
}
*/
fun onOpenFolder(context: Context) { fun onOpenFolder(context: Context) {
val file = LaunchUtils.getBaseDirectory(context) val file = LaunchUtils.getBaseDirectory(context)
val clipboardManager = val clipboardManager =
@@ -160,7 +145,9 @@ fun UpdateIndicator(
enablePopup = true enablePopup = true
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble() val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
CircularProgressIndicator(progress.toFloat()) CircularProgressIndicator(
progress = { progress.toFloat() },
)
} }
else -> {} else -> {}
} }
@@ -274,7 +261,7 @@ fun SettingsScreen(
navigationIcon = { navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) { IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = context.getString(R.string.back_icon_alt) contentDescription = context.getString(R.string.back_icon_alt)
) )
} }
@@ -315,19 +302,10 @@ fun SettingsScreen(
description = context.getString(R.string.preference_load_automatically_description), description = context.getString(R.string.preference_load_automatically_description),
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY, preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
) )
OptionsButton(
title = context.getString(R.string.preferences_copy_external_button),
description = LaunchUtils.getBaseDirectory(context).path,
onClick = { onOpenFolder(context) }
)
OptionsButton( OptionsButton(
title = stringResource(R.string.preferences_open_file_manager), title = stringResource(R.string.preferences_open_file_manager),
onClick = { onOpenFileManager(context) } onClick = { onOpenFileManager(context) }
) )
OptionsButton(
title = stringResource(R.string.preferences_view_logs),
onClick = { onOpenLogs(context) }
)
} }
OptionsGroup(context.getString(R.string.preference_category_updater)) { OptionsGroup(context.getString(R.string.preference_category_updater)) {
@@ -365,6 +343,27 @@ fun SettingsScreen(
} }
} }
OptionsGroup(stringResource(R.string.preference_category_developer)) {
SettingsStringCard(
title = stringResource(R.string.preference_launch_arguments_name),
dialogTitle = stringResource(R.string.preference_launch_arguments_set_title),
preferenceKey = PreferenceUtils.Key.LAUNCH_ARGUMENTS,
filterInput = { it.filter { c ->
// if only there was a better way to define this!
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(){}<>[]?:;'\"~`-_+=\\| ".contains(c)
}}
)
OptionsButton(
title = context.getString(R.string.preferences_copy_external_button),
description = LaunchUtils.getBaseDirectory(context).path,
onClick = { onOpenFolder(context) }
)
OptionsButton(
title = stringResource(R.string.preferences_view_logs),
onClick = { onOpenLogs(context) }
)
}
Text( Text(
stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME), stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME),
style = Typography.labelSmall, style = Typography.labelSmall,
@@ -375,240 +374,6 @@ fun SettingsScreen(
) )
} }
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
return preferences.toggleBoolean(preferenceKey)
}
fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
return preferences.getBoolean(preferenceKey)
}
@Composable
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
Column {
Text(
title,
style = Typography.titleMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
content()
}
}
@Composable
fun SettingsSelectCard(
title: String,
dialogTitle: String,
maxVal: Int,
preferenceKey: PreferenceUtils.Key,
toLabel: @Composable (Int) -> String,
extraSelectBehavior: ((Int) -> Unit)? = null
) {
val preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
var showDialog by remember { mutableStateOf(false) }
OptionsCard(
title = { OptionsTitle(title = title) },
modifier = Modifier
.clickable(
onClick = {
showDialog = true
},
role = Role.Button
)
) {
Text(toLabel(preferenceValue))
}
if (showDialog) {
val context = LocalContext.current
SelectDialog(
title = dialogTitle,
onDismissRequest = {
showDialog = false
},
onSelect = { selected ->
showDialog = false
PreferenceUtils.get(context)
.setInt(preferenceKey, selected)
extraSelectBehavior?.invoke(selected)
},
initialValue = preferenceValue,
toLabel = toLabel,
optionsCount = maxVal
)
}
}
@Composable
fun SelectDialog(
title: String,
onDismissRequest: () -> Unit,
onSelect: (Int) -> Unit,
initialValue: Int,
toLabel: @Composable (Int) -> String,
optionsCount: Int,
) {
var selectedValue by remember { mutableIntStateOf(initialValue) }
Dialog(onDismissRequest = onDismissRequest) {
Card(
shape = RoundedCornerShape(16.dp)
) {
// styling a dialog is actually a little hard if you're doing what i'm doing
// maybe there's a better way to make these padding values...
Column {
Text(
title,
style = Typography.titleLarge,
modifier = Modifier.padding(
start = 28.dp,
top = 24.dp,
bottom = 12.dp
)
)
// do not give the row or column padding!! it messes up the selection effect
(0..optionsCount).forEach { id ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { selectedValue = id },
role = Role.RadioButton
)
.padding(horizontal = 12.dp)
) {
RadioButton(
selected = selectedValue == id,
onClick = { selectedValue = id }
)
Text(
toLabel(id),
style = Typography.bodyMedium
)
}
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 16.dp,
end = 16.dp,
top = 4.dp
)
) {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
TextButton(onClick = { onSelect(selectedValue) }) {
Text(stringResource(R.string.message_box_accept))
}
}
}
}
}
}
@Composable
fun OptionsButton(title: String, description: String? = null, onClick: () -> Unit) {
OptionsCard(
title = {
OptionsTitle(
title = title,
description = description
)
},
modifier = Modifier
.clickable(onClick = onClick, role = Role.Button)
) { }
}
@Composable
fun SettingsCard(title: String, description: String? = null, preferenceKey: PreferenceUtils.Key) {
val context = LocalContext.current
val settingEnabled = remember {
mutableStateOf(getSetting(context, preferenceKey))
}
OptionsCard(
title = {
OptionsTitle(
Modifier.fillMaxWidth(0.75f),
title = title,
description = description
)
},
modifier = Modifier.toggleable(
value = settingEnabled.value,
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
role = Role.Switch,
)
) {
Switch(checked = settingEnabled.value, onCheckedChange = null)
}
}
@Composable
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null) {
Column(
modifier,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(title)
if (!description.isNullOrEmpty()) {
Text(
description,
style = Typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
@Composable
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) {
Row(
modifier
.fillMaxWidth()
.height(64.dp)
.padding(horizontal = 16.dp),
Arrangement.SpaceBetween,
Alignment.CenterVertically,
) {
title()
content()
}
}
@Preview(showBackground = true)
@Composable
fun OptionsCardPreview() {
GeodeLauncherTheme {
OptionsGroup(title = "Preview Group") {
SettingsCard(
title = "Load files from /test",
description = "Very long testing description goes here. It is incredibly long, it should wrap onto a new line.",
preferenceKey = PreferenceUtils.Key.LOAD_TESTING
)
SettingsCard(
title = "Testing option 2",
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
)
}
}
}
@Preview(showSystemUi = true) @Preview(showSystemUi = true)
@Composable @Composable
fun DefaultPreview() { fun DefaultPreview() {
@@ -0,0 +1,366 @@
package com.geode.launcher
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.PreferenceUtils
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
return preferences.toggleBoolean(preferenceKey)
}
fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
return preferences.getBoolean(preferenceKey)
}
@Composable
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
Column {
Text(
title,
style = Typography.titleMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
content()
}
}
@Composable
fun SettingsSelectCard(
title: String,
dialogTitle: String,
maxVal: Int,
preferenceKey: PreferenceUtils.Key,
toLabel: @Composable (Int) -> String,
extraSelectBehavior: ((Int) -> Unit)? = null
) {
val preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
var showDialog by remember { mutableStateOf(false) }
OptionsCard(
title = { OptionsTitle(title = title) },
modifier = Modifier
.clickable(
onClick = {
showDialog = true
},
role = Role.Button
)
) {
Text(toLabel(preferenceValue))
}
if (showDialog) {
val context = LocalContext.current
SelectDialog(
title = dialogTitle,
onDismissRequest = {
showDialog = false
},
onSelect = { selected ->
showDialog = false
PreferenceUtils.get(context)
.setInt(preferenceKey, selected)
extraSelectBehavior?.invoke(selected)
},
initialValue = preferenceValue,
toLabel = toLabel,
optionsCount = 0..maxVal
)
}
}
@Composable
fun SettingsStringCard(
title: String,
dialogTitle: String,
preferenceKey: PreferenceUtils.Key,
filterInput: ((String) -> String)? = null
) {
var preferenceValue by PreferenceUtils.useStringPreference(preferenceKey)
var showDialog by remember { mutableStateOf(false) }
OptionsCard(
title = {
OptionsTitle(title = title, description = preferenceValue)
},
modifier = Modifier
.clickable(
onClick = {
showDialog = true
},
role = Role.Button
)
) { }
if (showDialog) {
StringDialog(
title = dialogTitle,
onDismissRequest = { showDialog = false },
onSelect = {
preferenceValue = it
showDialog = false
},
initialValue = preferenceValue ?: "",
filterInput = filterInput
)
}
}
@Composable
fun StringDialog(
title: String,
onDismissRequest: () -> Unit,
onSelect: (String) -> Unit,
initialValue: String,
filterInput: ((String) -> String)? = null
) {
var enteredValue by remember { mutableStateOf(initialValue) }
AlertDialog(
onDismissRequest = { onDismissRequest() },
title = {
Text(title)
},
text = {
OutlinedTextField(
value = enteredValue,
onValueChange = {
enteredValue = filterInput?.invoke(it) ?: it
},
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Ascii,
autoCorrect = false
),
trailingIcon = {
if (enteredValue.isNotEmpty()) {
IconButton(onClick = { enteredValue = "" }) {
Icon(
Icons.Filled.Clear,
contentDescription = stringResource(R.string.preference_text_clear)
)
}
}
}
)
},
confirmButton = {
TextButton(onClick = { onSelect(enteredValue) }) {
Text(stringResource(R.string.message_box_accept))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
},
)
}
@Composable
fun <T> SelectDialog(
title: String,
onDismissRequest: () -> Unit,
onSelect: (T) -> Unit,
initialValue: T,
toLabel: @Composable (T) -> String,
optionsCount: Iterable<T>,
) {
var selectedValue by remember { mutableStateOf(initialValue) }
Dialog(onDismissRequest = onDismissRequest) {
Card(
shape = RoundedCornerShape(16.dp)
) {
// styling a dialog is actually a little hard if you're doing what i'm doing
// maybe there's a better way to make these padding values...
Column {
Text(
title,
style = Typography.titleLarge,
modifier = Modifier.padding(
start = 28.dp,
top = 24.dp,
bottom = 12.dp
)
)
// do not give the row or column padding!! it messes up the selection effect
optionsCount.forEach { id ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { selectedValue = id },
role = Role.RadioButton
)
.padding(horizontal = 12.dp)
) {
RadioButton(
selected = selectedValue == id,
onClick = { selectedValue = id }
)
Text(
toLabel(id),
style = Typography.bodyMedium
)
}
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 16.dp,
end = 16.dp,
top = 4.dp
)
) {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
TextButton(onClick = { onSelect(selectedValue) }) {
Text(stringResource(R.string.message_box_accept))
}
}
}
}
}
}
@Composable
fun OptionsButton(title: String, description: String? = null, onClick: () -> Unit) {
OptionsCard(
title = {
OptionsTitle(
title = title,
description = description
)
},
modifier = Modifier
.clickable(onClick = onClick, role = Role.Button)
) { }
}
@Composable
fun SettingsCard(title: String, description: String? = null, preferenceKey: PreferenceUtils.Key) {
val context = LocalContext.current
val settingEnabled = remember {
mutableStateOf(getSetting(context, preferenceKey))
}
OptionsCard(
title = {
OptionsTitle(
Modifier.fillMaxWidth(0.75f),
title = title,
description = description
)
},
modifier = Modifier.toggleable(
value = settingEnabled.value,
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
role = Role.Switch,
)
) {
Switch(checked = settingEnabled.value, onCheckedChange = null)
}
}
@Composable
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null) {
Column(
modifier,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(title)
if (!description.isNullOrEmpty()) {
Text(
description,
style = Typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
@Composable
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) {
Row(
modifier
.fillMaxWidth()
.height(64.dp)
.padding(horizontal = 16.dp),
Arrangement.SpaceBetween,
Alignment.CenterVertically,
) {
title()
content()
}
}
@Preview(showBackground = true)
@Composable
fun OptionsCardPreview() {
GeodeLauncherTheme {
OptionsGroup(title = "Preview Group") {
SettingsCard(
title = "Load files from /test",
description = "Very long testing description goes here. It is incredibly long, it should wrap onto a new line.",
preferenceKey = PreferenceUtils.Key.LOAD_TESTING
)
SettingsCard(
title = "Testing option 2",
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
)
}
}
}
@@ -3,11 +3,13 @@ package com.geode.launcher
import android.database.Cursor import android.database.Cursor
import android.database.MatrixCursor import android.database.MatrixCursor
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.CancellationSignal import android.os.CancellationSignal
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.DocumentsProvider import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import java.io.File import java.io.File
@@ -150,7 +152,7 @@ class UserDirectoryProvider : DocumentsProvider() {
notifyFileChange(file) notifyFileChange(file)
} }
override fun renameDocument(documentId: String, displayName: String): String? { override fun renameDocument(documentId: String, displayName: String): String {
val file = getFileForDocumentId(documentId) val file = getFileForDocumentId(documentId)
val dest = findFileNameForNewFile(File(file.parentFile, displayName)) val dest = findFileNameForNewFile(File(file.parentFile, displayName))
file.renameTo(dest) file.renameTo(dest)
@@ -203,4 +205,19 @@ class UserDirectoryProvider : DocumentsProvider() {
val uri = getDocumentUri(parentDocumentId) val uri = getDocumentUri(parentDocumentId)
context!!.contentResolver.notifyChange(uri, null) context!!.contentResolver.notifyChange(uri, null)
} }
@RequiresApi(Build.VERSION_CODES.O)
override fun findDocumentPath(
parentDocumentId: String?,
childDocumentId: String
): DocumentsContract.Path {
if (!parentDocumentId.isNullOrEmpty()) {
// not implementing this for now...
return super.findDocumentPath(parentDocumentId, childDocumentId)
}
val nonRootPath = listOf(ROOT) + childDocumentId.removePrefix(ROOT).split("/")
return DocumentsContract.Path(ROOT, nonRootPath)
}
} }
@@ -29,9 +29,50 @@ data class ProcessInformation(val processId: Int, val threadId: Int, val process
enum class LogPriority { enum class LogPriority {
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT; UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
class LogPriorityIterator(
start: LogPriority,
private val end: LogPriority,
private val step: Int
) : Iterator<LogPriority> {
private var hasNext: Boolean = if (step > 0) start <= end else start >= end
private var next = if (hasNext) start else end
override fun hasNext() = hasNext
override fun next(): LogPriority {
val current = next
if (current == end) {
if (!hasNext) {
throw NoSuchElementException()
}
hasNext = false
} else {
next = LogPriority.fromInt(next.toInt() + step)
}
return current
}
}
class LogPriorityProgression(
override val start: LogPriority,
override val endInclusive: LogPriority,
private val step: Int = 1
) : Iterable<LogPriority>, ClosedRange<LogPriority> {
override fun iterator(): Iterator<LogPriority> {
return LogPriorityIterator(start, endInclusive, step)
}
}
infix fun downTo(to: LogPriority) = LogPriorityProgression(this, to, -1)
operator fun rangeTo(to: LogPriority) = LogPriorityProgression(this, to)
companion object { companion object {
fun fromByte(byte: Byte): LogPriority { fun fromByte(byte: Byte): LogPriority = fromInt(byte.toInt())
return when (byte.toInt()) {
fun fromInt(int: Int) = when (int) {
0x1 -> DEFAULT 0x1 -> DEFAULT
0x2 -> VERBOSE 0x2 -> VERBOSE
0x3 -> DEBUG 0x3 -> DEBUG
@@ -41,10 +82,21 @@ enum class LogPriority {
0x7 -> FATAL 0x7 -> FATAL
0x8 -> SILENT 0x8 -> SILENT
else -> UNKNOWN else -> UNKNOWN
}
} }
} }
fun toInt() = when (this) {
DEFAULT -> 0x1
VERBOSE -> 0x2
DEBUG -> 0x3
INFO -> 0x4
WARN -> 0x5
ERROR -> 0x6
FATAL -> 0x7
SILENT -> 0x8
else -> 0x0
}
fun toChar(): Char { fun toChar(): Char {
return when (this) { return when (this) {
VERBOSE -> 'V' VERBOSE -> 'V'
@@ -37,6 +37,15 @@ class LogViewModel: ViewModel() {
var filterCrashes = false var filterCrashes = false
private set private set
// skip debug/verbose lines by default
var logLevel = LogPriority.INFO
set(value) {
field = value
_lineState.clear()
loadLogs()
}
fun toggleCrashBuffer() { fun toggleCrashBuffer() {
filterCrashes = !filterCrashes filterCrashes = !filterCrashes
@@ -70,8 +79,7 @@ class LogViewModel: ViewModel() {
while (true) { while (true) {
val line = LogLine.fromBufferedSource(logSource) val line = LogLine.fromBufferedSource(logSource)
// skip debug/verbose lines if (line.priority >= logLevel) {
if (line.priority >= LogPriority.INFO) {
logLines += line logLines += line
} }
@@ -217,7 +217,7 @@ class ReleaseManager private constructor(
val geodeFile = getGeodeOutputPath() val geodeFile = getGeodeOutputPath()
// check if an update is needed // check if an update is needed
if (latestVersion <= currentVersion && geodeFile.exists()) { if (latestVersion == currentVersion && geodeFile.exists()) {
_uiState.value = ReleaseManagerState.Finished() _uiState.value = ReleaseManagerState.Finished()
return return
} }
@@ -1,5 +1,6 @@
package com.geode.launcher.utils package com.geode.launcher.utils
import android.Manifest
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
@@ -7,13 +8,20 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.geode.launcher.BuildConfig
import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
@@ -21,7 +29,7 @@ import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.system.exitProcess import kotlin.system.exitProcess
@Suppress("unused") @Suppress("unused", "KotlinJniMissingFunction")
object GeodeUtils { object GeodeUtils {
private lateinit var activity: WeakReference<AppCompatActivity> private lateinit var activity: WeakReference<AppCompatActivity>
private lateinit var openFileResultLauncher: ActivityResultLauncher<GeodeOpenFileActivityResult.OpenFileParams> private lateinit var openFileResultLauncher: ActivityResultLauncher<GeodeOpenFileActivityResult.OpenFileParams>
@@ -29,6 +37,11 @@ object GeodeUtils {
private lateinit var openFilesResultLauncher: ActivityResultLauncher<GeodeOpenFilesActivityResult.OpenFileParams> private lateinit var openFilesResultLauncher: ActivityResultLauncher<GeodeOpenFilesActivityResult.OpenFileParams>
private lateinit var saveFileResultLauncher: ActivityResultLauncher<GeodeSaveFileActivityResult.SaveFileParams> private lateinit var saveFileResultLauncher: ActivityResultLauncher<GeodeSaveFileActivityResult.SaveFileParams>
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String> private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private lateinit var internalRequestPermissionsLauncher: ActivityResultLauncher<Array<String>>
private lateinit var internalRequestAllFilesLauncher: ActivityResultLauncher<Intent>
private var afterRequestPermissions: (() -> Unit)? = null
private var afterRequestPermissionsFailure: (() -> Unit)? = null
fun setContext(activity: AppCompatActivity) { fun setContext(activity: AppCompatActivity) {
this.activity = WeakReference(activity) this.activity = WeakReference(activity)
@@ -79,6 +92,32 @@ object GeodeUtils {
requestPermissionLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> requestPermissionLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
permissionCallback(isGranted) permissionCallback(isGranted)
} }
internalRequestPermissionsLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val isGranted = permissions.values.all { it }
if (isGranted) {
afterRequestPermissions?.invoke()
} else {
Toast.makeText(activity, R.string.missing_permissions, Toast.LENGTH_SHORT)
.show()
afterRequestPermissionsFailure?.invoke()
}
}
// only necessary on newer android versions
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) {
internalRequestAllFilesLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
if (Environment.isExternalStorageManager()) {
afterRequestPermissions?.invoke()
} else {
Toast.makeText(activity, R.string.missing_permissions, Toast.LENGTH_SHORT)
.show()
afterRequestPermissionsFailure?.invoke()
}
}
}
} }
@JvmStatic @JvmStatic
@@ -129,99 +168,175 @@ object GeodeUtils {
// TODO As of now this is unused // TODO As of now this is unused
@JvmStatic @JvmStatic
fun openFolder(path: String): Boolean { fun openFolder(path: String): Boolean {
activity.get()?.run { val context = activity.get()!!
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
}
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) { val pathFile = File(path)
startActivity(intent) val baseDirectory = LaunchUtils.getBaseDirectory(context)
return true val isInternalPath = pathFile.startsWith(baseDirectory)
val intent = if (isInternalPath) {
// TODO: figure out how to get this to point to the path it should be pointing at
// (the best i got was pointing at a file)
// val relativePath = pathFile.relativeTo(baseDirectory)
Intent(Intent.ACTION_VIEW).apply {
data = DocumentsContract.buildRootUri(
"${context.packageName}.user", UserDirectoryProvider.ROOT
)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
)
}
} else {
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addCategory(Intent.CATEGORY_OPENABLE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val documentFile = DocumentFile.fromFile(File(path))
putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.uri)
}
type = "*/*"
} }
} }
return false
return try {
context.startActivity(intent)
true
} catch (_: ActivityNotFoundException) {
false
}
} }
@Suppress("KotlinJniMissingFunction") private external fun selectFileCallback(path: String)
external fun selectFileCallback(path: String)
@Suppress("KotlinJniMissingFunction") private external fun selectFilesCallback(paths: Array<String>)
external fun selectFilesCallback(paths: Array<String>)
@Suppress("KotlinJniMissingFunction") private external fun failedCallback()
external fun failedCallback()
private fun checkForFilePermissions(onSuccess: () -> Unit, onFailure: () -> Unit) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) {
if (Environment.isExternalStorageManager()) {
onSuccess()
} else {
val intent = Intent(
ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
)
internalRequestAllFilesLauncher.launch(intent)
afterRequestPermissions = onSuccess
afterRequestPermissionsFailure = onFailure
}
} else {
val permissions = listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
val context = activity.get()!!
val needsPermissions = permissions.filter {
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
}
if (needsPermissions.isNotEmpty()) {
internalRequestPermissionsLauncher.launch(needsPermissions.toTypedArray())
afterRequestPermissions = onSuccess
afterRequestPermissionsFailure = onFailure
} else {
onSuccess()
}
}
}
@JvmStatic @JvmStatic
fun selectFile(path: String): Boolean { fun selectFile(path: String): Boolean {
activity.get()?.run { var uri: Uri?
var uri: Uri? DocumentFile.fromFile(File(path)).also {
DocumentFile.fromFile(File(path)).also { uri = it.uri
uri = it.uri }
}
try { return try {
openFileResultLauncher.launch(GeodeOpenFileActivityResult.OpenFileParams(arrayOf("*/*"), uri)) checkForFilePermissions(
return true onSuccess = {
} catch (e: ActivityNotFoundException) { openFileResultLauncher.launch(GeodeOpenFileActivityResult.OpenFileParams(arrayOf("*/*"), uri))
return false },
} onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
} }
return false
} }
@JvmStatic @JvmStatic
fun selectFiles(path: String): Boolean { fun selectFiles(path: String): Boolean {
activity.get()?.run { var uri: Uri?
var uri: Uri? DocumentFile.fromFile(File(path)).also {
DocumentFile.fromFile(File(path)).also { uri = it.uri
uri = it.uri }
}
try { return try {
openFilesResultLauncher.launch(GeodeOpenFilesActivityResult.OpenFileParams(arrayOf("*/*"), uri)) checkForFilePermissions(
return true onSuccess = {
} catch (e: ActivityNotFoundException) { openFilesResultLauncher.launch(GeodeOpenFilesActivityResult.OpenFileParams(arrayOf("*/*"), uri))
return false },
} onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
} }
return false
} }
@JvmStatic @JvmStatic
fun selectFolder(path: String): Boolean { fun selectFolder(path: String): Boolean {
activity.get()?.run { var uri: Uri?
var uri: Uri? DocumentFile.fromFile(File(path)).also {
DocumentFile.fromFile(File(path)).also { uri = it.uri
uri = it.uri
}
try {
openDirectoryResultLauncher.launch(uri)
return true
} catch (e: ActivityNotFoundException) {
return false
}
} }
return false
return try {
checkForFilePermissions(
onSuccess = {
openDirectoryResultLauncher.launch(uri)
},
onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
}
} }
@JvmStatic @JvmStatic
fun createFile(path: String): Boolean { fun createFile(path: String): Boolean {
activity.get()?.run { var uri: Uri?
var uri: Uri? DocumentFile.fromFile(File(path)).also {
DocumentFile.fromFile(File(path)).also { uri = it.uri
uri = it.uri }
}
return try {
try { checkForFilePermissions(
saveFileResultLauncher.launch(GeodeSaveFileActivityResult.SaveFileParams(null, uri)) onSuccess = {
return true saveFileResultLauncher.launch(GeodeSaveFileActivityResult.SaveFileParams(null, uri))
} catch (e: ActivityNotFoundException) { },
return false onFailure = { failedCallback() }
} )
true
} catch (e: ActivityNotFoundException) {
false
} }
return false
} }
@JvmStatic @JvmStatic
@@ -256,34 +371,105 @@ object GeodeUtils {
return "com.geode.launcher.user" == uri.authority return "com.geode.launcher.user" == uri.authority
} }
private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal"
private const val MANAGE_ALL_FILES = "${INTERNAL_PERMISSION_PREFIX}.MANAGE_ALL_FILES"
@JvmStatic @JvmStatic
fun getPermissionStatus(permission: String): Boolean { fun getPermissionStatus(permission: String): Boolean {
return ContextCompat.checkSelfPermission( val context = activity.get() ?: return false
activity.get()?.applicationContext ?: return false,
permission return when (permission) {
) == PackageManager.PERMISSION_GRANTED MANAGE_ALL_FILES -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) {
Environment.isExternalStorageManager()
} else {
val permissions = listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
return permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
}
else -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
} }
@JvmStatic @JvmStatic
fun requestPermission(permission: String) { fun requestPermission(permission: String) {
if (permission == MANAGE_ALL_FILES) {
// this function handles already having perms for us
checkForFilePermissions(onSuccess = {
permissionCallback(true)
}, onFailure = {
permissionCallback(false)
})
return
}
if (getPermissionStatus(permission)) { if (getPermissionStatus(permission)) {
permissionCallback(true) permissionCallback(true)
return return
} }
activity.get()?.run { try {
try { requestPermissionLauncher.launch(permission)
requestPermissionLauncher.launch(permission) } catch (e: ActivityNotFoundException) {
} catch (e: ActivityNotFoundException) { permissionCallback(false)
permissionCallback(false)
}
return
} }
permissionCallback(false)
} }
@Suppress("KotlinJniMissingFunction") private external fun permissionCallback(granted: Boolean)
external fun permissionCallback(granted: Boolean)
const val ARGUMENT_SAFE_MODE = "--geode:safe-mode"
private var additionalLaunchArguments = arrayListOf<String>()
fun setAdditionalLaunchArguments(vararg args: String) {
additionalLaunchArguments.addAll(args)
}
fun clearLaunchArguments() = setAdditionalLaunchArguments()
@JvmStatic
fun getLaunchArguments(): String? {
activity.get()?.apply {
val preferences = PreferenceUtils.get(this)
val userArgs = preferences.getString(PreferenceUtils.Key.LAUNCH_ARGUMENTS)
val args = if (!userArgs.isNullOrEmpty()) {
listOf(userArgs) + additionalLaunchArguments
} else additionalLaunchArguments
return args.joinToString(" ")
}
return null
}
interface CapabilityListener {
fun onCapabilityAdded(capability: String): Boolean
}
const val CAPABILITY_EXTENDED_INPUT = "extended_input"
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
fun setCapabilityListener(listener: CapabilityListener) {
capabilityListener = WeakReference(listener)
}
@JvmStatic
fun reportPlatformCapability(capability: String?): Boolean {
if (capability.isNullOrEmpty()) {
return false
}
return capabilityListener.get()?.onCapabilityAdded(capability) ?: false
}
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean)
external fun nativeActionScroll(scrollX: Float, scrollY: Float)
external fun resizeSurface(width: Int, height: Int)
} }
@@ -133,7 +133,9 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
BLACK_BACKGROUND, BLACK_BACKGROUND,
CURRENT_RELEASE_MODIFIED, CURRENT_RELEASE_MODIFIED,
LAST_DISMISSED_UPDATE, LAST_DISMISSED_UPDATE,
DISMISSED_GJ_UPDATE DISMISSED_GJ_UPDATE,
LAUNCH_ARGUMENTS,
IGNORE_LOAD_FAILURE
} }
private fun defaultValueForBooleanKey(key: Key): Boolean { private fun defaultValueForBooleanKey(key: Key): Boolean {
@@ -157,6 +159,8 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash" Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash"
Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate" Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate" Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate"
Key.LAUNCH_ARGUMENTS -> "PreferenceLaunchArguments"
Key.IGNORE_LOAD_FAILURE -> "PreferenceIgnoreLoadFailure"
} }
} }
@@ -10,6 +10,7 @@ import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import com.customRobTop.BaseRobTopActivity import com.customRobTop.BaseRobTopActivity
import com.geode.launcher.utils.GeodeUtils
private const val HANDLER_OPEN_IME_KEYBOARD = 2 private const val HANDLER_OPEN_IME_KEYBOARD = 2
private const val HANDLER_CLOSE_IME_KEYBOARD = 3 private const val HANDLER_CLOSE_IME_KEYBOARD = 3
@@ -37,6 +38,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
private lateinit var cocos2dxRenderer: Cocos2dxRenderer private lateinit var cocos2dxRenderer: Cocos2dxRenderer
var useKeyboardEvents = false
set(value) {
field = value
cocos2dxRenderer.sendResizeEvents = value
}
var cocos2dxEditText: Cocos2dxEditText? = null var cocos2dxEditText: Cocos2dxEditText? = null
set(value) { set(value) {
@@ -207,7 +213,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
} }
override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean { private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
return when (keyCode) { return when (keyCode) {
KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_MENU -> { KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_MENU -> {
if (keyEvent.repeatCount != 0 || BaseRobTopActivity.blockBackButton) { if (keyEvent.repeatCount != 0 || BaseRobTopActivity.blockBackButton) {
@@ -220,6 +226,71 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
} }
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (!useKeyboardEvents) {
return legacyKeyDown(keyCode, event)
}
return when (keyCode) {
// ignore system keys
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> {
super.onKeyDown(keyCode, event)
}
else -> {
if (BaseRobTopActivity.blockBackButton) {
return true
}
queueEvent {
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
}
true
}
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
if (!useKeyboardEvents) {
return super.onKeyUp(keyCode, event)
}
return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> {
super.onKeyUp(keyCode, event)
}
else -> {
if (event.repeatCount != 0 || BaseRobTopActivity.blockBackButton) {
return true
}
queueEvent {
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
}
true
}
}
}
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
if (!useKeyboardEvents) {
return super.onGenericMotionEvent(event)
}
if (event?.action == MotionEvent.ACTION_SCROLL) {
val scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL)
val scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
queueEvent {
GeodeUtils.nativeActionScroll(scrollX, scrollY)
}
return true
}
return super.onGenericMotionEvent(event)
}
fun setCocos2dxRenderer(renderer: Cocos2dxRenderer) { fun setCocos2dxRenderer(renderer: Cocos2dxRenderer) {
this.cocos2dxRenderer = renderer this.cocos2dxRenderer = renderer
setRenderer(this.cocos2dxRenderer) setRenderer(this.cocos2dxRenderer)
@@ -59,6 +59,7 @@ class Cocos2dxRenderer : GLSurfaceView.Renderer {
private var lastTickInNanoSeconds: Long = 0 private var lastTickInNanoSeconds: Long = 0
private var screenWidth = 0 private var screenWidth = 0
private var screenHeight = 0 private var screenHeight = 0
var sendResizeEvents = false
fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) { fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) {
screenWidth = surfaceWidth screenWidth = surfaceWidth
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,760Q546,760 593,713Q640,666 640,600L640,440Q640,374 593,327Q546,280 480,280Q414,280 367,327Q320,374 320,440L320,600Q320,666 367,713Q414,760 480,760ZM400,640L560,640L560,560L400,560L400,640ZM400,480L560,480L560,400L400,400L400,480ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520ZM480,840Q415,840 359.5,808Q304,776 272,720L160,720L160,640L244,640Q241,620 240.5,600Q240,580 240,560L160,560L160,480L240,480Q240,460 240.5,440Q241,420 244,400L160,400L160,320L272,320Q286,297 303.5,277Q321,257 344,242L280,176L336,120L422,206Q450,197 479,197Q508,197 536,206L624,120L680,176L614,242Q637,257 655.5,276.5Q674,296 688,320L800,320L800,400L716,400Q719,420 719.5,440Q720,460 720,480L800,480L800,560L720,560Q720,580 719.5,600Q719,620 716,640L800,640L800,720L688,720Q656,776 600.5,808Q545,840 480,840Z"/>
</vector>
+14 -2
View File
@@ -30,6 +30,8 @@
</string> </string>
<string name="launcher_update_available">A new launcher update is available!</string> <string name="launcher_update_available">A new launcher update is available!</string>
<string name="launcher_update_dismiss">Dismiss</string> <string name="launcher_update_dismiss">Dismiss</string>
<string name="safe_mode_enable_title">Enable safe mode?</string>
<string name="safe_mode_enable_description">Safe mode disables loading of all mods, allowing one to remove problematic mods or diagnose crashes. If you\'re not experiencing issues with Geode, you should probably not be using safe mode.</string>
<!-- Updater strings --> <!-- Updater strings -->
<string name="release_fetch_failed">Update failed.\n%1$s</string> <string name="release_fetch_failed">Update failed.\n%1$s</string>
@@ -46,13 +48,15 @@
<!-- GeometryDashActivity strings --> <!-- GeometryDashActivity strings -->
<string name="load_failed_link_error">Failed to link libraries</string> <string name="load_failed_link_error">Failed to link libraries</string>
<string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture of the launcher and Geometry Dash do not match, or from a system bug.\nCheck application logs for more details.</string> <string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture or version of the launcher and Geometry Dash do not match, or from a system bug.\nCheck application logs for more details.</string>
<string name="load_failed_abi_error_need_64bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 64-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string> <string name="load_failed_abi_error_need_64bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 64-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
<string name="load_failed_abi_error_need_32bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 32-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string> <string name="load_failed_abi_error_need_32bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 32-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
<string name="load_failed_generic_error">Load failed</string> <string name="load_failed_generic_error">Load failed</string>
<string name="load_failed_generic_error_description">Uncaught error during game load: `%1$s`.\nCheck application logs for more details. Please report this issue!</string> <string name="load_failed_generic_error_description">Uncaught error during game load: `%1$s`.\nCheck application logs for more details.</string>
<string name="message_box_cancel">Cancel</string> <string name="message_box_cancel">Cancel</string>
<string name="message_box_accept">OK</string> <string name="message_box_accept">OK</string>
<string name="message_box_continue">Continue</string>
<string name="missing_permissions">Missing required permissions for this action!</string>
<!-- SettingsActivity strings --> <!-- SettingsActivity strings -->
<string name="title_activity_settings">Settings</string> <string name="title_activity_settings">Settings</string>
@@ -90,6 +94,12 @@
<string name="preference_check_for_updates_no_internet">No internet found.</string> <string name="preference_check_for_updates_no_internet">No internet found.</string>
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string> <string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
<string name="preference_category_developer">Developer</string>
<string name="preference_launch_arguments_name">Launch arguments</string>
<string name="preference_launch_arguments_set_title">Set launch arguments</string>
<string name="preference_ignore_load_failure_name">Ignore loader errors</string>
<string name="preference_text_clear">Clear</string>
<!-- ApplicationLogsActivity strings --> <!-- ApplicationLogsActivity strings -->
<string name="title_activity_application_logs">Application Logs</string> <string name="title_activity_application_logs">Application Logs</string>
<string name="application_logs_no_logs">No logs available.</string> <string name="application_logs_no_logs">No logs available.</string>
@@ -98,6 +108,8 @@
<string name="application_log_copy_label">Log Message</string> <string name="application_log_copy_label">Log Message</string>
<string name="application_logs_more">More</string> <string name="application_logs_more">More</string>
<string name="application_logs_export">Save To</string> <string name="application_logs_export">Save To</string>
<string name="application_logs_level">Set Log Level</string>
<string name="application_logs_level_dialog_name">Set minimum log level</string>
<string name="application_logs_export_completed">Log export complete.</string> <string name="application_logs_export_completed">Log export complete.</string>
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string> <string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
<string name="application_logs_crash_only">Show Crash Buffer</string> <string name="application_logs_crash_only">Show Crash Buffer</string>
-16
View File
@@ -1,16 +0,0 @@
buildscript {
ext {
compose_version = '1.5.4'
compose_compiler = '1.5.7'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.2.1' apply false
id 'com.android.library' version '8.2.1' apply false
id 'org.jetbrains.kotlin.android' version '1.9.21' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21' apply false
}
tasks.register('clean', Delete) {
delete rootProject.buildDir
}
+16
View File
@@ -0,0 +1,16 @@
buildscript {
val composeBOM by extra("2024.02.00")
val composeCompiler by extra("1.5.9")
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.2.2" apply false
id("com.android.library") version "8.2.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22" apply false
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+3 -1
View File
@@ -20,4 +20,6 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the # Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
# nice optimization
org.gradle.configuration-cache=true
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+7 -7
View File
@@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -202,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+4 -1
View File
@@ -5,6 +5,8 @@ pluginManagement {
mavenCentral() mavenCentral()
} }
} }
@Suppress("UnstableApiUsage")
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
@@ -12,5 +14,6 @@ dependencyResolutionManagement {
mavenCentral() mavenCentral()
} }
} }
rootProject.name = "launcher" rootProject.name = "launcher"
include ':app' include(":app")