mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-24 00:59:21 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
642fdc36ee | ||
|
|
c6155443a2 | ||
|
|
b96561bf33 | ||
|
|
61c018ee0d | ||
|
|
41e3e0a133 | ||
|
|
3853a469cd | ||
|
|
62b31b8924 | ||
|
|
ab1d0c96f4 | ||
|
|
96354096e2 | ||
|
|
25b686f183 | ||
|
|
7694236434 | ||
|
|
7c0f1da312 | ||
|
|
019d9747b5 | ||
|
|
e806ec223a | ||
|
|
9f58e76f97 | ||
|
|
371750a184 | ||
|
|
2494bda4e8 | ||
|
|
b67be09cd3 | ||
|
|
09b1c9bc3a | ||
|
|
483fb56e25 | ||
|
|
7857fae68f | ||
|
|
61b84cd223 | ||
|
|
bd67332adc | ||
|
|
84657fd6b3 | ||
|
|
f197b45f9e | ||
|
|
d0dc8bec5d | ||
|
|
c9e0408eb2 | ||
|
|
b50a5b2b54 | ||
|
|
c8e3d7404c | ||
|
|
c4c41e324e | ||
|
|
4d324913df | ||
|
|
19bd549b1f | ||
|
|
9dc501c9ab | ||
|
|
5521869ac9 | ||
|
|
c9a712f692 | ||
|
|
6a0b3fc8bd | ||
|
|
684fd99b83 | ||
|
|
fd83daa776 | ||
|
|
9a87716635 | ||
|
|
31566ce64b | ||
|
|
d078879e7b | ||
|
|
5fa42b6ca5 | ||
|
|
e164b87436 | ||
|
|
433d6a0f46 | ||
|
|
cf8b1edbe2 | ||
|
|
500e00a9cc | ||
|
|
c3f80e9c74 | ||
|
|
7eb06e175f | ||
|
|
141681be05 | ||
|
|
dcfab5e001 |
No files matched your search
@@ -7,6 +7,15 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
config:
|
||||||
|
- name: Release
|
||||||
|
lower: release
|
||||||
|
|
||||||
|
- name: Debug
|
||||||
|
lower: debug
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -27,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: |
|
run: |
|
||||||
./gradlew app:assembleDebug \
|
./gradlew app:assemble${{ matrix.config.name }} \
|
||||||
-Pandroid.injected.signing.store.file="$HOME/release.keystore" \
|
-Pandroid.injected.signing.store.file="$HOME/release.keystore" \
|
||||||
-Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \
|
-Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \
|
||||||
-Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \
|
-Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \
|
||||||
@@ -37,8 +46,8 @@ jobs:
|
|||||||
id: rename_apk_universal
|
id: rename_apk_universal
|
||||||
run: |
|
run: |
|
||||||
SHORT_SHA=${GITHUB_SHA::7}
|
SHORT_SHA=${GITHUB_SHA::7}
|
||||||
APK_PATH=${{github.workspace}}/app/build/outputs/apk/debug/app-universal-debug.apk
|
APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-universal-${{ matrix.config.lower }}.apk
|
||||||
OUT_PATH=${{github.workspace}}/geode-launcher-debug-$SHORT_SHA.apk
|
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.apk
|
||||||
mv $APK_PATH $OUT_PATH
|
mv $APK_PATH $OUT_PATH
|
||||||
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
@@ -46,15 +55,15 @@ jobs:
|
|||||||
id: rename_apk
|
id: rename_apk
|
||||||
run: |
|
run: |
|
||||||
SHORT_SHA=${GITHUB_SHA::7}
|
SHORT_SHA=${GITHUB_SHA::7}
|
||||||
APK_PATH=${{github.workspace}}/app/build/outputs/apk/debug/app-armeabi-v7a-debug.apk
|
APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-armeabi-v7a-${{ matrix.config.lower }}.apk
|
||||||
OUT_PATH=${{github.workspace}}/geode-launcher-debug-android32-$SHORT_SHA.apk
|
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-android32-$SHORT_SHA.apk
|
||||||
mv $APK_PATH $OUT_PATH
|
mv $APK_PATH $OUT_PATH
|
||||||
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload development build
|
- name: Upload development build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: geode-launcher-debug
|
name: geode-launcher-${{ matrix.config.lower }}
|
||||||
path: |
|
path: |
|
||||||
${{ steps.rename_apk_universal.outputs.path }}
|
${{ steps.rename_apk_universal.outputs.path }}
|
||||||
${{ steps.rename_apk.outputs.path }}
|
${{ steps.rename_apk.outputs.path }}
|
||||||
+23
@@ -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.
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
# Geode Android Launcher
|
# Geode Android Launcher
|
||||||
|
|
||||||
Launches a vanilla copy of Geometry Dash with the Geode loader added.
|
Launches a vanilla copy of Geometry Dash with the Geode loader added.
|
||||||
|
|
||||||
## TODO:
|
|
||||||
|
|
||||||
* Figure out better way of packaging libgeode
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdk 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.geode.launcher"
|
|
||||||
minSdk 23
|
|
||||||
targetSdk 34
|
|
||||||
versionCode 7
|
|
||||||
versionName "1.0.0"
|
|
||||||
|
|
||||||
vectorDrawables {
|
|
||||||
useSupportLibrary true
|
|
||||||
}
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
arguments "-DUSE_TULIPHOOK:BOOL=OFF"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ndkConfig.abiFilters "arm64-v8a", "armeabi-v7a"
|
|
||||||
}
|
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
enable true
|
|
||||||
reset()
|
|
||||||
|
|
||||||
//noinspection ChromeOsAbiSupport (not my fault)
|
|
||||||
include "arm64-v8a", "armeabi-v7a"
|
|
||||||
|
|
||||||
universalApk true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
// enables a polyfill for java Instant on api levels < 26 (used for updater)
|
|
||||||
coreLibraryDesugaringEnabled true
|
|
||||||
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
buildFeatures {
|
|
||||||
compose true
|
|
||||||
buildConfig true
|
|
||||||
}
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion compose_compiler
|
|
||||||
}
|
|
||||||
packagingOptions {
|
|
||||||
resources {
|
|
||||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path file('src/main/cpp/CMakeLists.txt')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
namespace 'com.geode.launcher'
|
|
||||||
ndkVersion '26.1.10909125'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation platform('androidx.compose:compose-bom:2023.10.01')
|
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
|
||||||
implementation "androidx.compose.ui:ui"
|
|
||||||
implementation 'androidx.compose.material3:material3'
|
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview"
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
|
|
||||||
implementation 'androidx.activity:activity-compose:1.8.2'
|
|
||||||
implementation 'androidx.activity:activity-ktx:1.8.2'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
|
||||||
implementation "androidx.documentfile:documentfile:1.0.1"
|
|
||||||
implementation "com.squareup.okio:okio:3.7.0"
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.0"
|
|
||||||
debugImplementation "androidx.compose.ui:ui-tooling"
|
|
||||||
debugImplementation "androidx.compose.ui:ui-test-manifest"
|
|
||||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
|
|
||||||
}
|
|
||||||
@@ -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,8 +9,15 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.wifi"
|
android:name="android.hardware.wifi"
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
@@ -51,9 +51,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
@@ -104,16 +106,20 @@ fun getLogColor(priority: LogPriority): Color {
|
|||||||
// i just copied these from my ide tbh
|
// i just copied these from my ide tbh
|
||||||
return if (isDark) {
|
return if (isDark) {
|
||||||
when (priority) {
|
when (priority) {
|
||||||
LogPriority.ERROR -> Color(0xffdf5151)
|
LogPriority.ERROR -> Color(0xff_df_51_51)
|
||||||
LogPriority.WARN -> Color(0xffbcb500)
|
LogPriority.WARN -> Color(0xff_bc_b5_00)
|
||||||
LogPriority.FATAL -> Color(0xffb8b6fc)
|
LogPriority.FATAL -> Color(0xff_b8_b6_fc)
|
||||||
|
LogPriority.DEBUG -> Color(0xff_aa_aa_aa)
|
||||||
|
LogPriority.VERBOSE -> Color(0xff_77_77_77)
|
||||||
else -> Color.Unspecified
|
else -> Color.Unspecified
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
when (priority) {
|
when (priority) {
|
||||||
LogPriority.ERROR -> Color(0xffdf5151)
|
LogPriority.ERROR -> Color(0xff_df_51_51)
|
||||||
LogPriority.WARN -> Color(0xffeab700)
|
LogPriority.WARN -> Color(0xff_ea_b7_00)
|
||||||
LogPriority.FATAL -> Color(0xffa142f4)
|
LogPriority.FATAL -> Color(0xff_a1_42_f4)
|
||||||
|
LogPriority.DEBUG -> Color(0xff_77_77_77)
|
||||||
|
LogPriority.VERBOSE -> Color(0xff_aa_aa_aa)
|
||||||
else -> Color.Unspecified
|
else -> Color.Unspecified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,16 +144,39 @@ fun copyLogText(context: Context, logMessage: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectLogLevelDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSelect: (LogPriority) -> Unit,
|
||||||
|
initialValue: LogPriority
|
||||||
|
) {
|
||||||
|
SelectDialog(
|
||||||
|
title = stringResource(R.string.application_logs_level_dialog_name),
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onSelect = { onSelect(it) },
|
||||||
|
initialValue = initialValue,
|
||||||
|
toLabel = {
|
||||||
|
// i'm lazy
|
||||||
|
it.name.lowercase().replaceFirstChar { c -> c.uppercaseChar() }
|
||||||
|
},
|
||||||
|
optionsCount = LogPriority.FATAL downTo LogPriority.VERBOSE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onLongPress = {
|
onLongPress = {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
copyLogText(context, logLine.asSimpleString)
|
copyLogText(context, logLine.asSimpleString)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -205,6 +234,18 @@ fun ApplicationLogsScreen(
|
|||||||
listState.scrollToItem(logLines.size)
|
listState.scrollToItem(logLines.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showLogLevelSelect by remember { mutableStateOf(false) }
|
||||||
|
if (showLogLevelSelect) {
|
||||||
|
SelectLogLevelDialog(
|
||||||
|
onDismissRequest = { showLogLevelSelect = false },
|
||||||
|
onSelect = {
|
||||||
|
logViewModel.logLevel = it
|
||||||
|
showLogLevelSelect = false
|
||||||
|
},
|
||||||
|
initialValue = logViewModel.logLevel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -213,7 +254,7 @@ fun ApplicationLogsScreen(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = stringResource(R.string.back_icon_alt)
|
contentDescription = stringResource(R.string.back_icon_alt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -238,7 +279,7 @@ fun ApplicationLogsScreen(
|
|||||||
contentDescription = stringResource(R.string.application_logs_share)
|
contentDescription = stringResource(R.string.application_logs_share)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = { showMoreOptions = !showMoreOptions },) {
|
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.MoreVert,
|
Icons.Filled.MoreVert,
|
||||||
contentDescription = stringResource(R.string.application_logs_more)
|
contentDescription = stringResource(R.string.application_logs_more)
|
||||||
@@ -252,7 +293,7 @@ fun ApplicationLogsScreen(
|
|||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_filter_list),
|
painterResource(R.drawable.icon_bug_report),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -272,6 +313,22 @@ fun ApplicationLogsScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_filter_list),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.application_logs_level))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
showLogLevelSelect = true
|
||||||
|
showMoreOptions = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -345,7 +402,7 @@ fun ApplicationLogsScreen(
|
|||||||
logLines
|
logLines
|
||||||
) { line ->
|
) { line ->
|
||||||
LogCard(line)
|
LogCard(line)
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.geode.launcher
|
package com.geode.launcher
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -8,7 +7,6 @@ import android.content.IntentFilter
|
|||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
@@ -37,7 +35,7 @@ import java.io.FileInputStream
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
|
||||||
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener {
|
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener {
|
||||||
private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null
|
private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null
|
||||||
private val sTag = GeometryDashActivity::class.simpleName
|
private val sTag = GeometryDashActivity::class.simpleName
|
||||||
private var mIsRunning = false
|
private var mIsRunning = false
|
||||||
@@ -59,14 +57,18 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
try {
|
try {
|
||||||
tryLoadGame()
|
tryLoadGame()
|
||||||
} catch (e: UnsatisfiedLinkError) {
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
e.printStackTrace()
|
Log.e("GeodeLauncher", "Library linkage failure", e)
|
||||||
|
|
||||||
// generates helpful information for use in debugging library load failures
|
// generates helpful information for use in debugging library load failures
|
||||||
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
|
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
|
||||||
val abiMismatch = LaunchUtils.detectAbiMismatch(this, gdPackageInfo, e)
|
val abiMismatch = LaunchUtils.detectAbiMismatch(this, gdPackageInfo, e)
|
||||||
|
|
||||||
val errorMessage = if (abiMismatch) getString(R.string.load_failed_abi_error_description) else
|
val is64bit = LaunchUtils.is64bit
|
||||||
getString(R.string.load_failed_link_error_description)
|
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(
|
returnToMain(
|
||||||
getString(R.string.load_failed_link_error),
|
getString(R.string.load_failed_link_error),
|
||||||
@@ -75,7 +77,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
|
|
||||||
return
|
return
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e("GeodeLauncher", "Uncaught error during game load", e)
|
||||||
|
|
||||||
returnToMain(
|
returnToMain(
|
||||||
getString(R.string.load_failed_generic_error),
|
getString(R.string.load_failed_generic_error),
|
||||||
@@ -104,22 +106,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
|
|
||||||
setupRedirection(gdPackageInfo)
|
setupRedirection(gdPackageInfo)
|
||||||
|
|
||||||
// request read and write permissions
|
|
||||||
val hasStoragePerms = (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) and
|
|
||||||
(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
|
|
||||||
|
|
||||||
if (!hasStoragePerms) {
|
|
||||||
requestPermissions(
|
|
||||||
arrayOf(
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Cocos2dxHelper.init(this, this)
|
Cocos2dxHelper.init(this, this)
|
||||||
|
|
||||||
GeodeUtils.setContext(this)
|
GeodeUtils.setContext(this)
|
||||||
|
GeodeUtils.setCapabilityListener(this)
|
||||||
|
|
||||||
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
|
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
|
||||||
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
|
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
|
||||||
@@ -132,8 +122,25 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
|
|
||||||
setupPostLibraryLoad(gdPackageInfo)
|
setupPostLibraryLoad(gdPackageInfo)
|
||||||
|
|
||||||
if (!loadGeodeLibrary()) {
|
try {
|
||||||
|
loadGeodeLibrary()
|
||||||
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
|
handleGeodeException(e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleGeodeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleGeodeException(e: Throwable) {
|
||||||
|
e.printStackTrace()
|
||||||
|
|
||||||
|
val ignoreFailure = PreferenceUtils.get(this)
|
||||||
|
.getBoolean(PreferenceUtils.Key.IGNORE_LOAD_FAILURE)
|
||||||
|
|
||||||
|
if (ignoreFailure) {
|
||||||
Log.w("GeodeLauncher", "could not load Geode object!")
|
Log.w("GeodeLauncher", "could not load Geode object!")
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +148,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
try {
|
try {
|
||||||
LaunchUtils.addAssetsFromPackage(assets, packageInfo)
|
LaunchUtils.addAssetsFromPackage(assets, packageInfo)
|
||||||
} catch (e: NoSuchMethodException) {
|
} catch (e: NoSuchMethodException) {
|
||||||
e.printStackTrace()
|
Log.e("GeodeLauncher", "Failed to add asset redirection", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -153,7 +160,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR)
|
LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR)
|
||||||
LauncherFix.setDataPath(saveDir.path)
|
LauncherFix.setDataPath(saveDir.path)
|
||||||
} catch (e: UnsatisfiedLinkError) {
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
e.printStackTrace()
|
Log.e("GeodeLauncher", "Failed to load LauncherFix", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,26 +212,26 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||||
private fun loadGeodeLibrary(): Boolean {
|
private fun loadGeodeLibrary() {
|
||||||
// Load Geode if exists
|
// Load Geode if exists
|
||||||
// bundling the object with the application allows for nicer backtraces
|
// bundling the object with the application allows for nicer backtraces
|
||||||
try {
|
try {
|
||||||
// put libgeode.so in jniLibs/armeabi-v7a to get this
|
// put libgeode.so in jniLibs/armeabi-v7a to get this
|
||||||
System.loadLibrary("geode")
|
System.loadLibrary("geode")
|
||||||
return true
|
return
|
||||||
} catch (e: UnsatisfiedLinkError) {
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
// but users may prefer it stored with data
|
// but users may prefer it stored with data
|
||||||
val geodeFilename = LaunchUtils.geodeFilename
|
val geodeFilename = LaunchUtils.geodeFilename
|
||||||
val geodePath = File(filesDir.path, "launcher/$geodeFilename")
|
val geodePath = File(filesDir.path, "launcher/$geodeFilename")
|
||||||
if (geodePath.exists()) {
|
if (geodePath.exists()) {
|
||||||
System.load(geodePath.path)
|
System.load(geodePath.path)
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// you know zmx i have 0 clue what this does so im
|
// you know zmx i have 0 clue what this does so im
|
||||||
// just gonna like copy the binary from external
|
// just gonna like copy the binary from external
|
||||||
// also i get 20 million permission denied errors
|
// also i get 20 million permission denied errors
|
||||||
val externalGeodePath = LaunchUtils.getInstalledGeodePath(this) ?: return false
|
val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!!
|
||||||
|
|
||||||
val copiedPath = File(filesDir.path, "copied")
|
val copiedPath = File(filesDir.path, "copied")
|
||||||
if (copiedPath.exists()) {
|
if (copiedPath.exists()) {
|
||||||
@@ -241,17 +248,14 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (copiedGeodePath.exists()) {
|
if (copiedGeodePath.exists()) {
|
||||||
try {
|
println("Loading Geode from ${externalGeodePath.name}")
|
||||||
println("Loading Geode from ${externalGeodePath.name}")
|
System.load(copiedGeodePath.path)
|
||||||
System.load(copiedGeodePath.path)
|
return
|
||||||
} catch (e: UnsatisfiedLinkError) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createView(): FrameLayout {
|
private fun createView(): FrameLayout {
|
||||||
@@ -336,14 +340,19 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRestart() {
|
||||||
|
super.onRestart()
|
||||||
|
mIsOnPause = false
|
||||||
|
BaseRobTopActivity.isPaused = false
|
||||||
|
if (!this.mIsRunning) {
|
||||||
|
resumeGame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
mIsOnPause = true
|
mIsOnPause = true
|
||||||
BaseRobTopActivity.isPaused = true
|
BaseRobTopActivity.isPaused = true
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
if (mIsRunning) {
|
if (mIsRunning) {
|
||||||
pauseGame()
|
pauseGame()
|
||||||
}
|
}
|
||||||
@@ -441,4 +450,13 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCapabilityAdded(capability: String): Boolean {
|
||||||
|
if (capability == GeodeUtils.CAPABILITY_EXTENDED_INPUT) {
|
||||||
|
mGLSurfaceView?.useKeyboardEvents = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,8 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.PressInteraction
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -27,8 +29,11 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -46,7 +51,10 @@ import com.geode.launcher.utils.Constants
|
|||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import com.geode.launcher.updater.ReleaseManager
|
import com.geode.launcher.updater.ReleaseManager
|
||||||
|
import com.geode.launcher.utils.GeodeUtils
|
||||||
import com.geode.launcher.utils.useCountdownTimer
|
import com.geode.launcher.utils.useCountdownTimer
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import java.net.ConnectException
|
import java.net.ConnectException
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
@@ -122,7 +130,7 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String, onDis
|
|||||||
|
|
||||||
Spacer(Modifier.size(4.dp))
|
Spacer(Modifier.size(4.dp))
|
||||||
|
|
||||||
TextButton(onClick = { uriHandler.openUri(openTo) },) {
|
TextButton(onClick = { uriHandler.openUri(openTo) }) {
|
||||||
Text(stringResource(R.string.launcher_download))
|
Text(stringResource(R.string.launcher_download))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +145,7 @@ fun UpdateProgressIndicator(
|
|||||||
message: String,
|
message: String,
|
||||||
releaseViewModel: ReleaseViewModel,
|
releaseViewModel: ReleaseViewModel,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
progress: Float? = null
|
progress: (() -> Float)? = null
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
@@ -151,7 +159,7 @@ fun UpdateProgressIndicator(
|
|||||||
if (progress == null) {
|
if (progress == null) {
|
||||||
LinearProgressIndicator()
|
LinearProgressIndicator()
|
||||||
} else {
|
} else {
|
||||||
LinearProgressIndicator(progress)
|
LinearProgressIndicator(progress = progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -226,62 +234,91 @@ fun LoadFailedDialog(returnTitle: String, returnMessage: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateWarning() {
|
fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
val gdVersionCode = remember { LaunchUtils.getGeometryDashVersionCode(packageManager) }
|
val gdVersionCode = remember { LaunchUtils.getGeometryDashVersionCode(packageManager) }
|
||||||
val gdVersionString = remember { LaunchUtils.getGeometryDashVersionString(packageManager) }
|
val gdVersionString = remember { LaunchUtils.getGeometryDashVersionString(packageManager) }
|
||||||
|
|
||||||
var showUpdateWarning by remember { mutableStateOf(true) }
|
|
||||||
|
|
||||||
var lastDismissedVersion by PreferenceUtils.useLongPreference(
|
var lastDismissedVersion by PreferenceUtils.useLongPreference(
|
||||||
preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE
|
preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE
|
||||||
)
|
)
|
||||||
|
|
||||||
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && gdVersionCode != lastDismissedVersion) {
|
val canDismissRelease = gdVersionCode >= Constants.SUPPORTED_VERSION_CODE
|
||||||
if (showUpdateWarning) {
|
val shouldDismiss = canDismissRelease && gdVersionCode == lastDismissedVersion
|
||||||
AlertDialog(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Warning,
|
|
||||||
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(stringResource(R.string.launcher_unsupported_version_title))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(
|
|
||||||
R.string.launcher_unsupported_version_description,
|
|
||||||
gdVersionString, Constants.SUPPORTED_VERSION_STRING
|
|
||||||
))
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
showUpdateWarning = false
|
|
||||||
lastDismissedVersion = gdVersionCode
|
|
||||||
|
|
||||||
onLaunch(context)
|
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && !shouldDismiss) {
|
||||||
}) {
|
AlertDialog(
|
||||||
Text(stringResource(R.string.message_box_accept))
|
icon = {
|
||||||
}
|
Icon(
|
||||||
},
|
Icons.Filled.Warning,
|
||||||
dismissButton = {
|
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||||
TextButton(onClick = { showUpdateWarning = false }) {
|
)
|
||||||
Text(stringResource(R.string.message_box_cancel))
|
},
|
||||||
}
|
title = {
|
||||||
},
|
Text(stringResource(R.string.launcher_unsupported_version_title))
|
||||||
onDismissRequest = { showUpdateWarning = false }
|
},
|
||||||
)
|
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 {
|
} else {
|
||||||
LaunchedEffect(gdVersionCode) {
|
LaunchedEffect(gdVersionCode) {
|
||||||
onLaunch(context)
|
onLaunch(context, inSafeMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
|
||||||
|
AlertDialog(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Warning,
|
||||||
|
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
|
||||||
|
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.message_box_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onLaunch) {
|
||||||
|
Text(stringResource(R.string.message_box_continue))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) {
|
fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -312,8 +349,6 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ReleaseViewModel.ReleaseUIState.InDownload -> {
|
is ReleaseViewModel.ReleaseUIState.InDownload -> {
|
||||||
val progress = state.downloaded / state.outOf.toDouble()
|
|
||||||
|
|
||||||
val downloaded = remember(state.downloaded) {
|
val downloaded = remember(state.downloaded) {
|
||||||
formatShortFileSize(context, state.downloaded)
|
formatShortFileSize(context, state.downloaded)
|
||||||
}
|
}
|
||||||
@@ -330,7 +365,10 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
|
|||||||
),
|
),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
releaseViewModel = releaseViewModel,
|
releaseViewModel = releaseViewModel,
|
||||||
progress = progress.toFloat()
|
progress = {
|
||||||
|
val progress = state.downloaded / state.outOf.toDouble()
|
||||||
|
progress.toFloat()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
|
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
|
||||||
@@ -384,7 +422,13 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLaunch(context: Context) {
|
fun onLaunch(context: Context, safeMode: Boolean = false) {
|
||||||
|
if (safeMode) {
|
||||||
|
GeodeUtils.setAdditionalLaunchArguments(GeodeUtils.ARGUMENT_SAFE_MODE)
|
||||||
|
} else {
|
||||||
|
GeodeUtils.clearLaunchArguments()
|
||||||
|
}
|
||||||
|
|
||||||
val launchIntent = Intent(context, GeometryDashActivity::class.java)
|
val launchIntent = Intent(context, GeometryDashActivity::class.java)
|
||||||
launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
|
||||||
@@ -405,7 +449,7 @@ fun MainScreen(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val shouldAutomaticallyLaunch = PreferenceUtils.useBooleanPreference(
|
val shouldAutomaticallyLaunch by PreferenceUtils.useBooleanPreference(
|
||||||
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
|
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -418,6 +462,8 @@ fun MainScreen(
|
|||||||
val geodeInstalled = geodePreinstalled || geodeJustInstalled
|
val geodeInstalled = geodePreinstalled || geodeJustInstalled
|
||||||
|
|
||||||
var beginLaunch by remember { mutableStateOf(false) }
|
var beginLaunch by remember { mutableStateOf(false) }
|
||||||
|
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||||
|
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(shouldUpdate) {
|
LaunchedEffect(shouldUpdate) {
|
||||||
if (shouldUpdate && !releaseViewModel.hasPerformedCheck) {
|
if (shouldUpdate && !releaseViewModel.hasPerformedCheck) {
|
||||||
@@ -446,15 +492,11 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (gdInstalled && geodeInstalled) {
|
if (gdInstalled && geodeInstalled) {
|
||||||
if (shouldAutomaticallyLaunch.value && !releaseViewModel.isInUpdate && !disableAutomaticLaunch) {
|
val stopLaunch = releaseViewModel.isInUpdate || disableAutomaticLaunch || showSafeModeDialog
|
||||||
|
if (shouldAutomaticallyLaunch && !stopLaunch) {
|
||||||
val countdownTimer = useCountdownTimer(
|
val countdownTimer = useCountdownTimer(
|
||||||
time = 3000,
|
time = 3000,
|
||||||
onCountdownFinish = {
|
onCountdownFinish = { beginLaunch = true }
|
||||||
// just in case this changed async
|
|
||||||
if (shouldAutomaticallyLaunch.value) {
|
|
||||||
beginLaunch = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (countdownTimer != 0L) {
|
if (countdownTimer != 0L) {
|
||||||
@@ -471,10 +513,42 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compose apis don't provide a good way of adding long press to a button
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val viewConfiguration = LocalViewConfiguration.current
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
LaunchedEffect(interactionSource) {
|
||||||
|
interactionSource.interactions.collectLatest { interaction ->
|
||||||
|
when (interaction) {
|
||||||
|
is PressInteraction.Press -> {
|
||||||
|
launchInSafeMode = false
|
||||||
|
|
||||||
|
delay(viewConfiguration.longPressTimeoutMillis)
|
||||||
|
|
||||||
|
// perform a second delay to make the action more obvious
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
delay(viewConfiguration.longPressTimeoutMillis)
|
||||||
|
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
|
||||||
|
showSafeModeDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
is PressInteraction.Release -> {
|
||||||
|
if (!showSafeModeDialog) {
|
||||||
|
beginLaunch = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
Button(
|
Button(
|
||||||
onClick = { beginLaunch = true },
|
onClick = { },
|
||||||
enabled = !releaseViewModel.isInUpdate
|
enabled = !releaseViewModel.isInUpdate,
|
||||||
|
interactionSource = interactionSource
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.PlayArrow,
|
Icons.Filled.PlayArrow,
|
||||||
@@ -540,7 +614,24 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (beginLaunch) {
|
if (beginLaunch) {
|
||||||
UpdateWarning()
|
UpdateWarning(launchInSafeMode) {
|
||||||
|
beginLaunch = false
|
||||||
|
launchInSafeMode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSafeModeDialog) {
|
||||||
|
SafeModeDialog(
|
||||||
|
onDismiss = {
|
||||||
|
showSafeModeDialog = false
|
||||||
|
},
|
||||||
|
onLaunch = {
|
||||||
|
launchInSafeMode = true
|
||||||
|
beginLaunch = true
|
||||||
|
|
||||||
|
showSafeModeDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,22 +21,18 @@ import androidx.compose.animation.core.spring
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.selection.toggleable
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -45,7 +41,6 @@ import androidx.compose.ui.semantics.Role
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.geode.launcher.updater.ReleaseViewModel
|
import com.geode.launcher.updater.ReleaseViewModel
|
||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
@@ -84,16 +79,6 @@ class SettingsActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
fun onExportSaveData(context: Context) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"This function is not implemented yet!",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
fun onOpenFolder(context: Context) {
|
fun onOpenFolder(context: Context) {
|
||||||
val file = LaunchUtils.getBaseDirectory(context)
|
val file = LaunchUtils.getBaseDirectory(context)
|
||||||
val clipboardManager =
|
val clipboardManager =
|
||||||
@@ -160,7 +145,9 @@ fun UpdateIndicator(
|
|||||||
enablePopup = true
|
enablePopup = true
|
||||||
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
|
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
|
||||||
|
|
||||||
CircularProgressIndicator(progress.toFloat())
|
CircularProgressIndicator(
|
||||||
|
progress = { progress.toFloat() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
@@ -274,7 +261,7 @@ fun SettingsScreen(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = context.getString(R.string.back_icon_alt)
|
contentDescription = context.getString(R.string.back_icon_alt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -315,19 +302,10 @@ fun SettingsScreen(
|
|||||||
description = context.getString(R.string.preference_load_automatically_description),
|
description = context.getString(R.string.preference_load_automatically_description),
|
||||||
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
|
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
|
||||||
)
|
)
|
||||||
OptionsButton(
|
|
||||||
title = context.getString(R.string.preferences_copy_external_button),
|
|
||||||
description = LaunchUtils.getBaseDirectory(context).path,
|
|
||||||
onClick = { onOpenFolder(context) }
|
|
||||||
)
|
|
||||||
OptionsButton(
|
OptionsButton(
|
||||||
title = stringResource(R.string.preferences_open_file_manager),
|
title = stringResource(R.string.preferences_open_file_manager),
|
||||||
onClick = { onOpenFileManager(context) }
|
onClick = { onOpenFileManager(context) }
|
||||||
)
|
)
|
||||||
OptionsButton(
|
|
||||||
title = stringResource(R.string.preferences_view_logs),
|
|
||||||
onClick = { onOpenLogs(context) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(context.getString(R.string.preference_category_updater)) {
|
OptionsGroup(context.getString(R.string.preference_category_updater)) {
|
||||||
@@ -365,6 +343,27 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||||
|
SettingsStringCard(
|
||||||
|
title = stringResource(R.string.preference_launch_arguments_name),
|
||||||
|
dialogTitle = stringResource(R.string.preference_launch_arguments_set_title),
|
||||||
|
preferenceKey = PreferenceUtils.Key.LAUNCH_ARGUMENTS,
|
||||||
|
filterInput = { it.filter { c ->
|
||||||
|
// if only there was a better way to define this!
|
||||||
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(){}<>[]?:;'\"~`-_+=\\| ".contains(c)
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
OptionsButton(
|
||||||
|
title = context.getString(R.string.preferences_copy_external_button),
|
||||||
|
description = LaunchUtils.getBaseDirectory(context).path,
|
||||||
|
onClick = { onOpenFolder(context) }
|
||||||
|
)
|
||||||
|
OptionsButton(
|
||||||
|
title = stringResource(R.string.preferences_view_logs),
|
||||||
|
onClick = { onOpenLogs(context) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME),
|
stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME),
|
||||||
style = Typography.labelSmall,
|
style = Typography.labelSmall,
|
||||||
@@ -375,240 +374,6 @@ fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
|
||||||
val preferences = PreferenceUtils.get(context)
|
|
||||||
|
|
||||||
return preferences.toggleBoolean(preferenceKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
|
||||||
val preferences = PreferenceUtils.get(context)
|
|
||||||
|
|
||||||
return preferences.getBoolean(preferenceKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style = Typography.titleMedium,
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
)
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsSelectCard(
|
|
||||||
title: String,
|
|
||||||
dialogTitle: String,
|
|
||||||
maxVal: Int,
|
|
||||||
preferenceKey: PreferenceUtils.Key,
|
|
||||||
toLabel: @Composable (Int) -> String,
|
|
||||||
extraSelectBehavior: ((Int) -> Unit)? = null
|
|
||||||
) {
|
|
||||||
val preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
|
|
||||||
|
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
OptionsCard(
|
|
||||||
title = { OptionsTitle(title = title) },
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
onClick = {
|
|
||||||
showDialog = true
|
|
||||||
},
|
|
||||||
role = Role.Button
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(toLabel(preferenceValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDialog) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
SelectDialog(
|
|
||||||
title = dialogTitle,
|
|
||||||
onDismissRequest = {
|
|
||||||
showDialog = false
|
|
||||||
},
|
|
||||||
onSelect = { selected ->
|
|
||||||
showDialog = false
|
|
||||||
PreferenceUtils.get(context)
|
|
||||||
.setInt(preferenceKey, selected)
|
|
||||||
extraSelectBehavior?.invoke(selected)
|
|
||||||
},
|
|
||||||
initialValue = preferenceValue,
|
|
||||||
toLabel = toLabel,
|
|
||||||
optionsCount = maxVal
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SelectDialog(
|
|
||||||
title: String,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onSelect: (Int) -> Unit,
|
|
||||||
initialValue: Int,
|
|
||||||
toLabel: @Composable (Int) -> String,
|
|
||||||
optionsCount: Int,
|
|
||||||
) {
|
|
||||||
var selectedValue by remember { mutableIntStateOf(initialValue) }
|
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismissRequest) {
|
|
||||||
Card(
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
|
||||||
// styling a dialog is actually a little hard if you're doing what i'm doing
|
|
||||||
// maybe there's a better way to make these padding values...
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style = Typography.titleLarge,
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
start = 28.dp,
|
|
||||||
top = 24.dp,
|
|
||||||
bottom = 12.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// do not give the row or column padding!! it messes up the selection effect
|
|
||||||
(0..optionsCount).forEach { id ->
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(
|
|
||||||
onClick = { selectedValue = id },
|
|
||||||
role = Role.RadioButton
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp)
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = selectedValue == id,
|
|
||||||
onClick = { selectedValue = id }
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
toLabel(id),
|
|
||||||
style = Typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
bottom = 16.dp,
|
|
||||||
end = 16.dp,
|
|
||||||
top = 4.dp
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(stringResource(R.string.message_box_cancel))
|
|
||||||
}
|
|
||||||
|
|
||||||
TextButton(onClick = { onSelect(selectedValue) }) {
|
|
||||||
Text(stringResource(R.string.message_box_accept))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun OptionsButton(title: String, description: String? = null, onClick: () -> Unit) {
|
|
||||||
OptionsCard(
|
|
||||||
title = {
|
|
||||||
OptionsTitle(
|
|
||||||
title = title,
|
|
||||||
description = description
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = onClick, role = Role.Button)
|
|
||||||
) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsCard(title: String, description: String? = null, preferenceKey: PreferenceUtils.Key) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val settingEnabled = remember {
|
|
||||||
mutableStateOf(getSetting(context, preferenceKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
OptionsCard(
|
|
||||||
title = {
|
|
||||||
OptionsTitle(
|
|
||||||
Modifier.fillMaxWidth(0.75f),
|
|
||||||
title = title,
|
|
||||||
description = description
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.toggleable(
|
|
||||||
value = settingEnabled.value,
|
|
||||||
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
|
|
||||||
role = Role.Switch,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Switch(checked = settingEnabled.value, onCheckedChange = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null) {
|
|
||||||
Column(
|
|
||||||
modifier,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
Text(title)
|
|
||||||
if (!description.isNullOrEmpty()) {
|
|
||||||
Text(
|
|
||||||
description,
|
|
||||||
style = Typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
|
||||||
Row(
|
|
||||||
modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(64.dp)
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
Arrangement.SpaceBetween,
|
|
||||||
Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
title()
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun OptionsCardPreview() {
|
|
||||||
GeodeLauncherTheme {
|
|
||||||
OptionsGroup(title = "Preview Group") {
|
|
||||||
SettingsCard(
|
|
||||||
title = "Load files from /test",
|
|
||||||
description = "Very long testing description goes here. It is incredibly long, it should wrap onto a new line.",
|
|
||||||
preferenceKey = PreferenceUtils.Key.LOAD_TESTING
|
|
||||||
)
|
|
||||||
SettingsCard(
|
|
||||||
title = "Testing option 2",
|
|
||||||
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showSystemUi = true)
|
@Preview(showSystemUi = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultPreview() {
|
fun DefaultPreview() {
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
package com.geode.launcher
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.selection.toggleable
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Clear
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
|
import com.geode.launcher.ui.theme.Typography
|
||||||
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
|
||||||
|
|
||||||
|
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
||||||
|
val preferences = PreferenceUtils.get(context)
|
||||||
|
|
||||||
|
return preferences.toggleBoolean(preferenceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
||||||
|
val preferences = PreferenceUtils.get(context)
|
||||||
|
|
||||||
|
return preferences.getBoolean(preferenceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = Typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsSelectCard(
|
||||||
|
title: String,
|
||||||
|
dialogTitle: String,
|
||||||
|
maxVal: Int,
|
||||||
|
preferenceKey: PreferenceUtils.Key,
|
||||||
|
toLabel: @Composable (Int) -> String,
|
||||||
|
extraSelectBehavior: ((Int) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
|
||||||
|
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
OptionsCard(
|
||||||
|
title = { OptionsTitle(title = title) },
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
onClick = {
|
||||||
|
showDialog = true
|
||||||
|
},
|
||||||
|
role = Role.Button
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(toLabel(preferenceValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
SelectDialog(
|
||||||
|
title = dialogTitle,
|
||||||
|
onDismissRequest = {
|
||||||
|
showDialog = false
|
||||||
|
},
|
||||||
|
onSelect = { selected ->
|
||||||
|
showDialog = false
|
||||||
|
PreferenceUtils.get(context)
|
||||||
|
.setInt(preferenceKey, selected)
|
||||||
|
extraSelectBehavior?.invoke(selected)
|
||||||
|
},
|
||||||
|
initialValue = preferenceValue,
|
||||||
|
toLabel = toLabel,
|
||||||
|
optionsCount = 0..maxVal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsStringCard(
|
||||||
|
title: String,
|
||||||
|
dialogTitle: String,
|
||||||
|
preferenceKey: PreferenceUtils.Key,
|
||||||
|
filterInput: ((String) -> String)? = null
|
||||||
|
) {
|
||||||
|
var preferenceValue by PreferenceUtils.useStringPreference(preferenceKey)
|
||||||
|
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
OptionsCard(
|
||||||
|
title = {
|
||||||
|
OptionsTitle(title = title, description = preferenceValue)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
onClick = {
|
||||||
|
showDialog = true
|
||||||
|
},
|
||||||
|
role = Role.Button
|
||||||
|
)
|
||||||
|
) { }
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
StringDialog(
|
||||||
|
title = dialogTitle,
|
||||||
|
onDismissRequest = { showDialog = false },
|
||||||
|
onSelect = {
|
||||||
|
preferenceValue = it
|
||||||
|
showDialog = false
|
||||||
|
},
|
||||||
|
initialValue = preferenceValue ?: "",
|
||||||
|
filterInput = filterInput
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StringDialog(
|
||||||
|
title: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSelect: (String) -> Unit,
|
||||||
|
initialValue: String,
|
||||||
|
filterInput: ((String) -> String)? = null
|
||||||
|
) {
|
||||||
|
var enteredValue by remember { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { onDismissRequest() },
|
||||||
|
title = {
|
||||||
|
Text(title)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = enteredValue,
|
||||||
|
onValueChange = {
|
||||||
|
enteredValue = filterInput?.invoke(it) ?: it
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
autoCorrect = false
|
||||||
|
),
|
||||||
|
trailingIcon = {
|
||||||
|
if (enteredValue.isNotEmpty()) {
|
||||||
|
IconButton(onClick = { enteredValue = "" }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Clear,
|
||||||
|
contentDescription = stringResource(R.string.preference_text_clear)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { onSelect(enteredValue) }) {
|
||||||
|
Text(stringResource(R.string.message_box_accept))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(stringResource(R.string.message_box_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> SelectDialog(
|
||||||
|
title: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSelect: (T) -> Unit,
|
||||||
|
initialValue: T,
|
||||||
|
toLabel: @Composable (T) -> String,
|
||||||
|
optionsCount: Iterable<T>,
|
||||||
|
) {
|
||||||
|
var selectedValue by remember { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismissRequest) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
// styling a dialog is actually a little hard if you're doing what i'm doing
|
||||||
|
// maybe there's a better way to make these padding values...
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = Typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 28.dp,
|
||||||
|
top = 24.dp,
|
||||||
|
bottom = 12.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// do not give the row or column padding!! it messes up the selection effect
|
||||||
|
optionsCount.forEach { id ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(
|
||||||
|
onClick = { selectedValue = id },
|
||||||
|
role = Role.RadioButton
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = selectedValue == id,
|
||||||
|
onClick = { selectedValue = id }
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
toLabel(id),
|
||||||
|
style = Typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
bottom = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
top = 4.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(stringResource(R.string.message_box_cancel))
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(onClick = { onSelect(selectedValue) }) {
|
||||||
|
Text(stringResource(R.string.message_box_accept))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OptionsButton(title: String, description: String? = null, onClick: () -> Unit) {
|
||||||
|
OptionsCard(
|
||||||
|
title = {
|
||||||
|
OptionsTitle(
|
||||||
|
title = title,
|
||||||
|
description = description
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onClick, role = Role.Button)
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsCard(title: String, description: String? = null, preferenceKey: PreferenceUtils.Key) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val settingEnabled = remember {
|
||||||
|
mutableStateOf(getSetting(context, preferenceKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionsCard(
|
||||||
|
title = {
|
||||||
|
OptionsTitle(
|
||||||
|
Modifier.fillMaxWidth(0.75f),
|
||||||
|
title = title,
|
||||||
|
description = description
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.toggleable(
|
||||||
|
value = settingEnabled.value,
|
||||||
|
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
|
||||||
|
role = Role.Switch,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Switch(checked = settingEnabled.value, onCheckedChange = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null) {
|
||||||
|
Column(
|
||||||
|
modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(title)
|
||||||
|
if (!description.isNullOrEmpty()) {
|
||||||
|
Text(
|
||||||
|
description,
|
||||||
|
style = Typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(64.dp)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
Arrangement.SpaceBetween,
|
||||||
|
Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
title()
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun OptionsCardPreview() {
|
||||||
|
GeodeLauncherTheme {
|
||||||
|
OptionsGroup(title = "Preview Group") {
|
||||||
|
SettingsCard(
|
||||||
|
title = "Load files from /test",
|
||||||
|
description = "Very long testing description goes here. It is incredibly long, it should wrap onto a new line.",
|
||||||
|
preferenceKey = PreferenceUtils.Key.LOAD_TESTING
|
||||||
|
)
|
||||||
|
SettingsCard(
|
||||||
|
title = "Testing option 2",
|
||||||
|
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@ package com.geode.launcher
|
|||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.database.MatrixCursor
|
import android.database.MatrixCursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.DocumentsProvider
|
import android.provider.DocumentsProvider
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -150,7 +152,7 @@ class UserDirectoryProvider : DocumentsProvider() {
|
|||||||
notifyFileChange(file)
|
notifyFileChange(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun renameDocument(documentId: String, displayName: String): String? {
|
override fun renameDocument(documentId: String, displayName: String): String {
|
||||||
val file = getFileForDocumentId(documentId)
|
val file = getFileForDocumentId(documentId)
|
||||||
val dest = findFileNameForNewFile(File(file.parentFile, displayName))
|
val dest = findFileNameForNewFile(File(file.parentFile, displayName))
|
||||||
file.renameTo(dest)
|
file.renameTo(dest)
|
||||||
@@ -203,4 +205,19 @@ class UserDirectoryProvider : DocumentsProvider() {
|
|||||||
val uri = getDocumentUri(parentDocumentId)
|
val uri = getDocumentUri(parentDocumentId)
|
||||||
context!!.contentResolver.notifyChange(uri, null)
|
context!!.contentResolver.notifyChange(uri, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
override fun findDocumentPath(
|
||||||
|
parentDocumentId: String?,
|
||||||
|
childDocumentId: String
|
||||||
|
): DocumentsContract.Path {
|
||||||
|
if (!parentDocumentId.isNullOrEmpty()) {
|
||||||
|
// not implementing this for now...
|
||||||
|
return super.findDocumentPath(parentDocumentId, childDocumentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val nonRootPath = listOf(ROOT) + childDocumentId.removePrefix(ROOT).split("/")
|
||||||
|
|
||||||
|
return DocumentsContract.Path(ROOT, nonRootPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -29,9 +29,50 @@ data class ProcessInformation(val processId: Int, val threadId: Int, val process
|
|||||||
enum class LogPriority {
|
enum class LogPriority {
|
||||||
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
|
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
|
||||||
|
|
||||||
|
class LogPriorityIterator(
|
||||||
|
start: LogPriority,
|
||||||
|
private val end: LogPriority,
|
||||||
|
private val step: Int
|
||||||
|
) : Iterator<LogPriority> {
|
||||||
|
private var hasNext: Boolean = if (step > 0) start <= end else start >= end
|
||||||
|
private var next = if (hasNext) start else end
|
||||||
|
|
||||||
|
override fun hasNext() = hasNext
|
||||||
|
|
||||||
|
override fun next(): LogPriority {
|
||||||
|
val current = next
|
||||||
|
if (current == end) {
|
||||||
|
if (!hasNext) {
|
||||||
|
throw NoSuchElementException()
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext = false
|
||||||
|
} else {
|
||||||
|
next = LogPriority.fromInt(next.toInt() + step)
|
||||||
|
}
|
||||||
|
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogPriorityProgression(
|
||||||
|
override val start: LogPriority,
|
||||||
|
override val endInclusive: LogPriority,
|
||||||
|
private val step: Int = 1
|
||||||
|
) : Iterable<LogPriority>, ClosedRange<LogPriority> {
|
||||||
|
override fun iterator(): Iterator<LogPriority> {
|
||||||
|
return LogPriorityIterator(start, endInclusive, step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
infix fun downTo(to: LogPriority) = LogPriorityProgression(this, to, -1)
|
||||||
|
|
||||||
|
operator fun rangeTo(to: LogPriority) = LogPriorityProgression(this, to)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromByte(byte: Byte): LogPriority {
|
fun fromByte(byte: Byte): LogPriority = fromInt(byte.toInt())
|
||||||
return when (byte.toInt()) {
|
|
||||||
|
fun fromInt(int: Int) = when (int) {
|
||||||
0x1 -> DEFAULT
|
0x1 -> DEFAULT
|
||||||
0x2 -> VERBOSE
|
0x2 -> VERBOSE
|
||||||
0x3 -> DEBUG
|
0x3 -> DEBUG
|
||||||
@@ -41,10 +82,21 @@ enum class LogPriority {
|
|||||||
0x7 -> FATAL
|
0x7 -> FATAL
|
||||||
0x8 -> SILENT
|
0x8 -> SILENT
|
||||||
else -> UNKNOWN
|
else -> UNKNOWN
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toInt() = when (this) {
|
||||||
|
DEFAULT -> 0x1
|
||||||
|
VERBOSE -> 0x2
|
||||||
|
DEBUG -> 0x3
|
||||||
|
INFO -> 0x4
|
||||||
|
WARN -> 0x5
|
||||||
|
ERROR -> 0x6
|
||||||
|
FATAL -> 0x7
|
||||||
|
SILENT -> 0x8
|
||||||
|
else -> 0x0
|
||||||
|
}
|
||||||
|
|
||||||
fun toChar(): Char {
|
fun toChar(): Char {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
VERBOSE -> 'V'
|
VERBOSE -> 'V'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import kotlinx.coroutines.yield
|
|||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
import java.util.LinkedList
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel to manage log output from logcat.
|
* ViewModel to manage log output from logcat.
|
||||||
@@ -30,11 +30,22 @@ class LogViewModel: ViewModel() {
|
|||||||
private val _isLoading = MutableStateFlow(false)
|
private val _isLoading = MutableStateFlow(false)
|
||||||
val isLoading = _isLoading.asStateFlow()
|
val isLoading = _isLoading.asStateFlow()
|
||||||
|
|
||||||
|
private var failedToLoad = false
|
||||||
|
|
||||||
private var logJob: Job? = null
|
private var logJob: Job? = null
|
||||||
|
|
||||||
var filterCrashes = false
|
var filterCrashes = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// skip debug/verbose lines by default
|
||||||
|
var logLevel = LogPriority.INFO
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
|
||||||
|
_lineState.clear()
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleCrashBuffer() {
|
fun toggleCrashBuffer() {
|
||||||
filterCrashes = !filterCrashes
|
filterCrashes = !filterCrashes
|
||||||
|
|
||||||
@@ -43,14 +54,24 @@ class LogViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun logOutput(): List<LogLine> {
|
private suspend fun logOutput(): List<LogLine> {
|
||||||
val logLines = LinkedList<LogLine>()
|
val logLines = ArrayList<LogLine>()
|
||||||
|
|
||||||
|
// -B = binary format, -d = dump logs
|
||||||
val logCommand = if (filterCrashes) "logcat -b crash -B -d"
|
val logCommand = if (filterCrashes) "logcat -b crash -B -d"
|
||||||
else "logcat -B -d"
|
else "logcat -B -d"
|
||||||
|
|
||||||
// -B = binary format, -d = dump logs
|
val logProcess = try {
|
||||||
val logSource = Runtime.getRuntime().exec(logCommand)
|
Runtime.getRuntime().exec(logCommand)
|
||||||
.inputStream.source().buffer()
|
} catch (ioe: IOException) {
|
||||||
|
ioe.printStackTrace()
|
||||||
|
logLines += LogLine.showException(ioe)
|
||||||
|
|
||||||
|
failedToLoad = true
|
||||||
|
|
||||||
|
return logLines
|
||||||
|
}
|
||||||
|
|
||||||
|
val logSource = logProcess.inputStream.source().buffer()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
@@ -58,8 +79,7 @@ class LogViewModel: ViewModel() {
|
|||||||
while (true) {
|
while (true) {
|
||||||
val line = LogLine.fromBufferedSource(logSource)
|
val line = LogLine.fromBufferedSource(logSource)
|
||||||
|
|
||||||
// skip debug/verbose lines
|
if (line.priority >= logLevel) {
|
||||||
if (line.priority >= LogPriority.INFO) {
|
|
||||||
logLines += line
|
logLines += line
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +91,9 @@ class LogViewModel: ViewModel() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
logLines += LogLine.showException(e)
|
logLines += LogLine.showException(e)
|
||||||
|
|
||||||
|
// technically it maybe didn't completely fail...
|
||||||
|
failedToLoad = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return logLines
|
return logLines
|
||||||
@@ -88,10 +111,26 @@ class LogViewModel: ViewModel() {
|
|||||||
_lineState.clear()
|
_lineState.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLogData(): String {
|
private fun dumpLogcatText(): String {
|
||||||
return _lineState.joinToString("\n") { it.asSimpleString }
|
val logCommand = if (filterCrashes) "logcat -b crash -d"
|
||||||
|
else "logcat -d"
|
||||||
|
|
||||||
|
val logProcess = try {
|
||||||
|
Runtime.getRuntime().exec(logCommand)
|
||||||
|
} catch (ioe: IOException) {
|
||||||
|
ioe.printStackTrace()
|
||||||
|
return ioe.stackTraceToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return logProcess.inputStream.bufferedReader().readText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLogData(): String = if (failedToLoad) {
|
||||||
|
dumpLogcatText()
|
||||||
|
} else {
|
||||||
|
_lineState.joinToString("\n") { it.asSimpleString }
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadLogs() {
|
private fun loadLogs() {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ class ReleaseManager private constructor(
|
|||||||
val geodeFile = getGeodeOutputPath()
|
val geodeFile = getGeodeOutputPath()
|
||||||
|
|
||||||
// check if an update is needed
|
// check if an update is needed
|
||||||
if (latestVersion <= currentVersion && geodeFile.exists()) {
|
if (latestVersion == currentVersion && geodeFile.exists()) {
|
||||||
_uiState.value = ReleaseManagerState.Finished()
|
_uiState.value = ReleaseManagerState.Finished()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
package com.geode.launcher.utils
|
||||||
|
|
||||||
import android.app.Activity
|
import android.Manifest
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.database.Cursor
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.MediaStore
|
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
|
||||||
import android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.geode.launcher.BuildConfig
|
||||||
|
import com.geode.launcher.R
|
||||||
|
import com.geode.launcher.UserDirectoryProvider
|
||||||
|
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
|
||||||
|
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
|
||||||
|
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.net.URI
|
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused", "KotlinJniMissingFunction")
|
||||||
object GeodeUtils {
|
object GeodeUtils {
|
||||||
private lateinit var activity: WeakReference<AppCompatActivity>
|
private lateinit var activity: WeakReference<AppCompatActivity>
|
||||||
private lateinit var fileResultLauncher: ActivityResultLauncher<Intent>
|
private lateinit var openFileResultLauncher: ActivityResultLauncher<GeodeOpenFileActivityResult.OpenFileParams>
|
||||||
private lateinit var filesResultLauncher: ActivityResultLauncher<Intent>
|
private lateinit var openDirectoryResultLauncher: ActivityResultLauncher<Uri?>
|
||||||
|
private lateinit var openFilesResultLauncher: ActivityResultLauncher<GeodeOpenFilesActivityResult.OpenFileParams>
|
||||||
|
private lateinit var saveFileResultLauncher: ActivityResultLauncher<GeodeSaveFileActivityResult.SaveFileParams>
|
||||||
|
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
|
||||||
|
private lateinit var internalRequestPermissionsLauncher: ActivityResultLauncher<Array<String>>
|
||||||
|
private lateinit var internalRequestAllFilesLauncher: ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
|
private var afterRequestPermissions: (() -> Unit)? = null
|
||||||
|
private var afterRequestPermissionsFailure: (() -> Unit)? = null
|
||||||
|
|
||||||
fun setContext(activity: AppCompatActivity) {
|
fun setContext(activity: AppCompatActivity) {
|
||||||
this.activity = WeakReference(activity)
|
this.activity = WeakReference(activity)
|
||||||
fileResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
openFileResultLauncher = activity.registerForActivityResult(GeodeOpenFileActivityResult()) { uri ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (uri != null) {
|
||||||
val data = result.data
|
val path = FileUtils.getRealPathFromURI(activity, uri)
|
||||||
if (data != null) {
|
if (path != null) {
|
||||||
val uri = data.data
|
selectFileCallback(path)
|
||||||
if (uri != null) {
|
return@registerForActivityResult
|
||||||
val path = getRealPathFromURI(activity, uri)
|
|
||||||
if (path != null) {
|
|
||||||
selectFileCallback(path)
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
failedCallback()
|
failedCallback()
|
||||||
}
|
}
|
||||||
filesResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
openDirectoryResultLauncher = activity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (it != null) {
|
||||||
val data = result.data
|
val path = FileUtils.getRealPathFromURI(activity, it)
|
||||||
if (data != null) {
|
if (path != null)
|
||||||
val clipData = data.clipData
|
selectFileCallback(path)
|
||||||
if (clipData != null) {
|
return@registerForActivityResult
|
||||||
val paths = Array(clipData.itemCount) { "" }
|
}
|
||||||
for (i in 0 until clipData.itemCount) {
|
failedCallback()
|
||||||
val path = getRealPathFromURI(activity, clipData.getItemAt(i).uri)
|
}
|
||||||
if (path != null) {
|
openFilesResultLauncher = activity.registerForActivityResult(GeodeOpenFilesActivityResult()) { result ->
|
||||||
paths[i] = path
|
if (result.isEmpty()) {
|
||||||
}
|
failedCallback()
|
||||||
}
|
return@registerForActivityResult
|
||||||
selectFilesCallback(paths)
|
}
|
||||||
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()
|
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
|
@JvmStatic
|
||||||
fun getLogcatCrashBuffer(): String {
|
fun getLogcatCrashBuffer(): String {
|
||||||
try {
|
return try {
|
||||||
val logcatProcess = Runtime.getRuntime().exec("logcat -v brief -b crash -d")
|
val logcatProcess = Runtime.getRuntime().exec("logcat -v brief -b crash -d")
|
||||||
|
|
||||||
val logcatOutput = logcatProcess.inputStream.bufferedReader().readText()
|
logcatProcess.inputStream.bufferedReader().readText()
|
||||||
|
} catch (e: Exception) {
|
||||||
return logcatOutput
|
|
||||||
}
|
|
||||||
catch (e: Exception) {
|
|
||||||
Log.e("Geode", "Failed to get logcat crash buffer", e)
|
Log.e("Geode", "Failed to get logcat crash buffer", e)
|
||||||
return ""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,115 +158,185 @@ object GeodeUtils {
|
|||||||
activity.get()?.run {
|
activity.get()?.run {
|
||||||
packageManager.getLaunchIntentForPackage(packageName)?.also {
|
packageManager.getLaunchIntentForPackage(packageName)?.also {
|
||||||
val mainIntent = Intent.makeRestartActivityTask(it.component)
|
val mainIntent = Intent.makeRestartActivityTask(it.component)
|
||||||
mainIntent.putExtra("restarted", true);
|
mainIntent.putExtra("restarted", true)
|
||||||
startActivity(mainIntent)
|
startActivity(mainIntent)
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO As of now this is unused
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun openFolder(path: String): Boolean {
|
fun openFolder(path: String): Boolean {
|
||||||
activity.get()?.run {
|
val context = activity.get()!!
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
DocumentFile.fromFile(File(path)).also {
|
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
|
|
||||||
}
|
|
||||||
intent.setType("*/*")
|
|
||||||
|
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
val pathFile = File(path)
|
||||||
startActivity(intent)
|
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||||
return true
|
val isInternalPath = pathFile.startsWith(baseDirectory)
|
||||||
|
|
||||||
|
val intent = if (isInternalPath) {
|
||||||
|
// TODO: figure out how to get this to point to the path it should be pointing at
|
||||||
|
// (the best i got was pointing at a file)
|
||||||
|
// val relativePath = pathFile.relativeTo(baseDirectory)
|
||||||
|
|
||||||
|
Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
data = DocumentsContract.buildRootUri(
|
||||||
|
"${context.packageName}.user", UserDirectoryProvider.ROOT
|
||||||
|
)
|
||||||
|
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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
|
@JvmStatic
|
||||||
fun selectFile(path: String): Boolean {
|
fun selectFile(path: String): Boolean {
|
||||||
activity.get()?.run {
|
var uri: Uri?
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
DocumentFile.fromFile(File(path)).also {
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
uri = it.uri
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
}
|
||||||
DocumentFile.fromFile(File(path)).also {
|
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
|
return try {
|
||||||
}
|
checkForFilePermissions(
|
||||||
intent.setType("*/*")
|
onSuccess = {
|
||||||
|
openFileResultLauncher.launch(GeodeOpenFileActivityResult.OpenFileParams(arrayOf("*/*"), uri))
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
},
|
||||||
print("Geode Selectfile")
|
onFailure = { failedCallback() }
|
||||||
fileResultLauncher.launch(intent)
|
)
|
||||||
print("Geode launched ")
|
|
||||||
return true
|
true
|
||||||
}
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun selectFiles(path: String): Boolean {
|
fun selectFiles(path: String): Boolean {
|
||||||
activity.get()?.run {
|
var uri: Uri?
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
DocumentFile.fromFile(File(path)).also {
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
uri = it.uri
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
}
|
||||||
DocumentFile.fromFile(File(path)).also {
|
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
|
return try {
|
||||||
}
|
checkForFilePermissions(
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
onSuccess = {
|
||||||
intent.setType("*/*")
|
openFilesResultLauncher.launch(GeodeOpenFilesActivityResult.OpenFileParams(arrayOf("*/*"), uri))
|
||||||
|
},
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
onFailure = { failedCallback() }
|
||||||
filesResultLauncher.launch(intent)
|
)
|
||||||
return true
|
|
||||||
}
|
true
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun selectFolder(path: String): Boolean {
|
fun selectFolder(path: String): Boolean {
|
||||||
activity.get()?.run {
|
var uri: Uri?
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
DocumentFile.fromFile(File(path)).also {
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
uri = it.uri
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
return try {
|
||||||
|
checkForFilePermissions(
|
||||||
|
onSuccess = {
|
||||||
|
openDirectoryResultLauncher.launch(uri)
|
||||||
|
},
|
||||||
|
onFailure = { failedCallback() }
|
||||||
|
)
|
||||||
|
|
||||||
|
true
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun createFile(path: String): Boolean {
|
fun createFile(path: String): Boolean {
|
||||||
activity.get()?.run {
|
var uri: Uri?
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
DocumentFile.fromFile(File(path)).also {
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
uri = it.uri
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
}
|
||||||
DocumentFile.fromFile(File(path)).also {
|
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
|
return try {
|
||||||
}
|
checkForFilePermissions(
|
||||||
intent.setType("*/*")
|
onSuccess = {
|
||||||
|
saveFileResultLauncher.launch(GeodeSaveFileActivityResult.SaveFileParams(null, uri))
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
},
|
||||||
fileResultLauncher.launch(intent)
|
onFailure = { failedCallback() }
|
||||||
return true
|
)
|
||||||
}
|
|
||||||
|
true
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@@ -250,155 +367,109 @@ object GeodeUtils {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
|
|
||||||
// i am actually very lazy to move this to a separate class
|
|
||||||
|
|
||||||
fun getRealPathFromURI(context: Context, uri: Uri): String? {
|
|
||||||
when {
|
|
||||||
// DocumentProvider
|
|
||||||
DocumentsContract.isDocumentUri(context, uri) -> {
|
|
||||||
when {
|
|
||||||
// ExternalStorageProvider
|
|
||||||
isExternalStorageDocument(uri) -> {
|
|
||||||
val docId = DocumentsContract.getDocumentId(uri)
|
|
||||||
val split = docId.split(":").toTypedArray()
|
|
||||||
val type = split[0]
|
|
||||||
// This is for checking Main Memory
|
|
||||||
return if ("primary".equals(type, ignoreCase = true)) {
|
|
||||||
if (split.size > 1) {
|
|
||||||
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
|
|
||||||
} else {
|
|
||||||
Environment.getExternalStorageDirectory().toString() + "/"
|
|
||||||
}
|
|
||||||
// This is for checking SD Card
|
|
||||||
} else {
|
|
||||||
"storage" + "/" + docId.replace(":", "/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isDownloadsDocument(uri) -> {
|
|
||||||
val fileName = getFilePath(context, uri)
|
|
||||||
if (fileName != null) {
|
|
||||||
return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
|
|
||||||
}
|
|
||||||
var id = DocumentsContract.getDocumentId(uri)
|
|
||||||
if (id.startsWith("raw:")) {
|
|
||||||
id = id.replaceFirst("raw:".toRegex(), "")
|
|
||||||
val file = File(id)
|
|
||||||
if (file.exists()) return id
|
|
||||||
}
|
|
||||||
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
|
|
||||||
return getDataColumn(context, contentUri, null, null)
|
|
||||||
}
|
|
||||||
isMediaDocument(uri) -> {
|
|
||||||
val docId = DocumentsContract.getDocumentId(uri)
|
|
||||||
val split = docId.split(":").toTypedArray()
|
|
||||||
val type = split[0]
|
|
||||||
var contentUri: Uri? = null
|
|
||||||
when (type) {
|
|
||||||
"image" -> {
|
|
||||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
|
||||||
}
|
|
||||||
"video" -> {
|
|
||||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
|
||||||
}
|
|
||||||
"audio" -> {
|
|
||||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val selection = "_id=?"
|
|
||||||
val selectionArgs = arrayOf(split[1])
|
|
||||||
return getDataColumn(context, contentUri, selection, selectionArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add the geode provider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"content".equals(uri.scheme, ignoreCase = true) -> {
|
|
||||||
// Return the remote address
|
|
||||||
if (isGooglePhotosUri(uri)) return uri.lastPathSegment
|
|
||||||
return getDataColumn(context, uri, null, null)
|
|
||||||
}
|
|
||||||
"file".equals(uri.scheme, ignoreCase = true) -> {
|
|
||||||
return uri.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDataColumn(context: Context, uri: Uri?, selection: String?,
|
|
||||||
selectionArgs: Array<String>?): String? {
|
|
||||||
var cursor: Cursor? = null
|
|
||||||
val column = "_data"
|
|
||||||
val projection = arrayOf(
|
|
||||||
column
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (uri == null) return null
|
|
||||||
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs,
|
|
||||||
null)
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
val index = cursor.getColumnIndexOrThrow(column)
|
|
||||||
return cursor.getString(index)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor?.close()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun getFilePath(context: Context, uri: Uri?): String? {
|
|
||||||
var cursor: Cursor? = null
|
|
||||||
val projection = arrayOf(
|
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (uri == null) return null
|
|
||||||
cursor = context.contentResolver.query(uri, projection, null, null,
|
|
||||||
null)
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
|
|
||||||
return cursor.getString(index)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor?.close()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uri The Uri to check.
|
|
||||||
* @return Whether the Uri authority is ExternalStorageProvider.
|
|
||||||
*/
|
|
||||||
fun isExternalStorageDocument(uri: Uri): Boolean {
|
|
||||||
return "com.android.externalstorage.documents" == uri.authority
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uri The Uri to check.
|
|
||||||
* @return Whether the Uri authority is DownloadsProvider.
|
|
||||||
*/
|
|
||||||
fun isDownloadsDocument(uri: Uri): Boolean {
|
|
||||||
return "com.android.providers.downloads.documents" == uri.authority
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uri The Uri to check.
|
|
||||||
* @return Whether the Uri authority is MediaProvider.
|
|
||||||
*/
|
|
||||||
fun isMediaDocument(uri: Uri): Boolean {
|
|
||||||
return "com.android.providers.media.documents" == uri.authority
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uri The Uri to check.
|
|
||||||
* @return Whether the Uri authority is Google Photos.
|
|
||||||
*/
|
|
||||||
fun isGooglePhotosUri(uri: Uri): Boolean {
|
|
||||||
return "com.google.android.apps.photos.content" == uri.authority
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isGeodeUri(uri: Uri): Boolean {
|
fun isGeodeUri(uri: Uri): Boolean {
|
||||||
return "com.geode.launcher.user" == uri.authority
|
return "com.geode.launcher.user" == uri.authority
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal"
|
||||||
|
private const val MANAGE_ALL_FILES = "${INTERNAL_PERMISSION_PREFIX}.MANAGE_ALL_FILES"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getPermissionStatus(permission: String): Boolean {
|
||||||
|
val context = activity.get() ?: return false
|
||||||
|
|
||||||
|
return when (permission) {
|
||||||
|
MANAGE_ALL_FILES -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.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)
|
||||||
}
|
}
|
||||||
@@ -89,8 +89,9 @@ object LaunchUtils {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val applicationArchitecture: String = Build.CPU_ABI
|
val applicationArchitecture: String = Build.CPU_ABI
|
||||||
|
|
||||||
val platformName: String = if (applicationArchitecture == "arm64-v8a")
|
val is64bit = applicationArchitecture == "arm64-v8a"
|
||||||
"android64" else "android32"
|
|
||||||
|
val platformName: String = if (is64bit) "android64" else "android32"
|
||||||
|
|
||||||
val geodeFilename: String = "Geode.$platformName.so"
|
val geodeFilename: String = "Geode.$platformName.so"
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,9 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
BLACK_BACKGROUND,
|
BLACK_BACKGROUND,
|
||||||
CURRENT_RELEASE_MODIFIED,
|
CURRENT_RELEASE_MODIFIED,
|
||||||
LAST_DISMISSED_UPDATE,
|
LAST_DISMISSED_UPDATE,
|
||||||
DISMISSED_GJ_UPDATE
|
DISMISSED_GJ_UPDATE,
|
||||||
|
LAUNCH_ARGUMENTS,
|
||||||
|
IGNORE_LOAD_FAILURE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||||
@@ -157,6 +159,8 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash"
|
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash"
|
||||||
Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
|
Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
|
||||||
Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate"
|
Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate"
|
||||||
|
Key.LAUNCH_ARGUMENTS -> "PreferenceLaunchArguments"
|
||||||
|
Key.IGNORE_LOAD_FAILURE -> "PreferenceIgnoreLoadFailure"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.view.KeyEvent
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import com.customRobTop.BaseRobTopActivity
|
import com.customRobTop.BaseRobTopActivity
|
||||||
|
import com.geode.launcher.utils.GeodeUtils
|
||||||
|
|
||||||
private const val HANDLER_OPEN_IME_KEYBOARD = 2
|
private const val HANDLER_OPEN_IME_KEYBOARD = 2
|
||||||
private const val HANDLER_CLOSE_IME_KEYBOARD = 3
|
private const val HANDLER_CLOSE_IME_KEYBOARD = 3
|
||||||
@@ -37,6 +38,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var cocos2dxRenderer: Cocos2dxRenderer
|
private lateinit var cocos2dxRenderer: Cocos2dxRenderer
|
||||||
|
var useKeyboardEvents = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
cocos2dxRenderer.sendResizeEvents = value
|
||||||
|
}
|
||||||
|
|
||||||
var cocos2dxEditText: Cocos2dxEditText? = null
|
var cocos2dxEditText: Cocos2dxEditText? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -207,7 +213,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
|
private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
|
||||||
return when (keyCode) {
|
return when (keyCode) {
|
||||||
KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_MENU -> {
|
KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_MENU -> {
|
||||||
if (keyEvent.repeatCount != 0 || BaseRobTopActivity.blockBackButton) {
|
if (keyEvent.repeatCount != 0 || BaseRobTopActivity.blockBackButton) {
|
||||||
@@ -220,6 +226,71 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
|
if (!useKeyboardEvents) {
|
||||||
|
return legacyKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return when (keyCode) {
|
||||||
|
// ignore system keys
|
||||||
|
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> {
|
||||||
|
super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (BaseRobTopActivity.blockBackButton) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
queueEvent {
|
||||||
|
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
|
if (!useKeyboardEvents) {
|
||||||
|
return super.onKeyUp(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> {
|
||||||
|
super.onKeyUp(keyCode, event)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (event.repeatCount != 0 || BaseRobTopActivity.blockBackButton) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
queueEvent {
|
||||||
|
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
|
||||||
|
if (!useKeyboardEvents) {
|
||||||
|
return super.onGenericMotionEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event?.action == MotionEvent.ACTION_SCROLL) {
|
||||||
|
val scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL)
|
||||||
|
val scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
|
||||||
|
|
||||||
|
queueEvent {
|
||||||
|
GeodeUtils.nativeActionScroll(scrollX, scrollY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onGenericMotionEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
fun setCocos2dxRenderer(renderer: Cocos2dxRenderer) {
|
fun setCocos2dxRenderer(renderer: Cocos2dxRenderer) {
|
||||||
this.cocos2dxRenderer = renderer
|
this.cocos2dxRenderer = renderer
|
||||||
setRenderer(this.cocos2dxRenderer)
|
setRenderer(this.cocos2dxRenderer)
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class Cocos2dxRenderer : GLSurfaceView.Renderer {
|
|||||||
private var lastTickInNanoSeconds: Long = 0
|
private var lastTickInNanoSeconds: Long = 0
|
||||||
private var screenWidth = 0
|
private var screenWidth = 0
|
||||||
private var screenHeight = 0
|
private var screenHeight = 0
|
||||||
|
var sendResizeEvents = false
|
||||||
|
|
||||||
fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) {
|
fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) {
|
||||||
screenWidth = surfaceWidth
|
screenWidth = surfaceWidth
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M480,760Q546,760 593,713Q640,666 640,600L640,440Q640,374 593,327Q546,280 480,280Q414,280 367,327Q320,374 320,440L320,600Q320,666 367,713Q414,760 480,760ZM400,640L560,640L560,560L400,560L400,640ZM400,480L560,480L560,400L400,400L400,480ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520ZM480,840Q415,840 359.5,808Q304,776 272,720L160,720L160,640L244,640Q241,620 240.5,600Q240,580 240,560L160,560L160,480L240,480Q240,460 240.5,440Q241,420 244,400L160,400L160,320L272,320Q286,297 303.5,277Q321,257 344,242L280,176L336,120L422,206Q450,197 479,197Q508,197 536,206L624,120L680,176L614,242Q637,257 655.5,276.5Q674,296 688,320L800,320L800,400L716,400Q719,420 719.5,440Q720,460 720,480L800,480L800,560L720,560Q720,580 719.5,600Q719,620 716,640L800,640L800,720L688,720Q656,776 600.5,808Q545,840 480,840Z"/>
|
||||||
|
</vector>
|
||||||
@@ -22,11 +22,16 @@
|
|||||||
<string name="launcher_warning_icon_alt">warning</string>
|
<string name="launcher_warning_icon_alt">warning</string>
|
||||||
<string name="launcher_error_icon_alt">error</string>
|
<string name="launcher_error_icon_alt">error</string>
|
||||||
<string name="launcher_unsupported_version_title">Unsupported version</string>
|
<string name="launcher_unsupported_version_title">Unsupported version</string>
|
||||||
|
<string name="launcher_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">
|
<string name="launcher_unsupported_version_description">
|
||||||
The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s). A launcher update is required to fix this.\nLaunch will continue, but crashes may occur!
|
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>
|
||||||
<string name="launcher_update_available">A new launcher update is available!</string>
|
<string name="launcher_update_available">A new launcher update is available!</string>
|
||||||
<string name="launcher_update_dismiss">Dismiss</string>
|
<string name="launcher_update_dismiss">Dismiss</string>
|
||||||
|
<string name="safe_mode_enable_title">Enable safe mode?</string>
|
||||||
|
<string name="safe_mode_enable_description">Safe mode disables loading of all mods, allowing one to remove problematic mods or diagnose crashes. If you\'re not experiencing issues with Geode, you should probably not be using safe mode.</string>
|
||||||
|
|
||||||
<!-- Updater strings -->
|
<!-- Updater strings -->
|
||||||
<string name="release_fetch_failed">Update failed.\n%1$s</string>
|
<string name="release_fetch_failed">Update failed.\n%1$s</string>
|
||||||
@@ -43,12 +48,15 @@
|
|||||||
|
|
||||||
<!-- GeometryDashActivity strings -->
|
<!-- GeometryDashActivity strings -->
|
||||||
<string name="load_failed_link_error">Failed to link libraries</string>
|
<string name="load_failed_link_error">Failed to link libraries</string>
|
||||||
<string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture of the launcher and Geometry Dash do not match, or from a system bug.\nCheck application logs for more details.</string>
|
<string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture or version of the launcher and Geometry Dash do not match, or from a system bug.\nCheck application logs for more details.</string>
|
||||||
<string name="load_failed_abi_error_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the \"android32\" version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
|
<string name="load_failed_abi_error_need_64bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 64-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
|
||||||
|
<string name="load_failed_abi_error_need_32bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 32-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
|
||||||
<string name="load_failed_generic_error">Load failed</string>
|
<string name="load_failed_generic_error">Load failed</string>
|
||||||
<string name="load_failed_generic_error_description">Uncaught error during game load: `%1$s`.\nCheck application logs for more details. Please report this issue!</string>
|
<string name="load_failed_generic_error_description">Uncaught error during game load: `%1$s`.\nCheck application logs for more details.</string>
|
||||||
<string name="message_box_cancel">Cancel</string>
|
<string name="message_box_cancel">Cancel</string>
|
||||||
<string name="message_box_accept">OK</string>
|
<string name="message_box_accept">OK</string>
|
||||||
|
<string name="message_box_continue">Continue</string>
|
||||||
|
<string name="missing_permissions">Missing required permissions for this action!</string>
|
||||||
|
|
||||||
<!-- SettingsActivity strings -->
|
<!-- SettingsActivity strings -->
|
||||||
<string name="title_activity_settings">Settings</string>
|
<string name="title_activity_settings">Settings</string>
|
||||||
@@ -86,6 +94,12 @@
|
|||||||
<string name="preference_check_for_updates_no_internet">No internet found.</string>
|
<string name="preference_check_for_updates_no_internet">No internet found.</string>
|
||||||
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
|
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
|
||||||
|
|
||||||
|
<string name="preference_category_developer">Developer</string>
|
||||||
|
<string name="preference_launch_arguments_name">Launch arguments</string>
|
||||||
|
<string name="preference_launch_arguments_set_title">Set launch arguments</string>
|
||||||
|
<string name="preference_ignore_load_failure_name">Ignore loader errors</string>
|
||||||
|
<string name="preference_text_clear">Clear</string>
|
||||||
|
|
||||||
<!-- ApplicationLogsActivity strings -->
|
<!-- ApplicationLogsActivity strings -->
|
||||||
<string name="title_activity_application_logs">Application Logs</string>
|
<string name="title_activity_application_logs">Application Logs</string>
|
||||||
<string name="application_logs_no_logs">No logs available.</string>
|
<string name="application_logs_no_logs">No logs available.</string>
|
||||||
@@ -94,6 +108,8 @@
|
|||||||
<string name="application_log_copy_label">Log Message</string>
|
<string name="application_log_copy_label">Log Message</string>
|
||||||
<string name="application_logs_more">More</string>
|
<string name="application_logs_more">More</string>
|
||||||
<string name="application_logs_export">Save To</string>
|
<string name="application_logs_export">Save To</string>
|
||||||
|
<string name="application_logs_level">Set Log Level</string>
|
||||||
|
<string name="application_logs_level_dialog_name">Set minimum log level</string>
|
||||||
<string name="application_logs_export_completed">Log export complete.</string>
|
<string name="application_logs_export_completed">Log export complete.</string>
|
||||||
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
|
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
|
||||||
<string name="application_logs_crash_only">Show Crash Buffer</string>
|
<string name="application_logs_crash_only">Show Crash Buffer</string>
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -21,3 +21,5 @@ kotlin.code.style=official
|
|||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
# nice optimization
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
@@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@@ -202,11 +202,11 @@ fi
|
|||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Collect all arguments for the java command:
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# and any embedded shellness will be escaped.
|
||||||
# double quotes to make sure that they get re-expanded; and
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ pluginManagement {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
@@ -12,5 +14,6 @@ dependencyResolutionManagement {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "launcher"
|
rootProject.name = "launcher"
|
||||||
include ':app'
|
include(":app")
|
||||||
Reference in new issue
Block a user