Compare commits

...
20 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
20 changed files with 562 additions and 250 deletions

No files matched your search

-4
View File
@@ -1,7 +1,3 @@
# Geode Android Launcher
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 10
versionName "1.2.0"
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.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Divider
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
@@ -51,9 +51,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
@@ -165,12 +167,16 @@ fun SelectLogLevelDialog(
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
val context = LocalContext.current
val haptics = LocalHapticFeedback.current
Column(
modifier = modifier
.padding(8.dp)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
copyLogText(context, logLine.asSimpleString)
}
)
@@ -248,7 +254,7 @@ fun ApplicationLogsScreen(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_icon_alt)
)
}
@@ -396,7 +402,7 @@ fun ApplicationLogsScreen(
logLines
) { line ->
LogCard(line)
Divider()
HorizontalDivider()
}
}
}
@@ -1,6 +1,5 @@
package com.geode.launcher
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Intent
@@ -8,7 +7,6 @@ import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
@@ -37,7 +35,7 @@ import java.io.FileInputStream
import java.io.FileOutputStream
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener {
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener {
private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null
private val sTag = GeometryDashActivity::class.simpleName
private var mIsRunning = false
@@ -109,7 +107,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
setupRedirection(gdPackageInfo)
Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this)
GeodeUtils.setCapabilityListener(this)
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
@@ -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.enableEdgeToEdge
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -27,8 +29,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
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.PreferenceUtils
import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.useCountdownTimer
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import java.net.ConnectException
import java.net.UnknownHostException
@@ -122,7 +130,7 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String, onDis
Spacer(Modifier.size(4.dp))
TextButton(onClick = { uriHandler.openUri(openTo) },) {
TextButton(onClick = { uriHandler.openUri(openTo) }) {
Text(stringResource(R.string.launcher_download))
}
}
@@ -137,7 +145,7 @@ fun UpdateProgressIndicator(
message: String,
releaseViewModel: ReleaseViewModel,
modifier: Modifier = Modifier,
progress: Float? = null
progress: (() -> Float)? = null
) {
Column(
verticalArrangement = Arrangement.Center,
@@ -151,7 +159,7 @@ fun UpdateProgressIndicator(
if (progress == null) {
LinearProgressIndicator()
} else {
LinearProgressIndicator(progress)
LinearProgressIndicator(progress = progress)
}
TextButton(
@@ -226,67 +234,91 @@ fun LoadFailedDialog(returnTitle: String, returnMessage: String) {
}
@Composable
fun UpdateWarning() {
fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
val context = LocalContext.current
val packageManager = context.packageManager
val gdVersionCode = remember { LaunchUtils.getGeometryDashVersionCode(packageManager) }
val gdVersionString = remember { LaunchUtils.getGeometryDashVersionString(packageManager) }
var showUpdateWarning by remember { mutableStateOf(true) }
var lastDismissedVersion by PreferenceUtils.useLongPreference(
preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE
)
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && gdVersionCode != lastDismissedVersion) {
if (showUpdateWarning) {
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
}
val canDismissRelease = gdVersionCode >= Constants.SUPPORTED_VERSION_CODE
val shouldDismiss = canDismissRelease && gdVersionCode == lastDismissedVersion
Text(stringResource(
message, gdVersionString, Constants.SUPPORTED_VERSION_STRING
))
},
confirmButton = {
TextButton(onClick = {
showUpdateWarning = false
lastDismissedVersion = gdVersionCode
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && !shouldDismiss) {
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
}
onLaunch(context)
}) {
Text(stringResource(R.string.message_box_accept))
}
},
dismissButton = {
TextButton(onClick = { showUpdateWarning = false }) {
Text(stringResource(R.string.message_box_cancel))
}
},
onDismissRequest = { showUpdateWarning = false }
)
}
Text(stringResource(
message, gdVersionString, Constants.SUPPORTED_VERSION_STRING
))
},
confirmButton = {
TextButton(onClick = {
onDismiss()
lastDismissedVersion = gdVersionCode
onLaunch(context, inSafeMode)
}) {
Text(stringResource(R.string.message_box_accept))
}
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(R.string.message_box_cancel))
}
},
onDismissRequest = { onDismiss() }
)
} else {
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
fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) {
val context = LocalContext.current
@@ -317,8 +349,6 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
)
}
is ReleaseViewModel.ReleaseUIState.InDownload -> {
val progress = state.downloaded / state.outOf.toDouble()
val downloaded = remember(state.downloaded) {
formatShortFileSize(context, state.downloaded)
}
@@ -335,7 +365,10 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
),
modifier = modifier,
releaseViewModel = releaseViewModel,
progress = progress.toFloat()
progress = {
val progress = state.downloaded / state.outOf.toDouble()
progress.toFloat()
}
)
}
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)
launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
@@ -410,7 +449,7 @@ fun MainScreen(
) {
val context = LocalContext.current
val shouldAutomaticallyLaunch = PreferenceUtils.useBooleanPreference(
val shouldAutomaticallyLaunch by PreferenceUtils.useBooleanPreference(
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
)
@@ -423,6 +462,8 @@ fun MainScreen(
val geodeInstalled = geodePreinstalled || geodeJustInstalled
var beginLaunch by remember { mutableStateOf(false) }
var showSafeModeDialog by remember { mutableStateOf(false) }
var launchInSafeMode by remember { mutableStateOf(false) }
LaunchedEffect(shouldUpdate) {
if (shouldUpdate && !releaseViewModel.hasPerformedCheck) {
@@ -451,15 +492,11 @@ fun MainScreen(
)
if (gdInstalled && geodeInstalled) {
if (shouldAutomaticallyLaunch.value && !releaseViewModel.isInUpdate && !disableAutomaticLaunch) {
val stopLaunch = releaseViewModel.isInUpdate || disableAutomaticLaunch || showSafeModeDialog
if (shouldAutomaticallyLaunch && !stopLaunch) {
val countdownTimer = useCountdownTimer(
time = 3000,
onCountdownFinish = {
// just in case this changed async
if (shouldAutomaticallyLaunch.value) {
beginLaunch = true
}
}
onCountdownFinish = { beginLaunch = true }
)
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 {
Button(
onClick = { beginLaunch = true },
enabled = !releaseViewModel.isInUpdate
onClick = { },
enabled = !releaseViewModel.isInUpdate,
interactionSource = interactionSource
) {
Icon(
Icons.Filled.PlayArrow,
@@ -545,7 +614,24 @@ fun MainScreen(
}
if (beginLaunch) {
UpdateWarning()
UpdateWarning(launchInSafeMode) {
beginLaunch = false
launchInSafeMode = false
}
}
if (showSafeModeDialog) {
SafeModeDialog(
onDismiss = {
showSafeModeDialog = false
},
onLaunch = {
launchInSafeMode = true
beginLaunch = true
showSafeModeDialog = false
}
)
}
}
@@ -23,7 +23,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -145,7 +145,9 @@ fun UpdateIndicator(
enablePopup = true
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
CircularProgressIndicator(progress.toFloat())
CircularProgressIndicator(
progress = { progress.toFloat() },
)
}
else -> {}
}
@@ -259,7 +261,7 @@ fun SettingsScreen(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = context.getString(R.string.back_icon_alt)
)
}
@@ -351,10 +353,6 @@ fun SettingsScreen(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(){}<>[]?:;'\"~`-_+=\\| ".contains(c)
}}
)
SettingsCard(
title = stringResource(R.string.preference_ignore_load_failure_name),
preferenceKey = PreferenceUtils.Key.IGNORE_LOAD_FAILURE,
)
OptionsButton(
title = context.getString(R.string.preferences_copy_external_button),
description = LaunchUtils.getBaseDirectory(context).path,
@@ -11,8 +11,12 @@ 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
@@ -173,6 +177,16 @@ fun StringDialog(
keyboardType = KeyboardType.Ascii,
autoCorrect = false
),
trailingIcon = {
if (enteredValue.isNotEmpty()) {
IconButton(onClick = { enteredValue = "" }) {
Icon(
Icons.Filled.Clear,
contentDescription = stringResource(R.string.preference_text_clear)
)
}
}
}
)
},
confirmButton = {
@@ -3,11 +3,13 @@ package com.geode.launcher
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.Build
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import com.geode.launcher.utils.LaunchUtils
import java.io.File
@@ -150,7 +152,7 @@ class UserDirectoryProvider : DocumentsProvider() {
notifyFileChange(file)
}
override fun renameDocument(documentId: String, displayName: String): String? {
override fun renameDocument(documentId: String, displayName: String): String {
val file = getFileForDocumentId(documentId)
val dest = findFileNameForNewFile(File(file.parentFile, displayName))
file.renameTo(dest)
@@ -203,4 +205,19 @@ class UserDirectoryProvider : DocumentsProvider() {
val uri = getDocumentUri(parentDocumentId)
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)
}
}
@@ -9,7 +9,9 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
@@ -17,7 +19,9 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import com.geode.launcher.BuildConfig
import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
@@ -34,6 +38,7 @@ object GeodeUtils {
private lateinit var saveFileResultLauncher: ActivityResultLauncher<GeodeSaveFileActivityResult.SaveFileParams>
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private lateinit var internalRequestPermissionsLauncher: ActivityResultLauncher<Array<String>>
private lateinit var internalRequestAllFilesLauncher: ActivityResultLauncher<Intent>
private var afterRequestPermissions: (() -> Unit)? = null
private var afterRequestPermissionsFailure: (() -> Unit)? = null
@@ -93,12 +98,26 @@ object GeodeUtils {
if (isGranted) {
afterRequestPermissions?.invoke()
} else {
Toast.makeText(activity, activity.getString(R.string.missing_permissions), Toast.LENGTH_SHORT)
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
@@ -149,21 +168,48 @@ object GeodeUtils {
// TODO As of now this is unused
@JvmStatic
fun openFolder(path: String): Boolean {
activity.get()?.run {
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("*/*")
val context = activity.get()!!
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
return true
val pathFile = File(path)
val baseDirectory = LaunchUtils.getBaseDirectory(context)
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
}
}
private external fun selectFileCallback(path: String)
@@ -173,30 +219,38 @@ object GeodeUtils {
private external fun failedCallback()
private fun checkForFilePermissions(onSuccess: () -> Unit, onFailure: () -> Unit) {
val permissions = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) {
listOf(
Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO
)
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 {
listOf(
val permissions = listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
val context = activity.get()!!
val context = activity.get()!!
val needsPermissions = permissions.filter {
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
}
val needsPermissions = permissions.filter {
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
}
if (needsPermissions.isNotEmpty()) {
internalRequestPermissionsLauncher.launch(needsPermissions.toTypedArray())
afterRequestPermissions = onSuccess
} else {
onSuccess()
if (needsPermissions.isNotEmpty()) {
internalRequestPermissionsLauncher.launch(needsPermissions.toTypedArray())
afterRequestPermissions = onSuccess
afterRequestPermissionsFailure = onFailure
} else {
onSuccess()
}
}
}
@@ -317,32 +371,53 @@ object GeodeUtils {
return "com.geode.launcher.user" == uri.authority
}
private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal"
private const val MANAGE_ALL_FILES = "${INTERNAL_PERMISSION_PREFIX}.MANAGE_ALL_FILES"
@JvmStatic
fun getPermissionStatus(permission: String): Boolean {
return ContextCompat.checkSelfPermission(
activity.get()?.applicationContext ?: return false,
permission
) == PackageManager.PERMISSION_GRANTED
val context = activity.get() ?: return false
return when (permission) {
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
fun requestPermission(permission: String) {
if (permission == MANAGE_ALL_FILES) {
// this function handles already having perms for us
checkForFilePermissions(onSuccess = {
permissionCallback(true)
}, onFailure = {
permissionCallback(false)
})
return
}
if (getPermissionStatus(permission)) {
permissionCallback(true)
return
}
activity.get()?.run {
try {
requestPermissionLauncher.launch(permission)
} catch (e: ActivityNotFoundException) {
permissionCallback(false)
}
return
try {
requestPermissionLauncher.launch(permission)
} catch (e: ActivityNotFoundException) {
permissionCallback(false)
}
permissionCallback(false)
}
private external fun permissionCallback(granted: Boolean)
@@ -371,4 +446,30 @@ object GeodeUtils {
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)
}
@@ -10,6 +10,7 @@ import android.view.KeyEvent
import android.view.MotionEvent
import android.view.inputmethod.InputMethodManager
import com.customRobTop.BaseRobTopActivity
import com.geode.launcher.utils.GeodeUtils
private const val HANDLER_OPEN_IME_KEYBOARD = 2
private const val HANDLER_CLOSE_IME_KEYBOARD = 3
@@ -37,6 +38,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
}
private lateinit var cocos2dxRenderer: Cocos2dxRenderer
var useKeyboardEvents = false
set(value) {
field = value
cocos2dxRenderer.sendResizeEvents = value
}
var cocos2dxEditText: Cocos2dxEditText? = null
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) {
KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_MENU -> {
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) {
this.cocos2dxRenderer = renderer
setRenderer(this.cocos2dxRenderer)
@@ -59,6 +59,7 @@ class Cocos2dxRenderer : GLSurfaceView.Renderer {
private var lastTickInNanoSeconds: Long = 0
private var screenWidth = 0
private var screenHeight = 0
var sendResizeEvents = false
fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) {
screenWidth = surfaceWidth
+4
View File
@@ -30,6 +30,8 @@
</string>
<string name="launcher_update_available">A new launcher update is available!</string>
<string name="launcher_update_dismiss">Dismiss</string>
<string name="safe_mode_enable_title">Enable safe mode?</string>
<string name="safe_mode_enable_description">Safe mode disables loading of all mods, allowing one to remove problematic mods or diagnose crashes. If you\'re not experiencing issues with Geode, you should probably not be using safe mode.</string>
<!-- Updater strings -->
<string name="release_fetch_failed">Update failed.\n%1$s</string>
@@ -53,6 +55,7 @@
<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_accept">OK</string>
<string name="message_box_continue">Continue</string>
<string name="missing_permissions">Missing required permissions for this action!</string>
<!-- SettingsActivity strings -->
@@ -95,6 +98,7 @@
<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 -->
<string name="title_activity_application_logs">Application Logs</string>
-17
View File
@@ -1,17 +0,0 @@
buildscript {
ext {
compose_compiler = '1.5.8'
}
}
// 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('clean', Delete) {
delete rootProject.layout.buildDirectory
}
+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
# resources declared in the library itself and none from the library's dependencies,
# 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
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+7 -7
View File
@@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# 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 ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# 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" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
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.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
+4 -1
View File
@@ -5,6 +5,8 @@ pluginManagement {
mavenCentral()
}
}
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
@@ -12,5 +14,6 @@ dependencyResolutionManagement {
mavenCentral()
}
}
rootProject.name = "launcher"
include ':app'
include(":app")