Compare commits

..
76 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
mat c9a712f692 im stupid 2024-01-27 13:21:46 -03:00
mat 6a0b3fc8bd we're stupid 2024-01-27 13:17:21 -03:00
alk 684fd99b83 add release as a build i think 2024-01-27 18:34:18 +03:00
qimiko fd83daa776 generate more specific warnings 2024-01-27 03:13:37 -07:00
qimiko 9a87716635 add fallback mode to log export 2024-01-27 02:30:36 -07:00
Chloe 31566ce64b Merge pull request #12 from dankmeme01/main
Add support for requesting permissions from native code
2024-01-26 15:24:48 -07:00
dankmeme01 d078879e7b change handling of failures 2024-01-26 23:04:33 +01:00
dankmeme01 5fa42b6ca5 remove unused imports 2024-01-26 22:35:28 +01:00
dankmeme01 e164b87436 make permission requesting use launchers and add callback 2024-01-26 22:35:08 +01:00
dankmeme01 433d6a0f46 add support for requesting permissions from mods or Geode 2024-01-26 21:30:30 +01:00
mat cf8b1edbe2 Merge pull request #11 from Fleeym/crashfix
Hopefully fix crashes when tabbing out and back in
2024-01-25 15:39:55 -03:00
Fleeym 500e00a9cc ask for more file permission stuff 2024-01-25 19:58:18 +02:00
Fleeym c3f80e9c74 fix opengl context being deleted after it no longer exists 2024-01-25 19:58:09 +02:00
Chloe 7eb06e175f Merge pull request #10 from Fleeym/flame-stuff
fix file utils, move them to a separate class
2024-01-24 17:20:52 -07:00
Chloe 141681be05 add license 2024-01-23 20:24:37 -07:00
Fleeym dcfab5e001 fix file utils, move them to a separate class 2024-01-24 04:45:09 +02:00
qimiko 498bbf4528 accidentally broke a dependency 2024-01-23 05:14:36 -07:00
qimiko dc72608622 properly set value in set 2024-01-23 05:11:56 -07:00
qimiko a50e0a0830 remove extra println 2024-01-23 05:05:06 -07:00
qimiko db707dfce1 only show unsupported version once 2024-01-23 05:03:37 -07:00
qimiko 929cc7615e api to set preference through compose 2024-01-23 05:03:18 -07:00
qimiko bb2e3fbb1b version strings are meaningless 2024-01-23 04:48:55 -07:00
qimiko 7d548aef6c remove unused import 2024-01-23 03:18:54 -07:00
qimiko 7f64236e89 add launcher update check 2024-01-23 03:15:39 -07:00
qimiko 199b6f142b use nightly by default in debug mode 2024-01-23 03:12:44 -07:00
qimiko af87320f11 clarify unsupported version warning 2024-01-23 02:18:48 -07:00
qimiko 665756d85b automatically trim log messages 2024-01-23 01:33:54 -07:00
qimiko 590006413c filter by crash buffer option 2024-01-23 01:31:38 -07:00
qimiko 1b079fa3ff add ability to export logs to file 2024-01-23 01:01:14 -07:00
qimiko cb16db3bf9 use specific message for abi differences 2024-01-23 00:19:55 -07:00
qimiko de6fad41f6 fix nightly download url 2024-01-22 11:38:11 -07:00
qimiko d1a43230a9 gd may have updated 2024-01-22 08:16:42 -07:00
qimiko 442ef83339 show launcher version in settings 2024-01-22 00:28:29 -07:00
qimiko 1eab544692 reorganize release api 2024-01-22 00:16:01 -07:00
qimiko af0ebeab84 save game during onstop 2024-01-21 16:15:22 -07:00
qimiko 0844dc97d2 use kotlin stream copyto for file copies 2024-01-21 16:15:11 -07:00
qimiko 42c10c2729 navigation bar visibility changes 2024-01-21 03:40:26 -07:00
qimiko cf568bcbe2 use release hash instead of modified time 2024-01-20 20:28:08 -07:00
qimiko 36152d8773 add icon to retry button 2024-01-20 20:26:00 -07:00
qimiko 3f371be097 pretend log messages are utf8 2024-01-20 20:17:39 -07:00
qimiko 5eba097e38 only request permissions when needed 2024-01-20 18:37:59 -07:00
qimiko 7e868d8d0a log optimizations 2024-01-20 18:09:53 -07:00
41 changed files with 2230 additions and 974 deletions

No files matched your search

+16 -7
View File
@@ -7,6 +7,15 @@ on:
jobs:
build:
strategy:
matrix:
config:
- name: Release
lower: release
- name: Debug
lower: debug
runs-on: ubuntu-latest
steps:
@@ -27,7 +36,7 @@ jobs:
- name: Build project
run: |
./gradlew app:assembleDebug \
./gradlew app:assemble${{ matrix.config.name }} \
-Pandroid.injected.signing.store.file="$HOME/release.keystore" \
-Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \
-Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \
@@ -37,8 +46,8 @@ jobs:
id: rename_apk_universal
run: |
SHORT_SHA=${GITHUB_SHA::7}
APK_PATH=${{github.workspace}}/app/build/outputs/apk/debug/app-universal-debug.apk
OUT_PATH=${{github.workspace}}/geode-launcher-debug-$SHORT_SHA.apk
APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-universal-${{ matrix.config.lower }}.apk
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.apk
mv $APK_PATH $OUT_PATH
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
@@ -46,15 +55,15 @@ jobs:
id: rename_apk
run: |
SHORT_SHA=${GITHUB_SHA::7}
APK_PATH=${{github.workspace}}/app/build/outputs/apk/debug/app-armeabi-v7a-debug.apk
OUT_PATH=${{github.workspace}}/geode-launcher-debug-android32-$SHORT_SHA.apk
APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-armeabi-v7a-${{ matrix.config.lower }}.apk
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-android32-$SHORT_SHA.apk
mv $APK_PATH $OUT_PATH
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
- name: Upload development build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: geode-launcher-debug
name: geode-launcher-${{ matrix.config.lower }}
path: |
${{ steps.rename_apk_universal.outputs.path }}
${{ steps.rename_apk.outputs.path }}
+23
View File
@@ -0,0 +1,23 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
-4
View File
@@ -1,7 +1,3 @@
# Geode Android Launcher
Launches a vanilla copy of Geometry Dash with the Geode loader added.
## TODO:
* Figure out better way of packaging libgeode
-97
View File
@@ -1,97 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
}
android {
compileSdk 34
defaultConfig {
applicationId "com.geode.launcher"
minSdk 23
targetSdk 34
versionCode 6
versionName "1.0.0-beta.3"
vectorDrawables {
useSupportLibrary true
}
externalNativeBuild {
cmake {
arguments "-DUSE_TULIPHOOK:BOOL=OFF"
}
}
ndkConfig.abiFilters "arm64-v8a", "armeabi-v7a"
}
splits {
abi {
enable true
reset()
//noinspection ChromeOsAbiSupport (not my fault)
include "arm64-v8a", "armeabi-v7a"
universalApk true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
// enables a polyfill for java Instant on api levels < 26 (used for updater)
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_compiler
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
}
}
namespace 'com.geode.launcher'
ndkVersion '26.1.10909125'
}
dependencies {
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.compose.ui:ui"
implementation 'androidx.compose.material3:material3'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.activity:activity-ktx:1.8.2'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation "androidx.documentfile:documentfile:1.0.1"
implementation "com.squareup.okio:okio:3.7.0"
implementation "com.squareup.okhttp3:okhttp:4.12.0"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.0"
debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-test-manifest"
debugImplementation "androidx.compose.ui:ui-tooling-preview"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
}
+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")
}
+9 -2
View File
@@ -9,8 +9,15 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature
android:name="android.hardware.wifi"
@@ -9,7 +9,10 @@ import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -20,10 +23,14 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.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
@@ -38,12 +45,17 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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
@@ -64,6 +76,8 @@ import kotlinx.coroutines.launch
class ApplicationLogsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
@@ -92,16 +106,20 @@ fun getLogColor(priority: LogPriority): Color {
// i just copied these from my ide tbh
return if (isDark) {
when (priority) {
LogPriority.ERROR -> Color(0xffdf5151)
LogPriority.WARN -> Color(0xffbcb500)
LogPriority.FATAL -> Color(0xffb8b6fc)
LogPriority.ERROR -> Color(0xff_df_51_51)
LogPriority.WARN -> Color(0xff_bc_b5_00)
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 {
when (priority) {
LogPriority.ERROR -> Color(0xffdf5151)
LogPriority.WARN -> Color(0xffeab700)
LogPriority.FATAL -> Color(0xffa142f4)
LogPriority.ERROR -> Color(0xff_df_51_51)
LogPriority.WARN -> Color(0xff_ea_b7_00)
LogPriority.FATAL -> Color(0xff_a1_42_f4)
LogPriority.DEBUG -> Color(0xff_77_77_77)
LogPriority.VERBOSE -> Color(0xff_aa_aa_aa)
else -> Color.Unspecified
}
}
@@ -126,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
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)
}
)
@@ -149,7 +190,7 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
)
Text(
"[${logLine.tag}]: ${logLine.message}",
"[${logLine.tag}]: ${logLine.messageTrimmed}",
fontFamily = FontFamily.Monospace,
style = Typography.bodyMedium
)
@@ -171,10 +212,40 @@ fun ApplicationLogsScreen(
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState()
var showMoreOptions by remember { mutableStateOf(false) }
val saveLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
if (uri != null) {
coroutineScope.launch {
val logs = logViewModel.getLogData()
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.bufferedWriter().use { writer ->
writer.write(logs)
}
}
Toast.makeText(context, R.string.application_logs_export_completed, Toast.LENGTH_SHORT)
.show()
}
}
}
LaunchedEffect(logLines.size) {
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(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
@@ -183,40 +254,111 @@ fun ApplicationLogsScreen(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
actions = {
IconButton(onClick = {
if (!logLines.isEmpty()) {
coroutineScope.launch {
val data = logViewModel.getLogData()
coroutineScope.launch {
val data = logViewModel.getLogData()
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, data)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, data)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}
}) {
}, enabled = !logLines.isEmpty()) {
Icon(
Icons.Filled.Share,
contentDescription = stringResource(R.string.application_logs_share)
)
}
IconButton(onClick = {
logViewModel.clearLogs()
onBackPressedDispatcher?.onBackPressed()
}) {
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = stringResource(R.string.application_logs_delete)
Icons.Filled.MoreVert,
contentDescription = stringResource(R.string.application_logs_more)
)
}
DropdownMenu(
expanded = showMoreOptions,
onDismissRequest = { showMoreOptions = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_bug_report),
contentDescription = null,
)
},
trailingIcon = {
if (logViewModel.filterCrashes) {
Icon(
Icons.Filled.Check,
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
)
}
},
text = {
Text(stringResource(R.string.application_logs_crash_only))
}, onClick = {
logViewModel.toggleCrashBuffer()
showMoreOptions = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_filter_list),
contentDescription = null,
)
},
text = {
Text(stringResource(R.string.application_logs_level))
},
onClick = {
showLogLevelSelect = true
showMoreOptions = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_save),
contentDescription = null,
)
},
text = {
Text(stringResource(R.string.application_logs_export))
},
onClick = {
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
showMoreOptions = false
},
enabled = !logLines.isEmpty()
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_logs_delete))
}, onClick = {
logViewModel.clearLogs()
showMoreOptions = false
}
)
}
},
@@ -260,7 +402,7 @@ fun ApplicationLogsScreen(
logLines
) { line ->
LogCard(line)
Divider()
HorizontalDivider()
}
}
}
@@ -22,8 +22,8 @@ import com.customRobTop.BaseRobTopActivity
import com.customRobTop.JniToCpp
import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.DownloadUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import org.cocos2dx.lib.Cocos2dxEditText
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
@@ -35,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
@@ -44,10 +44,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private var mReceiver: BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupUIState()
super.onCreate(savedInstanceState)
// return back to main if Geometry Dash isn't found
if (!LaunchUtils.isGeometryDashInstalled(packageManager)) {
returnToMain()
@@ -57,20 +57,27 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
try {
tryLoadGame()
} catch (e: UnsatisfiedLinkError) {
e.printStackTrace()
Log.e("GeodeLauncher", "Library linkage failure", e)
// generates helpful information for use in debugging library load failures
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
val failureReason = LaunchUtils.diagnoseLoadErrors(this, gdPackageInfo)
val abiMismatch = LaunchUtils.detectAbiMismatch(this, gdPackageInfo, e)
val is64bit = LaunchUtils.is64bit
val errorMessage = when {
abiMismatch && is64bit -> getString(R.string.load_failed_abi_error_need_32bit_description)
abiMismatch -> getString(R.string.load_failed_abi_error_need_64bit_description)
else -> getString(R.string.load_failed_link_error_description)
}
returnToMain(
getString(R.string.load_failed_link_error),
getString(R.string.load_failed_link_error_description, failureReason.name)
errorMessage
)
return
} catch (e: Exception) {
e.printStackTrace()
Log.e("GeodeLauncher", "Uncaught error during game load", e)
returnToMain(
getString(R.string.load_failed_generic_error),
@@ -99,17 +106,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
setupRedirection(gdPackageInfo)
// request read and write permissions
requestPermissions(
arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
),
0
)
Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this)
GeodeUtils.setCapabilityListener(this)
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
@@ -122,8 +122,25 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
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!")
} else {
throw e
}
}
@@ -131,7 +148,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
try {
LaunchUtils.addAssetsFromPackage(assets, packageInfo)
} catch (e: NoSuchMethodException) {
e.printStackTrace()
Log.e("GeodeLauncher", "Failed to add asset redirection", e)
}
try {
@@ -143,7 +160,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR)
LauncherFix.setDataPath(saveDir.path)
} catch (e: UnsatisfiedLinkError) {
e.printStackTrace()
Log.e("GeodeLauncher", "Failed to load LauncherFix", e)
}
}
@@ -195,26 +212,26 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadGeodeLibrary(): Boolean {
private fun loadGeodeLibrary() {
// Load Geode if exists
// bundling the object with the application allows for nicer backtraces
try {
// put libgeode.so in jniLibs/armeabi-v7a to get this
System.loadLibrary("geode")
return true
return
} catch (e: UnsatisfiedLinkError) {
// but users may prefer it stored with data
val geodeFilename = LaunchUtils.geodeFilename
val geodePath = File(filesDir.path, "launcher/$geodeFilename")
if (geodePath.exists()) {
System.load(geodePath.path)
return true
return
}
// you know zmx i have 0 clue what this does so im
// just gonna like copy the binary from external
// 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")
if (copiedPath.exists()) {
@@ -231,17 +248,14 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
)
if (copiedGeodePath.exists()) {
try {
println("Loading Geode from ${externalGeodePath.name}")
System.load(copiedGeodePath.path)
} catch (e: UnsatisfiedLinkError) {
e.printStackTrace()
}
println("Loading Geode from ${externalGeodePath.name}")
System.load(copiedGeodePath.path)
return
}
}
}
return false
throw e
}
}
private fun createView(): FrameLayout {
@@ -281,13 +295,24 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private fun hideSystemUi() {
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowCompat.getInsetsController(window, window.decorView).apply {
hide(WindowInsetsCompat.Type.systemBars())
systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
hide(WindowInsetsCompat.Type.systemBars())
}
}
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
super.onWindowFocusChanged(hasWindowFocus)
mHasWindowFocus = hasWindowFocus
if (hasWindowFocus && !mIsOnPause) {
resumeGame()
}
hideSystemUi()
}
override fun onDestroy() {
super.onDestroy()
unregisterReceivers()
@@ -315,6 +340,15 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
}
}
override fun onRestart() {
super.onRestart()
mIsOnPause = false
BaseRobTopActivity.isPaused = false
if (!this.mIsRunning) {
resumeGame()
}
}
override fun onPause() {
super.onPause()
mIsOnPause = true
@@ -343,14 +377,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
}
}
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
super.onWindowFocusChanged(hasWindowFocus)
mHasWindowFocus = hasWindowFocus
if (hasWindowFocus && !mIsOnPause) {
resumeGame()
}
}
override fun runOnGLThread(runnable: Runnable) {
mGLSurfaceView?.queueEvent(runnable)
}
@@ -424,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
}
}
@@ -6,12 +6,16 @@ import android.os.Bundle
import android.text.format.Formatter.formatShortFileSize
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
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
@@ -25,7 +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
@@ -34,7 +42,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.api.ReleaseViewModel
import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme
@@ -42,14 +50,19 @@ import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import com.geode.launcher.utils.ReleaseManager
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
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
val gdInstalled = LaunchUtils.isGeometryDashInstalled(packageManager)
@@ -90,12 +103,49 @@ class MainActivity : ComponentActivity() {
}
}
@Composable
fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String, onDismiss: () -> Unit) {
val uriHandler = LocalUriHandler.current
ElevatedCard(modifier) {
Column(
// buttons add enough padding already, lower to compensate
modifier = Modifier.padding(
top = 20.dp,
start = 12.dp,
end = 12.dp,
bottom = 8.dp
),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
stringResource(R.string.launcher_update_available),
modifier = Modifier.padding(horizontal = 10.dp)
)
Row(modifier = Modifier.align(Alignment.End)) {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.launcher_update_dismiss))
}
Spacer(Modifier.size(4.dp))
TextButton(onClick = { uriHandler.openUri(openTo) }) {
Text(stringResource(R.string.launcher_download))
}
}
}
}
}
@Composable
fun UpdateProgressIndicator(
message: String,
releaseViewModel: ReleaseViewModel,
modifier: Modifier = Modifier,
progress: Float? = null
progress: (() -> Float)? = null
) {
Column(
verticalArrangement = Arrangement.Center,
@@ -109,7 +159,7 @@ fun UpdateProgressIndicator(
if (progress == null) {
LinearProgressIndicator()
} else {
LinearProgressIndicator(progress)
LinearProgressIndicator(progress = progress)
}
TextButton(
@@ -147,6 +197,11 @@ fun UpdateMessageIndicator(
releaseViewModel.runReleaseCheck(true)
},
) {
Icon(
Icons.Filled.Refresh,
contentDescription = stringResource(R.string.launcher_retry_icon_alt)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.release_fetch_button_retry))
}
}
@@ -179,56 +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) {
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 = {
Text(stringResource(
R.string.launcher_unsupported_version_description,
gdVersionString, Constants.SUPPORTED_VERSION_STRING
))
},
confirmButton = {
TextButton(onClick = {
showUpdateWarning = false
onLaunch(context)
}) {
Text(stringResource(R.string.message_box_accept))
}
},
dismissButton = {
TextButton(onClick = { showUpdateWarning = false }) {
Text(stringResource(R.string.message_box_cancel))
}
},
onDismissRequest = { showUpdateWarning = false }
)
}
val canDismissRelease = gdVersionCode >= Constants.SUPPORTED_VERSION_CODE
val shouldDismiss = canDismissRelease && gdVersionCode == lastDismissedVersion
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
}
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
@@ -259,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)
}
@@ -277,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 -> {
@@ -313,9 +404,31 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
}
}
}
val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState()
val nextUpdateValue = nextUpdate
if (!releaseViewModel.isInUpdate && nextUpdateValue != null) {
val updateUrl = nextUpdateValue.getLauncherDownload()?.browserDownloadUrl
?: nextUpdateValue.htmlUrl
LauncherUpdateIndicator(
modifier = modifier,
openTo = updateUrl,
onDismiss = {
releaseViewModel.dismissLauncherUpdate()
}
)
}
}
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
@@ -336,7 +449,7 @@ fun MainScreen(
) {
val context = LocalContext.current
val shouldAutomaticallyLaunch = PreferenceUtils.useBooleanPreference(
val shouldAutomaticallyLaunch by PreferenceUtils.useBooleanPreference(
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
)
@@ -349,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) {
@@ -377,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) {
@@ -402,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,
@@ -465,12 +608,30 @@ fun MainScreen(
UpdateCard(
releaseViewModel,
modifier = Modifier.padding(12.dp)
modifier = Modifier
.padding(12.dp)
)
}
if (beginLaunch) {
UpdateWarning()
UpdateWarning(launchInSafeMode) {
beginLaunch = false
launchInSafeMode = false
}
}
if (showSafeModeDialog) {
SafeModeDialog(
onDismiss = {
showSafeModeDialog = false
},
onLaunch = {
launchInSafeMode = true
beginLaunch = true
showSafeModeDialog = false
}
)
}
}
@@ -13,6 +13,7 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
@@ -20,22 +21,18 @@ import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.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
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@@ -44,9 +41,8 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.api.ReleaseViewModel
import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme
@@ -58,7 +54,9 @@ import java.net.UnknownHostException
class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption)
@@ -81,16 +79,6 @@ class SettingsActivity : ComponentActivity() {
}
}
/*
fun onExportSaveData(context: Context) {
Toast.makeText(
context,
"This function is not implemented yet!",
Toast.LENGTH_SHORT
).show()
}
*/
fun onOpenFolder(context: Context) {
val file = LaunchUtils.getBaseDirectory(context)
val clipboardManager =
@@ -157,7 +145,9 @@ fun UpdateIndicator(
enablePopup = true
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
CircularProgressIndicator(progress.toFloat())
CircularProgressIndicator(
progress = { progress.toFloat() },
)
}
else -> {}
}
@@ -271,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)
)
}
@@ -312,19 +302,10 @@ fun SettingsScreen(
description = context.getString(R.string.preference_load_automatically_description),
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
)
OptionsButton(
title = context.getString(R.string.preferences_copy_external_button),
description = LaunchUtils.getBaseDirectory(context).path,
onClick = { onOpenFolder(context) }
)
OptionsButton(
title = stringResource(R.string.preferences_open_file_manager),
onClick = { onOpenFileManager(context) }
)
OptionsButton(
title = stringResource(R.string.preferences_view_logs),
onClick = { onOpenLogs(context) }
)
}
OptionsGroup(context.getString(R.string.preference_category_updater)) {
@@ -361,253 +342,38 @@ fun SettingsScreen(
UpdateIndicator(snackbarHostState, updateStatus)
}
}
/*
OptionsGroup("Data") {
OptionsGroup(stringResource(R.string.preference_category_developer)) {
SettingsStringCard(
title = stringResource(R.string.preference_launch_arguments_name),
dialogTitle = stringResource(R.string.preference_launch_arguments_set_title),
preferenceKey = PreferenceUtils.Key.LAUNCH_ARGUMENTS,
filterInput = { it.filter { c ->
// if only there was a better way to define this!
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(){}<>[]?:;'\"~`-_+=\\| ".contains(c)
}}
)
OptionsButton(
title = "Export save data",
onClick = { onExportSaveData(context) }
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(
stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME),
style = Typography.labelSmall,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
)
}
}
)
}
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)
@Composable
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.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)
}
}
@@ -0,0 +1,36 @@
package com.geode.launcher.activityresult
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.activity.result.contract.ActivityResultContract
open class GeodeOpenFileActivityResult : ActivityResultContract<GeodeOpenFileActivityResult.OpenFileParams, Uri?>() {
class OpenFileParams(val extraMimes: Array<String>, val defaultPath: Uri?) {}
override fun createIntent(context: Context, input: OpenFileParams): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_MIME_TYPES, input.extraMimes)
.setType("*/*")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (input.defaultPath != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
}
}
return intent
}
final override fun getSynchronousResult(
context: Context,
input: OpenFileParams
): SynchronousResult<Uri?>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
@@ -0,0 +1,55 @@
package com.geode.launcher.activityresult
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.activity.result.contract.ActivityResultContract
open class GeodeOpenFilesActivityResult : ActivityResultContract<GeodeOpenFilesActivityResult.OpenFileParams, List<@JvmSuppressWildcards Uri>>() {
class OpenFileParams(val extraMimes: Array<String>, val defaultPath: Uri?) {}
override fun createIntent(context: Context, input: OpenFileParams): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra(Intent.EXTRA_MIME_TYPES, input.extraMimes)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
.setType("*/*")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (input.defaultPath != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
}
}
return intent
}
final override fun getSynchronousResult(
context: Context,
input: OpenFileParams
): SynchronousResult<List<@JvmSuppressWildcards Uri>>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> {
// Gracefully stolen from ActivityResultContracts.Intent.getclipDataUris
val clipData = intent.takeIf {
resultCode == Activity.RESULT_OK
}?.clipData
val resultSet = LinkedHashSet<Uri>()
intent?.data?.let { data ->
resultSet.add(data)
}
if (clipData == null && resultSet.isEmpty()) {
return emptyList()
} else if (clipData != null) {
for (i in 0 until clipData.itemCount) {
val uri = clipData.getItemAt(i).uri
if (uri != null) {
resultSet.add(uri)
}
}
}
return ArrayList(resultSet)
}
}
@@ -0,0 +1,34 @@
package com.geode.launcher.activityresult
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.activity.result.contract.ActivityResultContract
open class GeodeSaveFileActivityResult : ActivityResultContract<GeodeSaveFileActivityResult.SaveFileParams, Uri?>() {
class SaveFileParams(val mimeType: String?, val defaultPath: Uri?) {}
override fun createIntent(context: Context, input: SaveFileParams): Intent {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setType(input.mimeType ?: "*/*")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (input.defaultPath != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
}
}
return intent
}
final override fun getSynchronousResult(
context: Context,
input: SaveFileParams
): SynchronousResult<Uri?>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
@@ -5,14 +5,14 @@ import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaInstant
import kotlinx.datetime.toLocalDateTime
import java.io.DataInputStream
import okio.BufferedSource
import java.io.IOException
fun DataInputStream.readCChar(): Char {
return this.readByte().toInt().toChar()
fun BufferedSource.readCChar(): Char {
return this.readUtf8CodePoint().toChar()
}
fun DataInputStream.readCString(): String {
fun BufferedSource.readCString(): String {
val buffer = StringBuilder()
var lastByte = this.readCChar()
@@ -24,40 +24,55 @@ fun DataInputStream.readCString(): String {
return buffer.toString()
}
fun DataInputStream.readLeUShort(): UShort {
val byteA = this.readByte().toInt() and 0xff
val byteB = this.readByte().toInt() and 0xff
val shortValue = (byteB shl 8) or byteA
return shortValue.toUShort()
}
fun DataInputStream.readLeInt(): Int {
val byteA = this.readByte().toInt() and 0xff
val byteB = this.readByte().toInt() and 0xff
val byteC = this.readByte().toInt() and 0xff
val byteD = this.readByte().toInt() and 0xff
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
}
fun DataInputStream.readLeUInt(): UInt {
val byteA = this.readByte().toUInt() and 0xffu
val byteB = this.readByte().toUInt() and 0xffu
val byteC = this.readByte().toUInt() and 0xffu
val byteD = this.readByte().toUInt() and 0xffu
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
}
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
enum class LogPriority {
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 {
fun fromByte(byte: Byte): LogPriority {
return when (byte.toInt()) {
fun fromByte(byte: Byte): LogPriority = fromInt(byte.toInt())
fun fromInt(int: Int) = when (int) {
0x1 -> DEFAULT
0x2 -> VERBOSE
0x3 -> DEBUG
@@ -67,10 +82,21 @@ enum class LogPriority {
0x7 -> FATAL
0x8 -> SILENT
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 {
return when (this) {
VERBOSE -> 'V'
@@ -107,7 +133,7 @@ data class LogLine(
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
}
fun fromInputStream(readStream: DataInputStream): LogLine {
fun fromBufferedSource(source: BufferedSource): LogLine {
/*
// from android <liblog/include/log/log_read.h>
// there are multiple logger entry formats
@@ -135,27 +161,27 @@ data class LogLine(
};
*/
/* val payloadLength = */ readStream.readLeUShort()
val headerSize = readStream.readLeUShort()
/* val payloadLength = */ source.readShortLe()
val headerSize = source.readShortLe().toUShort()
val entryVersion = headerSizeToVersion(headerSize)
val pid = readStream.readLeInt()
val tid = readStream.readLeUInt().toInt()
val sec = readStream.readLeUInt().toLong()
val nSec = readStream.readLeUInt().toInt()
val lid = readStream.readLeUInt().toInt()
val pid = source.readIntLe()
val tid = source.readIntLe()
val sec = source.readIntLe().toLong()
val nSec = source.readIntLe()
val lid = source.readIntLe()
val uid = if (entryVersion == EntryVersion.V4)
readStream.readLeUInt().toInt() else 0
source.readIntLe() else 0
val processInformation = ProcessInformation(pid, tid, uid)
val time = Instant.fromEpochSeconds(sec, nSec)
val priorityByte = readStream.readByte()
val priorityByte = source.readByte()
val priority = LogPriority.fromByte(priorityByte)
val tag = readStream.readCString()
val message = readStream.readCString()
val tag = source.readCString()
val message = source.readCString()
return LogLine(
process = processInformation,
@@ -179,6 +205,10 @@ data class LogLine(
val identifier = time.toJavaInstant()
val formattedTime = this.time.toLocalDateTime(TimeZone.currentSystemDefault())
val asSimpleString = "$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.message}"
val messageTrimmed by lazy { this.message.trim() }
val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
val asSimpleString by lazy {
"$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
}
}
@@ -4,14 +4,16 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import java.io.DataInputStream
import okio.buffer
import okio.source
import java.io.EOFException
import java.util.LinkedList
import java.io.IOException
/**
* ViewModel to manage log output from logcat.
@@ -25,25 +27,59 @@ class LogViewModel: ViewModel() {
private val _lineState = mutableStateListOf<LogLine>()
val lineState = _lineState
private var _isLoading = MutableStateFlow(false)
var isLoading = _isLoading.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
private var failedToLoad = false
private var logJob: Job? = null
var filterCrashes = false
private set
// skip debug/verbose lines by default
var logLevel = LogPriority.INFO
set(value) {
field = value
_lineState.clear()
loadLogs()
}
fun toggleCrashBuffer() {
filterCrashes = !filterCrashes
_lineState.clear()
loadLogs()
}
private suspend fun logOutput(): List<LogLine> {
val logLines = LinkedList<LogLine>()
val logLines = ArrayList<LogLine>()
val logStream = DataInputStream(
// -B = binary format, -d = dump logs
Runtime.getRuntime().exec("logcat -B -d").inputStream
)
// -B = binary format, -d = dump logs
val logCommand = if (filterCrashes) "logcat -b crash -B -d"
else "logcat -B -d"
val logProcess = try {
Runtime.getRuntime().exec(logCommand)
} catch (ioe: IOException) {
ioe.printStackTrace()
logLines += LogLine.showException(ioe)
failedToLoad = true
return logLines
}
val logSource = logProcess.inputStream.source().buffer()
try {
coroutineScope {
// this runs until the stream is exhausted
while (true) {
val line = LogLine.fromInputStream(logStream)
val line = LogLine.fromBufferedSource(logSource)
// skip debug/verbose lines
if (line.priority >= LogPriority.INFO) {
if (line.priority >= logLevel) {
logLines += line
}
@@ -55,12 +91,17 @@ class LogViewModel: ViewModel() {
} catch (e: Exception) {
e.printStackTrace()
logLines += LogLine.showException(e)
// technically it maybe didn't completely fail...
failedToLoad = true
}
return logLines
}
fun clearLogs() {
logJob?.cancel()
viewModelScope.launch(Dispatchers.IO) {
// -c = clear
Runtime.getRuntime().exec("logcat -c")
@@ -70,17 +111,40 @@ class LogViewModel: ViewModel() {
_lineState.clear()
}
fun getLogData(): String {
return _lineState.joinToString("\n") { it.asSimpleString }
private fun dumpLogcatText(): String {
val logCommand = if (filterCrashes) "logcat -b crash -d"
else "logcat -d"
val logProcess = try {
Runtime.getRuntime().exec(logCommand)
} catch (ioe: IOException) {
ioe.printStackTrace()
return ioe.stackTraceToString()
}
return logProcess.inputStream.bufferedReader().readText()
}
init {
fun getLogData(): String = if (failedToLoad) {
dumpLogcatText()
} else {
_lineState.joinToString("\n") { it.asSimpleString }
}
private fun loadLogs() {
_isLoading.value = true
viewModelScope.launch(Dispatchers.IO) {
logJob?.cancel()
logJob = viewModelScope.launch(Dispatchers.IO) {
val lines = logOutput()
_lineState.addAll(lines)
_isLoading.value = false
logJob = null
}
}
init {
loadLogs()
}
}
@@ -79,24 +79,6 @@ fun GeodeLauncherTheme(
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
with ((view.context as Activity).window) {
WindowCompat.setDecorFitsSystemWindows(this, false)
statusBarColor = Color.Transparent.toArgb()
navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(this, view).apply {
isAppearanceLightStatusBars = theme == Theme.LIGHT
isAppearanceLightNavigationBars = theme == Theme.LIGHT
}
}
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
@@ -1,4 +1,4 @@
package com.geode.launcher.api
package com.geode.launcher.updater
import com.geode.launcher.utils.LaunchUtils
import kotlinx.datetime.Instant
@@ -23,7 +23,8 @@ class Release(
val tagName: String,
val createdAt: Instant,
val publishedAt: Instant,
val assets: List<Asset>
val assets: List<Asset>,
val htmlUrl: String
) {
fun getDescription(): String {
if (tagName == "nightly") {
@@ -42,7 +43,7 @@ class Release(
return createdAt.epochSeconds
}
fun getAndroidDownload(): Asset? {
fun getGeodeDownload(): Asset? {
// try to find an asset that matches the architecture first
val platform = LaunchUtils.platformName
@@ -51,4 +52,22 @@ class Release(
it.name.endsWith(releaseSuffix)
}
}
fun getLauncherDownload(): Asset? {
val platform = LaunchUtils.platformName
val use32BitPlatform = platform == "android32"
return assets.find { asset ->
/* you know it's good when you pull out the truth table
* u32bp | contains | found
* 1 | 1 | 1
* 0 | 1 | 0
* 1 | 0 | 0
* 0 | 0 | 1
* surprise! it's an xnor
*/
(asset.name.contains("android32")) == use32BitPlatform
}
}
}
@@ -1,9 +1,11 @@
package com.geode.launcher.utils
package com.geode.launcher.updater
import android.content.Context
import android.util.Log
import com.geode.launcher.api.Release
import com.geode.launcher.api.ReleaseRepository
import com.geode.launcher.BuildConfig
import com.geode.launcher.utils.DownloadUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
@@ -15,6 +17,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import okhttp3.Cache
import okhttp3.OkHttpClient
import okio.buffer
import okio.source
import java.io.File
import java.io.InterruptedIOException
@@ -30,7 +34,7 @@ class ReleaseManager private constructor(
private lateinit var managerInstance: ReleaseManager
fun get(context: Context): ReleaseManager {
if (!::managerInstance.isInitialized) {
if (!Companion::managerInstance.isInitialized) {
val applicationContext = context.applicationContext
val httpClient = OkHttpClient.Builder()
@@ -74,6 +78,9 @@ class ReleaseManager private constructor(
val isInUpdate: Boolean
get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished
private val _availableLauncherUpdate = MutableStateFlow<Release?>(null)
val availableLauncherUpdate = _availableLauncherUpdate.asStateFlow()
private fun sendError(e: Exception) {
_uiState.value = ReleaseManagerState.Failure(e)
@@ -84,21 +91,24 @@ class ReleaseManager private constructor(
}
}
fun dismissLauncherUpdate() {
val update = availableLauncherUpdate.value ?: return
val sharedPreferences = PreferenceUtils.get(applicationContext)
sharedPreferences.setString(PreferenceUtils.Key.LAST_DISMISSED_UPDATE, update.tagName)
_availableLauncherUpdate.value = null
}
private suspend fun getLatestRelease(): Release? {
val sharedPreferences = PreferenceUtils.get(applicationContext)
val useNightly = sharedPreferences.getBoolean(PreferenceUtils.Key.RELEASE_CHANNEL)
val latestRelease = if (useNightly) {
releaseRepository.getLatestNightlyRelease()
} else {
releaseRepository.getLatestRelease()
}
return latestRelease
return releaseRepository.getLatestGeodeRelease(useNightly)
}
private suspend fun performUpdate(release: Release) {
val releaseAsset = release.getAndroidDownload()
val releaseAsset = release.getGeodeDownload()
if (releaseAsset == null) {
val noAssetException = Exception("missing Android download")
_uiState.value = ReleaseManagerState.Failure(noAssetException)
@@ -110,7 +120,10 @@ class ReleaseManager private constructor(
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size.toLong())
try {
val fileStream = DownloadUtils.downloadStream(httpClient, releaseAsset.browserDownloadUrl) { progress, outOf ->
val fileStream = DownloadUtils.downloadStream(
httpClient,
releaseAsset.browserDownloadUrl
) { progress, outOf ->
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
}
@@ -145,13 +158,30 @@ class ReleaseManager private constructor(
}
val sharedPreferences = PreferenceUtils.get(applicationContext)
val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED)
?: return false
val fileLastModified = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED)
if (fileLastModified == 0L) {
return false
val currentFileHash = computeFileHash(geodeFile)
return originalFileHash != currentFileHash
}
private fun checkLauncherUpdate(launcherUpdate: Release) {
if (BuildConfig.DEBUG) {
println("ignoring launcher update check - in debug mode!")
return
}
return fileLastModified != geodeFile.lastModified()
val sharedPreferences = PreferenceUtils.get(applicationContext)
val lastDismissedVersion = sharedPreferences.getString(PreferenceUtils.Key.LAST_DISMISSED_UPDATE)
if (!lastDismissedVersion.isNullOrEmpty() && launcherUpdate.tagName == lastDismissedVersion) {
return
}
if (launcherUpdate.tagName != BuildConfig.VERSION_NAME) {
_availableLauncherUpdate.value = launcherUpdate
}
}
private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) {
@@ -162,6 +192,17 @@ class ReleaseManager private constructor(
return
}
val launcherRelease = try {
releaseRepository.getLatestLauncherRelease()
} catch (e: Exception) {
sendError(e)
return
}
if (launcherRelease != null) {
checkLauncherUpdate(launcherRelease)
}
if (release == null) {
_uiState.value = ReleaseManagerState.Finished()
return
@@ -176,7 +217,7 @@ class ReleaseManager private constructor(
val geodeFile = getGeodeOutputPath()
// check if an update is needed
if (latestVersion <= currentVersion && geodeFile.exists()) {
if (latestVersion == currentVersion && geodeFile.exists()) {
_uiState.value = ReleaseManagerState.Finished()
return
}
@@ -203,13 +244,16 @@ class ReleaseManager private constructor(
release.getDescriptor()
)
// store hash as modified time is unreliable (don't know why)
val outputFile = getGeodeOutputPath()
sharedPreferences.setLong(
PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED,
outputFile.lastModified()
)
val fileHash = computeFileHash(outputFile)
sharedPreferences.setString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED, fileHash)
}
private fun computeFileHash(file: File): String =
file.source().buffer().readByteString().md5().hex()
private fun getGeodeOutputPath(): File {
val geodeName = LaunchUtils.geodeFilename
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
@@ -1,4 +1,4 @@
package com.geode.launcher.api
package com.geode.launcher.updater
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
import kotlinx.serialization.ExperimentalSerializationApi
@@ -12,20 +12,24 @@ import java.net.URL
class ReleaseRepository(private val httpClient: OkHttpClient) {
companion object {
private const val GITHUB_API_BASE = "https://api.github.com/repos/geode-sdk/geode"
private const val GITHUB_API_BASE = "https://api.github.com"
private const val GITHUB_API_HEADER = "X-GitHub-Api-Version"
private const val GITHUB_API_VERSION = "2022-11-28"
}
suspend fun getLatestNightlyRelease(): Release? {
val nightlyPath = "$GITHUB_API_BASE/releases/tags/nightly"
val url = URL(nightlyPath)
suspend fun getLatestLauncherRelease(): Release? {
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
val url = URL(releasePath)
return getReleaseByUrl(url)
}
suspend fun getLatestRelease(): Release? {
val releasePath = "$GITHUB_API_BASE/releases/latest"
suspend fun getLatestGeodeRelease(isNightly: Boolean = false): Release? {
val geodeBaseUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases"
val releasePath = if (isNightly) "$geodeBaseUrl/tags/nightly"
else "$geodeBaseUrl/latest"
val url = URL(releasePath)
return getReleaseByUrl(url)
@@ -1,4 +1,4 @@
package com.geode.launcher.api
package com.geode.launcher.updater
import android.app.Application
import androidx.lifecycle.ViewModel
@@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.AP
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.geode.launcher.utils.ReleaseManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -53,6 +53,8 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
val isInUpdate
get() = ReleaseManager.get(application).isInUpdate
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
var hasPerformedCheck = false
private set
@@ -78,7 +80,7 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
fun useGlobalCheckState() {
hasPerformedCheck = true
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
val releaseFlow = ReleaseManager.get(application)
.uiState
@@ -89,11 +91,17 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
fun runReleaseCheck(isManual: Boolean = false) {
hasPerformedCheck = true
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
val releaseFlow = ReleaseManager.get(application)
.checkForUpdates(isManual)
syncUiState(releaseFlow)
}
}
fun dismissLauncherUpdate() {
viewModelScope.launch(Dispatchers.IO) {
ReleaseManager.get(application).dismissLauncherUpdate()
}
}
}
@@ -13,8 +13,8 @@ object Constants {
@SuppressLint("SdCardPath")
const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}"
const val SUPPORTED_VERSION_CODE = 37L
const val SUPPORTED_VERSION_STRING = "2.2.13"
const val SUPPORTED_VERSION_CODE = 38L
const val SUPPORTED_VERSION_STRING = "2.205"
const val LAUNCHER_KEY_RETURN_TITLE = "return_title"
const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message"
@@ -116,19 +116,13 @@ object DownloadUtils {
fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
// gotta love copying
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FileUtils.copy(inputStream, outputStream)
inputStream.use { input -> outputStream.use { output ->
FileUtils.copy(input, output)
}}
} else {
inputStream.use { input ->
outputStream.use { output ->
val buffer = ByteArray(4 * 1024)
while (true) {
val byteCount = input.read(buffer)
if (byteCount < 0) break
output.write(buffer, 0, byteCount)
}
output.flush()
}
}
inputStream.use { input -> outputStream.use { output ->
input.copyTo(output)
}}
}
}
}
@@ -0,0 +1,196 @@
package com.geode.launcher.utils
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import java.io.File
class FileUtils {
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
// i am actually very lazy to move this to a separate class
// hello zmx i moved it
companion object {
fun getRealPathFromURI(context: Context, uri: Uri): String? {
when {
// DocumentProvider
DocumentsContract.isDocumentUri(context, uri) -> {
when {
// ExternalStorageProvider
isExternalStorageDocument(uri) -> {
return handleExternalStorageDocument(context, uri)
}
isDownloadsDocument(uri) -> {
return handleDownloadsDocument(context, uri)
}
isMediaDocument(uri) -> {
return handleMediaDocument(context, uri)
}
// TODO: add the geode provider
}
}
"content".equals(uri.scheme, ignoreCase = true) -> {
// Return the remote address
if (isGooglePhotosUri(uri)) return uri.lastPathSegment
// This happened with a directory
if (isExternalStorageDocument(uri)) {
return handleExternalStorageDocument(context, uri)
}
return getDataColumn(context, uri, null, null)
}
"file".equals(uri.scheme, ignoreCase = true) -> {
return uri.path
}
}
return null
}
private fun handleMediaDocument(context: Context, uri: Uri): String? {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(
context,
contentUri,
selection,
selectionArgs
)
}
private fun handleDownloadsDocument(context: Context, uri: Uri): String? {
val fileName = getFilePath(context, uri)
if (fileName != null) {
return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
}
var id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
id = id.replaceFirst("raw:".toRegex(), "")
val file = File(id)
if (file.exists()) return id
}
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
}
private fun handleExternalStorageDocument(context: Context, uri: Uri): String {
val docId: String = if (DocumentsContract.isDocumentUri(context, uri)) {
DocumentsContract.getDocumentId(uri)
} else {
// please please please PLEASE let this be the only other case
DocumentsContract.getTreeDocumentId(uri)
}
val split = docId.split(":").toTypedArray()
val type = split[0]
// This is for checking Main Memory
return if ("primary".equals(type, ignoreCase = true)) {
if (split.size > 1) {
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} else {
Environment.getExternalStorageDirectory().toString() + "/"
}
// This is for checking SD Card
} else {
"storage" + "/" + docId.replace(":", "/")
}
}
private fun getFilePath(context: Context, uri: Uri?): String? {
var cursor: Cursor? = null
val projection = arrayOf(
MediaStore.MediaColumns.DISPLAY_NAME
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(
uri, projection, null, null,
null
)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
val data = cursor.getString(index)
cursor.close()
return data
}
} catch (e: Exception) {
cursor?.close()
} finally {
cursor?.close()
}
return null
}
private fun getDataColumn(context: Context, uri: Uri?, selection: String?,
selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(
column
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(
uri, projection, selection, selectionArgs,
null
)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
val data = cursor.getString(index)
cursor.close()
return data
}
} catch (e: Exception) {
cursor?.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
}
}
@@ -1,87 +1,134 @@
package com.geode.launcher.utils
import android.app.Activity
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
import android.provider.OpenableColumns
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
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
import java.io.File
import java.lang.ref.WeakReference
import java.net.URI
import kotlin.system.exitProcess
@Suppress("unused")
@Suppress("unused", "KotlinJniMissingFunction")
object GeodeUtils {
private lateinit var activity: WeakReference<AppCompatActivity>
private lateinit var fileResultLauncher: ActivityResultLauncher<Intent>
private lateinit var filesResultLauncher: ActivityResultLauncher<Intent>
private lateinit var openFileResultLauncher: ActivityResultLauncher<GeodeOpenFileActivityResult.OpenFileParams>
private lateinit var openDirectoryResultLauncher: ActivityResultLauncher<Uri?>
private lateinit var openFilesResultLauncher: ActivityResultLauncher<GeodeOpenFilesActivityResult.OpenFileParams>
private lateinit var saveFileResultLauncher: ActivityResultLauncher<GeodeSaveFileActivityResult.SaveFileParams>
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private lateinit var internalRequestPermissionsLauncher: ActivityResultLauncher<Array<String>>
private lateinit var internalRequestAllFilesLauncher: ActivityResultLauncher<Intent>
private var afterRequestPermissions: (() -> Unit)? = null
private var afterRequestPermissionsFailure: (() -> Unit)? = null
fun setContext(activity: AppCompatActivity) {
this.activity = WeakReference(activity)
fileResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
if (data != null) {
val uri = data.data
if (uri != null) {
val path = getRealPathFromURI(activity, uri)
if (path != null) {
selectFileCallback(path)
return@registerForActivityResult
}
}
openFileResultLauncher = activity.registerForActivityResult(GeodeOpenFileActivityResult()) { uri ->
if (uri != null) {
val path = FileUtils.getRealPathFromURI(activity, uri)
if (path != null) {
selectFileCallback(path)
return@registerForActivityResult
}
}
failedCallback()
}
filesResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
if (data != null) {
val clipData = data.clipData
if (clipData != null) {
val paths = Array(clipData.itemCount) { "" }
for (i in 0 until clipData.itemCount) {
val path = getRealPathFromURI(activity, clipData.getItemAt(i).uri)
if (path != null) {
paths[i] = path
}
}
selectFilesCallback(paths)
return@registerForActivityResult
}
openDirectoryResultLauncher = activity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
if (it != null) {
val path = FileUtils.getRealPathFromURI(activity, it)
if (path != null)
selectFileCallback(path)
return@registerForActivityResult
}
failedCallback()
}
openFilesResultLauncher = activity.registerForActivityResult(GeodeOpenFilesActivityResult()) { result ->
if (result.isEmpty()) {
failedCallback()
return@registerForActivityResult
}
val paths: Array<String> = Array(result.size) {"n = $it"}
for (i in result.indices) {
val path = FileUtils.getRealPathFromURI(activity, result[i])
if (path != null) {
paths[i] = path
}
}
selectFilesCallback(paths)
return@registerForActivityResult
}
saveFileResultLauncher = activity.registerForActivityResult(GeodeSaveFileActivityResult()) { uri ->
if (uri != null) {
val path = FileUtils.getRealPathFromURI(activity, uri)
if (path != null) {
selectFileCallback(path)
return@registerForActivityResult
}
}
failedCallback()
}
requestPermissionLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
permissionCallback(isGranted)
}
internalRequestPermissionsLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val isGranted = permissions.values.all { it }
if (isGranted) {
afterRequestPermissions?.invoke()
} else {
Toast.makeText(activity, R.string.missing_permissions, Toast.LENGTH_SHORT)
.show()
afterRequestPermissionsFailure?.invoke()
}
}
// only necessary on newer android versions
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.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
fun getLogcatCrashBuffer(): String {
try {
return try {
val logcatProcess = Runtime.getRuntime().exec("logcat -v brief -b crash -d")
val logcatOutput = logcatProcess.inputStream.bufferedReader().readText()
return logcatOutput
}
catch (e: Exception) {
logcatProcess.inputStream.bufferedReader().readText()
} catch (e: Exception) {
Log.e("Geode", "Failed to get logcat crash buffer", e)
return ""
""
}
}
@@ -111,115 +158,185 @@ object GeodeUtils {
activity.get()?.run {
packageManager.getLaunchIntentForPackage(packageName)?.also {
val mainIntent = Intent.makeRestartActivityTask(it.component)
mainIntent.putExtra("restarted", true);
mainIntent.putExtra("restarted", true)
startActivity(mainIntent)
exitProcess(0)
}
}
}
// 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
}
}
external fun selectFileCallback(path: String)
private external fun selectFileCallback(path: String)
external fun selectFilesCallback(paths: Array<String>)
private external fun selectFilesCallback(paths: Array<String>)
external fun failedCallback()
private 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
fun selectFile(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("*/*")
if (intent.resolveActivity(packageManager) != null) {
print("Geode Selectfile")
fileResultLauncher.launch(intent)
print("Geode launched ")
return true
}
var uri: Uri?
DocumentFile.fromFile(File(path)).also {
uri = it.uri
}
return try {
checkForFilePermissions(
onSuccess = {
openFileResultLauncher.launch(GeodeOpenFileActivityResult.OpenFileParams(arrayOf("*/*"), uri))
},
onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
}
return false
}
@JvmStatic
fun selectFiles(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.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) {
filesResultLauncher.launch(intent)
return true
}
var uri: Uri?
DocumentFile.fromFile(File(path)).also {
uri = it.uri
}
return try {
checkForFilePermissions(
onSuccess = {
openFilesResultLauncher.launch(GeodeOpenFilesActivityResult.OpenFileParams(arrayOf("*/*"), uri))
},
onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
}
return false
}
@JvmStatic
fun selectFolder(path: String): Boolean {
activity.get()?.run {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
}
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) {
fileResultLauncher.launch(intent)
return true
}
var uri: Uri?
DocumentFile.fromFile(File(path)).also {
uri = it.uri
}
return false
return try {
checkForFilePermissions(
onSuccess = {
openDirectoryResultLauncher.launch(uri)
},
onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
}
}
@JvmStatic
fun createFile(path: String): Boolean {
activity.get()?.run {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
}
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) {
fileResultLauncher.launch(intent)
return true
}
var uri: Uri?
DocumentFile.fromFile(File(path)).also {
uri = it.uri
}
return try {
checkForFilePermissions(
onSuccess = {
saveFileResultLauncher.launch(GeodeSaveFileActivityResult.SaveFileParams(null, uri))
},
onFailure = { failedCallback() }
)
true
} catch (e: ActivityNotFoundException) {
false
}
return false
}
@JvmStatic
@@ -235,7 +352,8 @@ object GeodeUtils {
}
private val gameVersionMap = mapOf(
37L to "2.200"
37L to "2.200",
38L to "2.205"
)
@JvmStatic
@@ -249,155 +367,109 @@ object GeodeUtils {
return ""
}
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
// i am actually very lazy to move this to a separate class
fun getRealPathFromURI(context: Context, uri: Uri): String? {
when {
// DocumentProvider
DocumentsContract.isDocumentUri(context, uri) -> {
when {
// ExternalStorageProvider
isExternalStorageDocument(uri) -> {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
// This is for checking Main Memory
return if ("primary".equals(type, ignoreCase = true)) {
if (split.size > 1) {
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} else {
Environment.getExternalStorageDirectory().toString() + "/"
}
// This is for checking SD Card
} else {
"storage" + "/" + docId.replace(":", "/")
}
}
isDownloadsDocument(uri) -> {
val fileName = getFilePath(context, uri)
if (fileName != null) {
return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
}
var id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
id = id.replaceFirst("raw:".toRegex(), "")
val file = File(id)
if (file.exists()) return id
}
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
}
isMediaDocument(uri) -> {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs)
}
// TODO: add the geode provider
}
}
"content".equals(uri.scheme, ignoreCase = true) -> {
// Return the remote address
if (isGooglePhotosUri(uri)) return uri.lastPathSegment
return getDataColumn(context, uri, null, null)
}
"file".equals(uri.scheme, ignoreCase = true) -> {
return uri.path
}
}
return null
}
fun getDataColumn(context: Context, uri: Uri?, selection: String?,
selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(
column
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs,
null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
fun getFilePath(context: Context, uri: Uri?): String? {
var cursor: Cursor? = null
val projection = arrayOf(
MediaStore.MediaColumns.DISPLAY_NAME
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, null, null,
null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
fun isGeodeUri(uri: Uri): Boolean {
return "com.geode.launcher.user" == uri.authority
}
private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal"
private const val MANAGE_ALL_FILES = "${INTERNAL_PERMISSION_PREFIX}.MANAGE_ALL_FILES"
@JvmStatic
fun getPermissionStatus(permission: String): Boolean {
val context = activity.get() ?: return false
return when (permission) {
MANAGE_ALL_FILES -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.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
}
try {
requestPermissionLauncher.launch(permission)
} catch (e: ActivityNotFoundException) {
permissionCallback(false)
}
}
private external fun permissionCallback(granted: Boolean)
const val ARGUMENT_SAFE_MODE = "--geode:safe-mode"
private var additionalLaunchArguments = arrayListOf<String>()
fun setAdditionalLaunchArguments(vararg args: String) {
additionalLaunchArguments.addAll(args)
}
fun clearLaunchArguments() = setAdditionalLaunchArguments()
@JvmStatic
fun getLaunchArguments(): String? {
activity.get()?.apply {
val preferences = PreferenceUtils.get(this)
val userArgs = preferences.getString(PreferenceUtils.Key.LAUNCH_ARGUMENTS)
val args = if (!userArgs.isNullOrEmpty()) {
listOf(userArgs) + additionalLaunchArguments
} else additionalLaunchArguments
return args.joinToString(" ")
}
return null
}
interface CapabilityListener {
fun onCapabilityAdded(capability: String): Boolean
}
const val CAPABILITY_EXTENDED_INPUT = "extended_input"
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)
}
@@ -11,10 +11,6 @@ import android.util.Log
import java.io.File
object LaunchUtils {
enum class FailureReason {
NOT_FOUND, ABI_MISMATCH, UNKNOWN
}
fun isGeometryDashInstalled(packageManager: PackageManager): Boolean {
return try {
packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
@@ -40,7 +36,7 @@ object LaunchUtils {
return game.versionName
}
fun diagnoseLoadErrors(context: Context, packageInfo: PackageInfo): FailureReason {
fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean {
val abi = applicationArchitecture
val isExtracted =
packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS
@@ -50,33 +46,42 @@ object LaunchUtils {
"Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi"
Log.i("GeodeLauncher", metadata)
// easiest check! these messages are hardcoded in bionic/linker/linker_phdr.cpp
if (loadException.message?.contains("32-bit instead of 64-bit") == true) {
return true
}
if (loadException.message?.contains("64-bit instead of 32-bit") == true) {
return true
}
// determine if it can find gd libraries for the opposite architecture
val gdBinaryName = "lib${Constants.COCOS_LIB_NAME}.so"
val oppositeArchitecture = if (abi == "arm64-v8a") "armeabi-v7a" else "arm64-v8a"
if (packageInfo.applicationInfo.nativeLibraryDir.contains(oppositeArchitecture)) {
return FailureReason.ABI_MISMATCH
// detect issues using native library directory (only works on extracted libraries)
val nativeLibraryDir = packageInfo.applicationInfo.nativeLibraryDir
if (nativeLibraryDir.contains(oppositeArchitecture)) {
return true
}
// android has some odd behavior where 32bit libraries get installed to the arm directory instead of armeabi-v7a
// detect it, ig
if (oppositeArchitecture == "armeabi-v7a" && nativeLibraryDir.contains("/lib/arm/")) {
return true
}
if (!isExtracted) {
try {
context.assets.openNonAssetFd("lib/$oppositeArchitecture/$gdBinaryName")
return FailureReason.ABI_MISMATCH
return true
} catch (_: Exception) {
// this is good, actually!
}
}
// try fetching for its own libraries (only for uncompressed libraries)
if (!isExtracted) {
try {
context.assets.openNonAssetFd("lib/$abi/$gdBinaryName")
} catch (_: Exception) {
return FailureReason.NOT_FOUND
}
}
return FailureReason.UNKNOWN
return false
}
// supposedly CPU_ABI returns the current arch for the running application
@@ -84,8 +89,9 @@ object LaunchUtils {
@Suppress("DEPRECATION")
val applicationArchitecture: String = Build.CPU_ABI
val platformName: String = if (applicationArchitecture == "arm64-v8a")
"android64" else "android32"
val is64bit = applicationArchitecture == "arm64-v8a"
val platformName: String = if (is64bit) "android64" else "android32"
val geodeFilename: String = "Geode.$platformName.so"
@@ -13,6 +13,7 @@ import androidx.core.content.edit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import com.geode.launcher.BuildConfig
/**
* Extension object for SharedPreferences to add better key safety and default values.
@@ -23,36 +24,69 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
@Composable
fun useBooleanPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> {
return usePreference(preferenceKey, lifecycleOwner) { p, k ->
p.getBoolean(k)
}
return usePreference(
preferenceKey,
lifecycleOwner,
preferenceGet = { p, k -> p.getBoolean(k) },
preferenceSet = { p, k, v -> p.setBoolean(k, v) }
)
}
@Composable
fun useStringPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<String?> {
return usePreference(preferenceKey, lifecycleOwner) { p, k ->
p.getString(k)
}
return usePreference(
preferenceKey,
lifecycleOwner,
preferenceGet = { p, k -> p.getString(k) },
preferenceSet = { p, k, v -> p.setString(k, v) }
)
}
@Composable
fun useIntPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Int> {
return usePreference(preferenceKey, lifecycleOwner) { p, k ->
p.getInt(k)
}
return usePreference(
preferenceKey,
lifecycleOwner,
preferenceGet = { p, k -> p.getInt(k) },
preferenceSet = { p, k, v -> p.setInt(k, v) }
)
}
@Composable
private fun <T> usePreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, preferenceGet: (PreferenceUtils, Key) -> T): MutableState<T> {
fun useLongPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Long> {
return usePreference(
preferenceKey,
lifecycleOwner,
preferenceGet = { p, k -> p.getLong(k) },
preferenceSet = { p, k, v -> p.setLong(k, v) }
)
}
@Composable
private fun <T> usePreference(
preferenceKey: Key,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
preferenceGet: (PreferenceUtils, Key) -> T,
preferenceSet: (PreferenceUtils, Key, T) -> Unit
): MutableState<T> {
val context = LocalContext.current
val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE)
val preferences = get(sharedPreferences)
val preferenceValue = remember {
mutableStateOf(
preferenceGet(preferences, preferenceKey)
)
val state = mutableStateOf(preferenceGet(preferences, preferenceKey))
object : MutableState<T> by state {
override var value: T
get() = state.value
set(value) {
// ignore setting the same thing (messes up default preferences)
if (state.value == value) { return }
state.value = value
preferenceSet(preferences, preferenceKey, value)
}
}
}
val listener = SharedPreferences.OnSharedPreferenceChangeListener {
@@ -98,11 +132,16 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
THEME,
BLACK_BACKGROUND,
CURRENT_RELEASE_MODIFIED,
LAST_DISMISSED_UPDATE,
DISMISSED_GJ_UPDATE,
LAUNCH_ARGUMENTS,
IGNORE_LOAD_FAILURE
}
private fun defaultValueForBooleanKey(key: Key): Boolean {
return when (key) {
Key.UPDATE_AUTOMATICALLY -> true
Key.RELEASE_CHANNEL -> BuildConfig.DEBUG
else -> false
}
}
@@ -117,7 +156,11 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor"
Key.THEME -> "PreferenceTheme"
Key.BLACK_BACKGROUND -> "PreferenceBlackBackground"
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModified"
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash"
Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate"
Key.LAUNCH_ARGUMENTS -> "PreferenceLaunchArguments"
Key.IGNORE_LOAD_FAILURE -> "PreferenceIgnoreLoadFailure"
}
}
@@ -128,6 +171,13 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
return sharedPreferences.getBoolean(keyName, defaultValue)
}
fun setBoolean(key: Key, value: Boolean) {
val keyName = keyToName(key)
sharedPreferences.edit {
putBoolean(keyName, value)
}
}
fun toggleBoolean(key: Key): Boolean {
val currentValue = getBoolean(key)
val keyName = keyToName(key)
@@ -144,7 +194,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
return sharedPreferences.getString(keyName, null)
}
fun setString(key: Key, value: String) {
fun setString(key: Key, value: String?) {
val keyName = keyToName(key)
sharedPreferences.edit {
putString(keyName, value)
@@ -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
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,760Q546,760 593,713Q640,666 640,600L640,440Q640,374 593,327Q546,280 480,280Q414,280 367,327Q320,374 320,440L320,600Q320,666 367,713Q414,760 480,760ZM400,640L560,640L560,560L400,560L400,640ZM400,480L560,480L560,400L400,400L400,480ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520ZM480,840Q415,840 359.5,808Q304,776 272,720L160,720L160,640L244,640Q241,620 240.5,600Q240,580 240,560L160,560L160,480L240,480Q240,460 240.5,440Q241,420 244,400L160,400L160,320L272,320Q286,297 303.5,277Q321,257 344,242L280,176L336,120L422,206Q450,197 479,197Q508,197 536,206L624,120L680,176L614,242Q637,257 655.5,276.5Q674,296 688,320L800,320L800,400L716,400Q719,420 719.5,440Q720,460 720,480L800,480L800,560L720,560Q720,580 719.5,600Q719,620 716,640L800,640L800,720L688,720Q656,776 600.5,808Q545,840 480,840Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M400,720L400,640L560,640L560,720L400,720ZM240,520L240,440L720,440L720,520L240,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M840,280L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L680,120L840,280ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM240,400L600,400L600,240L240,240L240,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/>
</vector>
+33 -6
View File
@@ -12,6 +12,7 @@
<string name="launcher_launch_icon_alt">play icon</string>
<string name="launcher_download_icon_alt">download icon</string>
<string name="launcher_settings_icon_alt">settings icon</string>
<string name="launcher_retry_icon_alt">retry icon</string>
<string name="geode_download_title">Geode is not installed.</string>
<string name="geode_download_description">Geode is downloading. Please wait.</string>
<plurals name="automatically_load_countdown">
@@ -21,9 +22,16 @@
<string name="launcher_warning_icon_alt">warning</string>
<string name="launcher_error_icon_alt">error</string>
<string name="launcher_unsupported_version_title">Unsupported version</string>
<string name="launcher_unsupported_version_description">
The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s).\nLaunch will continue, but crashes may occur!
<string name="launcher_outdated_version_description">
The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s). You must update Geometry Dash to fix this.\nLaunch will continue, but crashes may occur!
</string>
<string name="launcher_unsupported_version_description">
The current version of Geometry Dash is not compatible with the version supported by the launcher. A launcher update is required to fix this.\nLaunch will continue, but crashes may occur!
</string>
<string name="launcher_update_available">A new launcher update is available!</string>
<string name="launcher_update_dismiss">Dismiss</string>
<string name="safe_mode_enable_title">Enable safe mode?</string>
<string name="safe_mode_enable_description">Safe mode disables loading of all mods, allowing one to remove problematic mods or diagnose crashes. If you\'re not experiencing issues with Geode, you should probably not be using safe mode.</string>
<!-- Updater strings -->
<string name="release_fetch_failed">Update failed.\n%1$s</string>
@@ -40,11 +48,15 @@
<!-- GeometryDashActivity strings -->
<string name="load_failed_link_error">Failed to link libraries</string>
<string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture of the launcher and Geometry Dash do not match, or from a system bug.\nReported issue: %1$s.\nCheck application logs for more details.</string>
<string name="load_failed_link_error_description">Game libraries could not be found. 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_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_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_accept">OK</string>
<string name="message_box_continue">Continue</string>
<string name="missing_permissions">Missing required permissions for this action!</string>
<!-- SettingsActivity strings -->
<string name="title_activity_settings">Settings</string>
@@ -73,7 +85,8 @@
<string name="preference_release_channel_name">Use nightly release channel</string>
<string name="preference_check_for_updates_button">Check for updates</string>
<string name="preference_check_for_updates_description">Current version: %1$s</string>
<string name="preference_check_for_updates_description">Current loader version: %1$s</string>
<string name="preference_launcher_version">Geode Launcher v%1$s</string>
<string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string>
<string name="preference_check_for_updates_none_found">No updates found.</string>
<string name="preference_check_for_updates_success">Geode has been updated!</string>
@@ -81,10 +94,24 @@
<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_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 -->
<string name="title_activity_application_logs">Application Logs</string>
<string name="application_logs_no_logs">No logs available.</string>
<string name="application_logs_delete">Clear</string>
<string name="application_logs_delete">Clear All</string>
<string name="application_logs_share">Share</string>
<string name="application_log_copy_label">Log Message</string>
<string name="application_logs_more">More</string>
<string name="application_logs_export">Save To</string>
<string name="application_logs_level">Set Log Level</string>
<string name="application_logs_level_dialog_name">Set minimum log level</string>
<string name="application_logs_export_completed">Log export complete.</string>
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
<string name="application_logs_crash_only">Show Crash Buffer</string>
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
</resources>
-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
# 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")