mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-24 00:59:21 +00:00
Compare commits
85
Commits
1.0.0-beta.1
...
input
| 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 | ||
|
|
498bbf4528 | ||
|
|
dc72608622 | ||
|
|
a50e0a0830 | ||
|
|
db707dfce1 | ||
|
|
929cc7615e | ||
|
|
bb2e3fbb1b | ||
|
|
7d548aef6c | ||
|
|
7f64236e89 | ||
|
|
199b6f142b | ||
|
|
af87320f11 | ||
|
|
665756d85b | ||
|
|
590006413c | ||
|
|
1b079fa3ff | ||
|
|
cb16db3bf9 | ||
|
|
de6fad41f6 | ||
|
|
d1a43230a9 | ||
|
|
442ef83339 | ||
|
|
1eab544692 | ||
|
|
af0ebeab84 | ||
|
|
0844dc97d2 | ||
|
|
42c10c2729 | ||
|
|
cf568bcbe2 | ||
|
|
36152d8773 | ||
|
|
3f371be097 | ||
|
|
5eba097e38 | ||
|
|
7e868d8d0a | ||
|
|
97b851cbc1 | ||
|
|
397cb9b9bd | ||
|
|
05eb3af243 | ||
|
|
e7a3b72fad | ||
|
|
3e83bc8039 | ||
|
|
c0b5df3f2f | ||
|
|
c13136de52 | ||
|
|
ac8db3a98c | ||
|
|
7554a4888a |
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,23 +36,34 @@ 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}}" \
|
||||||
-Pandroid.injected.signing.key.password="${{secrets.KEY_PASSWORD}}"
|
-Pandroid.injected.signing.key.password="${{secrets.KEY_PASSWORD}}"
|
||||||
|
|
||||||
|
- name: Rename universal apk
|
||||||
|
id: rename_apk_universal
|
||||||
|
run: |
|
||||||
|
SHORT_SHA=${GITHUB_SHA::7}
|
||||||
|
APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-universal-${{ matrix.config.lower }}.apk
|
||||||
|
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.apk
|
||||||
|
mv $APK_PATH $OUT_PATH
|
||||||
|
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Rename apk
|
- name: Rename apk
|
||||||
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-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-$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: ${{ steps.rename_apk.outputs.path }}
|
path: |
|
||||||
|
${{ steps.rename_apk_universal.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,84 +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 4
|
|
||||||
versionName "2.0.0-beta.1"
|
|
||||||
|
|
||||||
vectorDrawables {
|
|
||||||
useSupportLibrary true
|
|
||||||
}
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
arguments "-DUSE_TULIPHOOK:BOOL=OFF"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ndkConfig.abiFilters "arm64-v8a", "armeabi-v7a"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
// enables a polyfill for java Instant on api levels < 26 (used for updater)
|
|
||||||
coreLibraryDesugaringEnabled true
|
|
||||||
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
buildFeatures {
|
|
||||||
compose true
|
|
||||||
}
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion compose_compiler
|
|
||||||
}
|
|
||||||
packagingOptions {
|
|
||||||
resources {
|
|
||||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path file('src/main/cpp/CMakeLists.txt')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
namespace 'com.geode.launcher'
|
|
||||||
ndkVersion '26.1.10909125'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
|
||||||
implementation "androidx.compose.ui:ui:$compose_version"
|
|
||||||
implementation 'androidx.compose.material3:material3:1.1.2'
|
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
|
||||||
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:$compose_version"
|
|
||||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
|
|
||||||
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,9 +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.READ_LOGS" />
|
<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"
|
||||||
@@ -28,6 +34,11 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.GeodeLauncher"
|
android:theme="@style/Theme.GeodeLauncher"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".ApplicationLogsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_application_logs"
|
||||||
|
android:theme="@style/Theme.GeodeLauncher" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
@@ -58,11 +69,12 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:authorities="com.geode.launcher.user"
|
|
||||||
android:name=".UserDirectoryProvider"
|
android:name=".UserDirectoryProvider"
|
||||||
android:grantUriPermissions="true"
|
android:authorities="com.geode.launcher.user"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
@@ -78,4 +90,5 @@
|
|||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
package com.geode.launcher
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.OnBackPressedDispatcher
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.geode.launcher.log.LogLine
|
||||||
|
import com.geode.launcher.log.LogPriority
|
||||||
|
import com.geode.launcher.log.LogViewModel
|
||||||
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
|
import com.geode.launcher.ui.theme.Theme
|
||||||
|
import com.geode.launcher.ui.theme.Typography
|
||||||
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ApplicationLogsActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||||
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
|
) {
|
||||||
|
ApplicationLogsScreen(onBackPressedDispatcher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getLogColor(priority: LogPriority): Color {
|
||||||
|
val isDark = LocalTheme.current != Theme.LIGHT
|
||||||
|
|
||||||
|
// i just copied these from my ide tbh
|
||||||
|
return if (isDark) {
|
||||||
|
when (priority) {
|
||||||
|
LogPriority.ERROR -> Color(0xff_df_51_51)
|
||||||
|
LogPriority.WARN -> Color(0xff_bc_b5_00)
|
||||||
|
LogPriority.FATAL -> Color(0xff_b8_b6_fc)
|
||||||
|
LogPriority.DEBUG -> Color(0xff_aa_aa_aa)
|
||||||
|
LogPriority.VERBOSE -> Color(0xff_77_77_77)
|
||||||
|
else -> Color.Unspecified
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (priority) {
|
||||||
|
LogPriority.ERROR -> Color(0xff_df_51_51)
|
||||||
|
LogPriority.WARN -> Color(0xff_ea_b7_00)
|
||||||
|
LogPriority.FATAL -> Color(0xff_a1_42_f4)
|
||||||
|
LogPriority.DEBUG -> Color(0xff_77_77_77)
|
||||||
|
LogPriority.VERBOSE -> Color(0xff_aa_aa_aa)
|
||||||
|
else -> Color.Unspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyLogText(context: Context, logMessage: String) {
|
||||||
|
val clipboardManager =
|
||||||
|
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
clipboardManager.setPrimaryClip(
|
||||||
|
ClipData.newPlainText(
|
||||||
|
context.getString(R.string.application_log_copy_label),
|
||||||
|
logMessage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.text_copied),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectLogLevelDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSelect: (LogPriority) -> Unit,
|
||||||
|
initialValue: LogPriority
|
||||||
|
) {
|
||||||
|
SelectDialog(
|
||||||
|
title = stringResource(R.string.application_logs_level_dialog_name),
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onSelect = { onSelect(it) },
|
||||||
|
initialValue = initialValue,
|
||||||
|
toLabel = {
|
||||||
|
// i'm lazy
|
||||||
|
it.name.lowercase().replaceFirstChar { c -> c.uppercaseChar() }
|
||||||
|
},
|
||||||
|
optionsCount = LogPriority.FATAL downTo LogPriority.VERBOSE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onLongPress = {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
copyLogText(context, logLine.asSimpleString)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
|
||||||
|
style = Typography.labelSmall,
|
||||||
|
color = getLogColor(logLine.priority),
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"[${logLine.tag}]: ${logLine.messageTrimmed}",
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
style = Typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ApplicationLogsScreen(
|
||||||
|
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||||
|
logViewModel: LogViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val logLines = logViewModel.lineState
|
||||||
|
val isLoading by logViewModel.isLoading.collectAsState()
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
var showMoreOptions by remember { mutableStateOf(false) }
|
||||||
|
val saveLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val logs = logViewModel.getLogData()
|
||||||
|
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write(logs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(context, R.string.application_logs_export_completed, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(logLines.size) {
|
||||||
|
listState.scrollToItem(logLines.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
var showLogLevelSelect by remember { mutableStateOf(false) }
|
||||||
|
if (showLogLevelSelect) {
|
||||||
|
SelectLogLevelDialog(
|
||||||
|
onDismissRequest = { showLogLevelSelect = false },
|
||||||
|
onSelect = {
|
||||||
|
logViewModel.logLevel = it
|
||||||
|
showLogLevelSelect = false
|
||||||
|
},
|
||||||
|
initialValue = logViewModel.logLevel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
Column {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back_icon_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val data = logViewModel.getLogData()
|
||||||
|
|
||||||
|
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
putExtra(Intent.EXTRA_TEXT, data)
|
||||||
|
type = "text/plain"
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||||
|
|
||||||
|
context.startActivity(shareIntent)
|
||||||
|
}
|
||||||
|
}, enabled = !logLines.isEmpty()) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Share,
|
||||||
|
contentDescription = stringResource(R.string.application_logs_share)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.MoreVert,
|
||||||
|
contentDescription = stringResource(R.string.application_logs_more)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showMoreOptions,
|
||||||
|
onDismissRequest = { showMoreOptions = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_bug_report),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
if (logViewModel.filterCrashes) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Check,
|
||||||
|
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.application_logs_crash_only))
|
||||||
|
}, onClick = {
|
||||||
|
logViewModel.toggleCrashBuffer()
|
||||||
|
showMoreOptions = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_filter_list),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.application_logs_level))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
showLogLevelSelect = true
|
||||||
|
showMoreOptions = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_save),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.application_logs_export))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
|
||||||
|
showMoreOptions = false
|
||||||
|
},
|
||||||
|
enabled = !logLines.isEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_delete),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.application_logs_delete))
|
||||||
|
}, onClick = {
|
||||||
|
logViewModel.clearLogs()
|
||||||
|
showMoreOptions = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.title_activity_application_logs),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
LazyColumn(
|
||||||
|
Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
state = listState
|
||||||
|
) {
|
||||||
|
if (!isLoading && logLines.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.application_logs_no_logs),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = Typography.bodyLarge,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(
|
||||||
|
logLines
|
||||||
|
) { line ->
|
||||||
|
LogCard(line)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
fun LogsPreview() {
|
||||||
|
GeodeLauncherTheme {
|
||||||
|
ApplicationLogsScreen(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,8 +22,8 @@ import com.customRobTop.BaseRobTopActivity
|
|||||||
import com.customRobTop.JniToCpp
|
import com.customRobTop.JniToCpp
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
import com.geode.launcher.utils.DownloadUtils
|
import com.geode.launcher.utils.DownloadUtils
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
|
||||||
import com.geode.launcher.utils.GeodeUtils
|
import com.geode.launcher.utils.GeodeUtils
|
||||||
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import org.cocos2dx.lib.Cocos2dxEditText
|
import org.cocos2dx.lib.Cocos2dxEditText
|
||||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
|
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
|
||||||
@@ -35,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
|
||||||
@@ -44,10 +44,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
private var mReceiver: BroadcastReceiver? = null
|
private var mReceiver: BroadcastReceiver? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setupUIState()
|
setupUIState()
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
// return back to main if Geometry Dash isn't found
|
// return back to main if Geometry Dash isn't found
|
||||||
if (!LaunchUtils.isGeometryDashInstalled(packageManager)) {
|
if (!LaunchUtils.isGeometryDashInstalled(packageManager)) {
|
||||||
returnToMain()
|
returnToMain()
|
||||||
@@ -57,26 +57,27 @@ 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 abi = LaunchUtils.applicationArchitecture
|
val is64bit = LaunchUtils.is64bit
|
||||||
val isExtracted = gdPackageInfo.applicationInfo.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS
|
val errorMessage = when {
|
||||||
val isSplit = (gdPackageInfo.applicationInfo.splitSourceDirs?.size ?: 0) > 1
|
abiMismatch && is64bit -> getString(R.string.load_failed_abi_error_need_32bit_description)
|
||||||
|
abiMismatch -> getString(R.string.load_failed_abi_error_need_64bit_description)
|
||||||
val metadata = "Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi"
|
else -> getString(R.string.load_failed_link_error_description)
|
||||||
Log.i("GeodeLauncher", metadata)
|
}
|
||||||
|
|
||||||
returnToMain(
|
returnToMain(
|
||||||
getString(R.string.load_failed_link_error),
|
getString(R.string.load_failed_link_error),
|
||||||
getString(R.string.load_failed_link_error_description)
|
errorMessage
|
||||||
)
|
)
|
||||||
|
|
||||||
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),
|
||||||
@@ -105,17 +106,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
|
|
||||||
setupRedirection(gdPackageInfo)
|
setupRedirection(gdPackageInfo)
|
||||||
|
|
||||||
// request read and write permissions
|
|
||||||
requestPermissions(
|
|
||||||
arrayOf(
|
|
||||||
android.Manifest.permission.READ_EXTERNAL_STORAGE,
|
|
||||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
Cocos2dxHelper.init(this, this)
|
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)
|
||||||
@@ -128,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,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 {
|
||||||
@@ -149,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,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()) {
|
||||||
@@ -237,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)
|
||||||
} catch (e: UnsatisfiedLinkError) {
|
return
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createView(): FrameLayout {
|
private fun createView(): FrameLayout {
|
||||||
@@ -287,13 +295,24 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
|
|
||||||
private fun hideSystemUi() {
|
private fun hideSystemUi() {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
hide(WindowInsetsCompat.Type.systemBars())
|
||||||
systemBarsBehavior =
|
systemBarsBehavior =
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
hide(WindowInsetsCompat.Type.systemBars())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasWindowFocus)
|
||||||
|
mHasWindowFocus = hasWindowFocus
|
||||||
|
if (hasWindowFocus && !mIsOnPause) {
|
||||||
|
resumeGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSystemUi()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
unregisterReceivers()
|
unregisterReceivers()
|
||||||
@@ -321,6 +340,15 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRestart() {
|
||||||
|
super.onRestart()
|
||||||
|
mIsOnPause = false
|
||||||
|
BaseRobTopActivity.isPaused = false
|
||||||
|
if (!this.mIsRunning) {
|
||||||
|
resumeGame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
mIsOnPause = true
|
mIsOnPause = true
|
||||||
@@ -349,14 +377,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
||||||
super.onWindowFocusChanged(hasWindowFocus)
|
|
||||||
mHasWindowFocus = hasWindowFocus
|
|
||||||
if (hasWindowFocus && !mIsOnPause) {
|
|
||||||
resumeGame()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun runOnGLThread(runnable: Runnable) {
|
override fun runOnGLThread(runnable: Runnable) {
|
||||||
mGLSurfaceView?.queueEvent(runnable)
|
mGLSurfaceView?.queueEvent(runnable)
|
||||||
}
|
}
|
||||||
@@ -430,4 +450,13 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCapabilityAdded(capability: String): Boolean {
|
||||||
|
if (capability == GeodeUtils.CAPABILITY_EXTENDED_INPUT) {
|
||||||
|
mGLSurfaceView?.useKeyboardEvents = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,12 +6,16 @@ import android.os.Bundle
|
|||||||
import android.text.format.Formatter.formatShortFileSize
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
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
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Warning
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@@ -25,7 +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.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
|
||||||
@@ -34,7 +42,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.geode.launcher.api.ReleaseViewModel
|
import com.geode.launcher.updater.ReleaseViewModel
|
||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
import com.geode.launcher.ui.theme.LocalTheme
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
import com.geode.launcher.ui.theme.Theme
|
import com.geode.launcher.ui.theme.Theme
|
||||||
@@ -42,14 +50,19 @@ import com.geode.launcher.ui.theme.Typography
|
|||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.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.utils.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
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val gdInstalled = LaunchUtils.isGeometryDashInstalled(packageManager)
|
val gdInstalled = LaunchUtils.isGeometryDashInstalled(packageManager)
|
||||||
@@ -70,7 +83,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
MainScreen(gdInstalled, geodeInstalled)
|
val loadFailed = !returnMessage.isNullOrEmpty() && !returnTitle.isNullOrEmpty()
|
||||||
|
MainScreen(gdInstalled, geodeInstalled, loadFailed)
|
||||||
|
|
||||||
if (!returnMessage.isNullOrEmpty() && !returnTitle.isNullOrEmpty()) {
|
if (!returnMessage.isNullOrEmpty() && !returnTitle.isNullOrEmpty()) {
|
||||||
LoadFailedDialog(returnTitle, returnMessage)
|
LoadFailedDialog(returnTitle, returnMessage)
|
||||||
@@ -89,12 +103,49 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String, onDismiss: () -> Unit) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
ElevatedCard(modifier) {
|
||||||
|
Column(
|
||||||
|
// buttons add enough padding already, lower to compensate
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
top = 20.dp,
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp,
|
||||||
|
bottom = 8.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.launcher_update_available),
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(modifier = Modifier.align(Alignment.End)) {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.launcher_update_dismiss))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.size(4.dp))
|
||||||
|
|
||||||
|
TextButton(onClick = { uriHandler.openUri(openTo) }) {
|
||||||
|
Text(stringResource(R.string.launcher_download))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateProgressIndicator(
|
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,
|
||||||
@@ -108,7 +159,7 @@ fun UpdateProgressIndicator(
|
|||||||
if (progress == null) {
|
if (progress == null) {
|
||||||
LinearProgressIndicator()
|
LinearProgressIndicator()
|
||||||
} else {
|
} else {
|
||||||
LinearProgressIndicator(progress)
|
LinearProgressIndicator(progress = progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -146,6 +197,11 @@ fun UpdateMessageIndicator(
|
|||||||
releaseViewModel.runReleaseCheck(true)
|
releaseViewModel.runReleaseCheck(true)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Refresh,
|
||||||
|
contentDescription = stringResource(R.string.launcher_retry_icon_alt)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
Text(stringResource(R.string.release_fetch_button_retry))
|
Text(stringResource(R.string.release_fetch_button_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,17 +234,21 @@ 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(
|
||||||
|
preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE
|
||||||
|
)
|
||||||
|
|
||||||
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE) {
|
val canDismissRelease = gdVersionCode >= Constants.SUPPORTED_VERSION_CODE
|
||||||
if (showUpdateWarning) {
|
val shouldDismiss = canDismissRelease && gdVersionCode == lastDismissedVersion
|
||||||
|
|
||||||
|
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && !shouldDismiss) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -200,34 +260,65 @@ fun UpdateWarning() {
|
|||||||
Text(stringResource(R.string.launcher_unsupported_version_title))
|
Text(stringResource(R.string.launcher_unsupported_version_title))
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
|
val message = if (gdVersionCode >= Constants.SUPPORTED_VERSION_CODE) {
|
||||||
|
R.string.launcher_unsupported_version_description
|
||||||
|
} else {
|
||||||
|
R.string.launcher_outdated_version_description
|
||||||
|
}
|
||||||
|
|
||||||
Text(stringResource(
|
Text(stringResource(
|
||||||
R.string.launcher_unsupported_version_description,
|
message, gdVersionString, Constants.SUPPORTED_VERSION_STRING
|
||||||
gdVersionString, Constants.SUPPORTED_VERSION_STRING
|
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
showUpdateWarning = false
|
onDismiss()
|
||||||
onLaunch(context)
|
lastDismissedVersion = gdVersionCode
|
||||||
|
|
||||||
|
onLaunch(context, inSafeMode)
|
||||||
}) {
|
}) {
|
||||||
Text(stringResource(R.string.message_box_accept))
|
Text(stringResource(R.string.message_box_accept))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showUpdateWarning = false }) {
|
TextButton(onClick = { onDismiss() }) {
|
||||||
Text(stringResource(R.string.message_box_cancel))
|
Text(stringResource(R.string.message_box_cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDismissRequest = { showUpdateWarning = false }
|
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
|
||||||
@@ -258,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)
|
||||||
}
|
}
|
||||||
@@ -276,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 -> {
|
||||||
@@ -312,9 +404,31 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState()
|
||||||
|
val nextUpdateValue = nextUpdate
|
||||||
|
|
||||||
|
if (!releaseViewModel.isInUpdate && nextUpdateValue != null) {
|
||||||
|
val updateUrl = nextUpdateValue.getLauncherDownload()?.browserDownloadUrl
|
||||||
|
?: nextUpdateValue.htmlUrl
|
||||||
|
|
||||||
|
LauncherUpdateIndicator(
|
||||||
|
modifier = modifier,
|
||||||
|
openTo = updateUrl,
|
||||||
|
onDismiss = {
|
||||||
|
releaseViewModel.dismissLauncherUpdate()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLaunch(context: Context) {
|
fun onLaunch(context: Context, safeMode: Boolean = false) {
|
||||||
|
if (safeMode) {
|
||||||
|
GeodeUtils.setAdditionalLaunchArguments(GeodeUtils.ARGUMENT_SAFE_MODE)
|
||||||
|
} else {
|
||||||
|
GeodeUtils.clearLaunchArguments()
|
||||||
|
}
|
||||||
|
|
||||||
val launchIntent = Intent(context, GeometryDashActivity::class.java)
|
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
|
||||||
|
|
||||||
@@ -330,11 +444,12 @@ fun onSettings(context: Context) {
|
|||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
gdInstalled: Boolean = true,
|
gdInstalled: Boolean = true,
|
||||||
geodePreinstalled: Boolean = true,
|
geodePreinstalled: Boolean = true,
|
||||||
|
disableAutomaticLaunch: Boolean = false,
|
||||||
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
||||||
) {
|
) {
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -347,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) {
|
||||||
@@ -375,15 +492,11 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (gdInstalled && geodeInstalled) {
|
if (gdInstalled && geodeInstalled) {
|
||||||
if (shouldAutomaticallyLaunch.value && !releaseViewModel.isInUpdate) {
|
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) {
|
||||||
@@ -400,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,
|
||||||
@@ -463,12 +608,30 @@ fun MainScreen(
|
|||||||
|
|
||||||
UpdateCard(
|
UpdateCard(
|
||||||
releaseViewModel,
|
releaseViewModel,
|
||||||
modifier = Modifier.padding(12.dp)
|
modifier = Modifier
|
||||||
|
.padding(12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (beginLaunch) {
|
if (beginLaunch) {
|
||||||
UpdateWarning()
|
UpdateWarning(launchInSafeMode) {
|
||||||
|
beginLaunch = false
|
||||||
|
launchInSafeMode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSafeModeDialog) {
|
||||||
|
SafeModeDialog(
|
||||||
|
onDismiss = {
|
||||||
|
showSafeModeDialog = false
|
||||||
|
},
|
||||||
|
onLaunch = {
|
||||||
|
launchInSafeMode = true
|
||||||
|
beginLaunch = true
|
||||||
|
|
||||||
|
showSafeModeDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.OnBackPressedDispatcher
|
import androidx.activity.OnBackPressedDispatcher
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
@@ -19,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
|
||||||
@@ -43,9 +41,8 @@ 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.api.ReleaseViewModel
|
import com.geode.launcher.updater.ReleaseViewModel
|
||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
import com.geode.launcher.ui.theme.LocalTheme
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
import com.geode.launcher.ui.theme.Theme
|
import com.geode.launcher.ui.theme.Theme
|
||||||
@@ -57,7 +54,9 @@ import java.net.UnknownHostException
|
|||||||
|
|
||||||
class SettingsActivity : ComponentActivity() {
|
class SettingsActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||||
val theme = Theme.fromInt(themeOption)
|
val theme = Theme.fromInt(themeOption)
|
||||||
@@ -80,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 =
|
||||||
@@ -103,16 +92,28 @@ fun onOpenFolder(context: Context) {
|
|||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.export_folder_copied),
|
context.getString(R.string.text_copied),
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onOpenFileManager(context: Context) {
|
fun onOpenFileManager(context: Context) {
|
||||||
|
// future thoughts: Intent.ACTION_VIEW may not work on some devices
|
||||||
|
// the "correct" solution is to go through a list of possible intents/applications
|
||||||
|
// don't feel like writing that behavior. this is good enough
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
data = DocumentsContract.buildRootUri(
|
||||||
type = "vnd.android.document/directory"
|
"${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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -144,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 -> {}
|
||||||
}
|
}
|
||||||
@@ -211,6 +214,11 @@ fun updateTheme(context: Context, theme: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onOpenLogs(context: Context) {
|
||||||
|
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||||
|
context.startActivity(launchIntent)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
@@ -253,8 +261,8 @@ 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.settings_back_icon_alt)
|
contentDescription = context.getString(R.string.back_icon_alt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -289,20 +297,11 @@ fun SettingsScreen(
|
|||||||
title = stringResource(R.string.preference_black_background_name),
|
title = stringResource(R.string.preference_black_background_name),
|
||||||
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
||||||
)
|
)
|
||||||
SettingsCard(
|
|
||||||
title = context.getString(R.string.preference_load_testing_name),
|
|
||||||
preferenceKey = PreferenceUtils.Key.LOAD_TESTING,
|
|
||||||
)
|
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = context.getString(R.string.preference_load_automatically_name),
|
title = context.getString(R.string.preference_load_automatically_name),
|
||||||
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) }
|
||||||
@@ -343,251 +342,36 @@ fun SettingsScreen(
|
|||||||
UpdateIndicator(snackbarHostState, updateStatus)
|
UpdateIndicator(snackbarHostState, updateStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
OptionsGroup("Data") {
|
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||||
|
SettingsStringCard(
|
||||||
|
title = stringResource(R.string.preference_launch_arguments_name),
|
||||||
|
dialogTitle = stringResource(R.string.preference_launch_arguments_set_title),
|
||||||
|
preferenceKey = PreferenceUtils.Key.LAUNCH_ARGUMENTS,
|
||||||
|
filterInput = { it.filter { c ->
|
||||||
|
// if only there was a better way to define this!
|
||||||
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(){}<>[]?:;'\"~`-_+=\\| ".contains(c)
|
||||||
|
}}
|
||||||
|
)
|
||||||
OptionsButton(
|
OptionsButton(
|
||||||
title = "Export save data",
|
title = context.getString(R.string.preferences_copy_external_button),
|
||||||
onClick = { onExportSaveData(context) }
|
description = LaunchUtils.getBaseDirectory(context).path,
|
||||||
|
onClick = { onOpenFolder(context) }
|
||||||
|
)
|
||||||
|
OptionsButton(
|
||||||
|
title = stringResource(R.string.preferences_view_logs),
|
||||||
|
onClick = { onOpenLogs(context) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
Text(
|
||||||
title,
|
stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME),
|
||||||
style = Typography.titleMedium,
|
style = Typography.labelSmall,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.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)
|
||||||
|
|||||||
@@ -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,16 +3,16 @@ 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
|
||||||
|
|
||||||
private const val ROOT = "root"
|
|
||||||
|
|
||||||
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
|
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
|
||||||
DocumentsContract.Root.COLUMN_ROOT_ID,
|
DocumentsContract.Root.COLUMN_ROOT_ID,
|
||||||
DocumentsContract.Root.COLUMN_FLAGS,
|
DocumentsContract.Root.COLUMN_FLAGS,
|
||||||
@@ -30,7 +30,12 @@ private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
|
|||||||
DocumentsContract.Document.COLUMN_SIZE
|
DocumentsContract.Document.COLUMN_SIZE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// a lot of this code is pulled from <https://github.com/dolphin-emu/dolphin>
|
||||||
class UserDirectoryProvider : DocumentsProvider() {
|
class UserDirectoryProvider : DocumentsProvider() {
|
||||||
|
companion object {
|
||||||
|
internal const val ROOT = "root"
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var rootDir: File
|
private lateinit var rootDir: File
|
||||||
|
|
||||||
override fun onCreate(): Boolean {
|
override fun onCreate(): Boolean {
|
||||||
@@ -147,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)
|
||||||
@@ -200,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package com.geode.launcher.log
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toJavaInstant
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import okio.BufferedSource
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
fun BufferedSource.readCChar(): Char {
|
||||||
|
return this.readUtf8CodePoint().toChar()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BufferedSource.readCString(): String {
|
||||||
|
val buffer = StringBuilder()
|
||||||
|
|
||||||
|
var lastByte = this.readCChar()
|
||||||
|
while (lastByte != '\u0000') {
|
||||||
|
buffer.append(lastByte)
|
||||||
|
lastByte = this.readCChar()
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
|
||||||
|
|
||||||
|
enum class LogPriority {
|
||||||
|
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
|
||||||
|
|
||||||
|
class LogPriorityIterator(
|
||||||
|
start: LogPriority,
|
||||||
|
private val end: LogPriority,
|
||||||
|
private val step: Int
|
||||||
|
) : Iterator<LogPriority> {
|
||||||
|
private var hasNext: Boolean = if (step > 0) start <= end else start >= end
|
||||||
|
private var next = if (hasNext) start else end
|
||||||
|
|
||||||
|
override fun hasNext() = hasNext
|
||||||
|
|
||||||
|
override fun next(): LogPriority {
|
||||||
|
val current = next
|
||||||
|
if (current == end) {
|
||||||
|
if (!hasNext) {
|
||||||
|
throw NoSuchElementException()
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext = false
|
||||||
|
} else {
|
||||||
|
next = LogPriority.fromInt(next.toInt() + step)
|
||||||
|
}
|
||||||
|
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogPriorityProgression(
|
||||||
|
override val start: LogPriority,
|
||||||
|
override val endInclusive: LogPriority,
|
||||||
|
private val step: Int = 1
|
||||||
|
) : Iterable<LogPriority>, ClosedRange<LogPriority> {
|
||||||
|
override fun iterator(): Iterator<LogPriority> {
|
||||||
|
return LogPriorityIterator(start, endInclusive, step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
infix fun downTo(to: LogPriority) = LogPriorityProgression(this, to, -1)
|
||||||
|
|
||||||
|
operator fun rangeTo(to: LogPriority) = LogPriorityProgression(this, to)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromByte(byte: Byte): LogPriority = fromInt(byte.toInt())
|
||||||
|
|
||||||
|
fun fromInt(int: Int) = when (int) {
|
||||||
|
0x1 -> DEFAULT
|
||||||
|
0x2 -> VERBOSE
|
||||||
|
0x3 -> DEBUG
|
||||||
|
0x4 -> INFO
|
||||||
|
0x5 -> WARN
|
||||||
|
0x6 -> ERROR
|
||||||
|
0x7 -> FATAL
|
||||||
|
0x8 -> SILENT
|
||||||
|
else -> UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toInt() = when (this) {
|
||||||
|
DEFAULT -> 0x1
|
||||||
|
VERBOSE -> 0x2
|
||||||
|
DEBUG -> 0x3
|
||||||
|
INFO -> 0x4
|
||||||
|
WARN -> 0x5
|
||||||
|
ERROR -> 0x6
|
||||||
|
FATAL -> 0x7
|
||||||
|
SILENT -> 0x8
|
||||||
|
else -> 0x0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toChar(): Char {
|
||||||
|
return when (this) {
|
||||||
|
VERBOSE -> 'V'
|
||||||
|
DEBUG -> 'D'
|
||||||
|
INFO -> 'I'
|
||||||
|
WARN -> 'W'
|
||||||
|
ERROR -> 'E'
|
||||||
|
FATAL -> 'F'
|
||||||
|
SILENT -> 'S'
|
||||||
|
else -> '?'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a log entry from logcat.
|
||||||
|
*/
|
||||||
|
data class LogLine(
|
||||||
|
val process: ProcessInformation,
|
||||||
|
val time: Instant,
|
||||||
|
val logId: Int,
|
||||||
|
val priority: LogPriority,
|
||||||
|
val tag: String,
|
||||||
|
val message: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private enum class EntryVersion {
|
||||||
|
V3, V4
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun headerSizeToVersion(size: UShort) = when (size.toInt()) {
|
||||||
|
0x18 -> EntryVersion.V3
|
||||||
|
0x1c -> EntryVersion.V4
|
||||||
|
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromBufferedSource(source: BufferedSource): LogLine {
|
||||||
|
/*
|
||||||
|
// from android <liblog/include/log/log_read.h>
|
||||||
|
// there are multiple logger entry formats
|
||||||
|
// use the header_size to determine the one you have
|
||||||
|
|
||||||
|
struct logger_entry_v3 {
|
||||||
|
uint16_t len; // length of the payload
|
||||||
|
uint16_t hdr_size; // sizeof(struct logger_entry_v3)
|
||||||
|
int32_t pid; // generating process's pid
|
||||||
|
int32_t tid; // generating process's tid
|
||||||
|
int32_t sec; // seconds since Epoch
|
||||||
|
int32_t nsec; // nanoseconds
|
||||||
|
uint32_t lid; // log id of the payload
|
||||||
|
}
|
||||||
|
|
||||||
|
struct logger_entry_v4 {
|
||||||
|
uint16_t len; // length of the payload
|
||||||
|
uint16_t hdr_size; // sizeof(struct logger_entry_v4 = 28)
|
||||||
|
int32_t pid; // generating process's pid
|
||||||
|
uint32_t tid; // generating process's tid
|
||||||
|
uint32_t sec; // seconds since Epoch
|
||||||
|
uint32_t nsec; // nanoseconds
|
||||||
|
uint32_t lid; // log id of the payload, bottom 4 bits currently
|
||||||
|
uint32_t uid; // generating process's uid
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* val payloadLength = */ source.readShortLe()
|
||||||
|
val headerSize = source.readShortLe().toUShort()
|
||||||
|
|
||||||
|
val entryVersion = headerSizeToVersion(headerSize)
|
||||||
|
|
||||||
|
val pid = source.readIntLe()
|
||||||
|
val tid = source.readIntLe()
|
||||||
|
val sec = source.readIntLe().toLong()
|
||||||
|
val nSec = source.readIntLe()
|
||||||
|
val lid = source.readIntLe()
|
||||||
|
val uid = if (entryVersion == EntryVersion.V4)
|
||||||
|
source.readIntLe() else 0
|
||||||
|
|
||||||
|
val processInformation = ProcessInformation(pid, tid, uid)
|
||||||
|
val time = Instant.fromEpochSeconds(sec, nSec)
|
||||||
|
|
||||||
|
val priorityByte = source.readByte()
|
||||||
|
val priority = LogPriority.fromByte(priorityByte)
|
||||||
|
|
||||||
|
val tag = source.readCString()
|
||||||
|
val message = source.readCString()
|
||||||
|
|
||||||
|
return LogLine(
|
||||||
|
process = processInformation,
|
||||||
|
priority = priority,
|
||||||
|
time = time,
|
||||||
|
logId = lid,
|
||||||
|
tag = tag,
|
||||||
|
message = message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showException(exception: Exception) = LogLine(
|
||||||
|
process = ProcessInformation(0, 0, 0),
|
||||||
|
time = Clock.System.now(),
|
||||||
|
logId = 0,
|
||||||
|
priority = LogPriority.FATAL,
|
||||||
|
tag = "GeodeLauncher",
|
||||||
|
message = "Failed to parse log entry with ${exception.stackTraceToString()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val identifier = time.toJavaInstant()
|
||||||
|
|
||||||
|
val messageTrimmed by lazy { this.message.trim() }
|
||||||
|
|
||||||
|
val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
|
||||||
|
val asSimpleString by lazy {
|
||||||
|
"$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.geode.launcher.log
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
import java.io.EOFException
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel to manage log output from logcat.
|
||||||
|
* Currently, it loads the list of logs at init with no ability to read additional logs.
|
||||||
|
*/
|
||||||
|
class LogViewModel: ViewModel() {
|
||||||
|
// the original version of this code did support streaming btw
|
||||||
|
// the states were way too annoying to manage across multiple threads
|
||||||
|
// yay!
|
||||||
|
|
||||||
|
private val _lineState = mutableStateListOf<LogLine>()
|
||||||
|
val lineState = _lineState
|
||||||
|
|
||||||
|
private val _isLoading = MutableStateFlow(false)
|
||||||
|
val isLoading = _isLoading.asStateFlow()
|
||||||
|
|
||||||
|
private var failedToLoad = false
|
||||||
|
|
||||||
|
private var logJob: Job? = null
|
||||||
|
|
||||||
|
var filterCrashes = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
// skip debug/verbose lines by default
|
||||||
|
var logLevel = LogPriority.INFO
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
|
||||||
|
_lineState.clear()
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleCrashBuffer() {
|
||||||
|
filterCrashes = !filterCrashes
|
||||||
|
|
||||||
|
_lineState.clear()
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun logOutput(): List<LogLine> {
|
||||||
|
val logLines = ArrayList<LogLine>()
|
||||||
|
|
||||||
|
// -B = binary format, -d = dump logs
|
||||||
|
val logCommand = if (filterCrashes) "logcat -b crash -B -d"
|
||||||
|
else "logcat -B -d"
|
||||||
|
|
||||||
|
val logProcess = try {
|
||||||
|
Runtime.getRuntime().exec(logCommand)
|
||||||
|
} catch (ioe: IOException) {
|
||||||
|
ioe.printStackTrace()
|
||||||
|
logLines += LogLine.showException(ioe)
|
||||||
|
|
||||||
|
failedToLoad = true
|
||||||
|
|
||||||
|
return logLines
|
||||||
|
}
|
||||||
|
|
||||||
|
val logSource = logProcess.inputStream.source().buffer()
|
||||||
|
|
||||||
|
try {
|
||||||
|
coroutineScope {
|
||||||
|
// this runs until the stream is exhausted
|
||||||
|
while (true) {
|
||||||
|
val line = LogLine.fromBufferedSource(logSource)
|
||||||
|
|
||||||
|
if (line.priority >= logLevel) {
|
||||||
|
logLines += line
|
||||||
|
}
|
||||||
|
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: EOFException) {
|
||||||
|
// ignore, end of file reached
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
logLines += LogLine.showException(e)
|
||||||
|
|
||||||
|
// technically it maybe didn't completely fail...
|
||||||
|
failedToLoad = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return logLines
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLogs() {
|
||||||
|
logJob?.cancel()
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
// -c = clear
|
||||||
|
Runtime.getRuntime().exec("logcat -c")
|
||||||
|
.waitFor()
|
||||||
|
}
|
||||||
|
|
||||||
|
_lineState.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dumpLogcatText(): String {
|
||||||
|
val logCommand = if (filterCrashes) "logcat -b crash -d"
|
||||||
|
else "logcat -d"
|
||||||
|
|
||||||
|
val logProcess = try {
|
||||||
|
Runtime.getRuntime().exec(logCommand)
|
||||||
|
} catch (ioe: IOException) {
|
||||||
|
ioe.printStackTrace()
|
||||||
|
return ioe.stackTraceToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return logProcess.inputStream.bufferedReader().readText()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLogData(): String = if (failedToLoad) {
|
||||||
|
dumpLogcatText()
|
||||||
|
} else {
|
||||||
|
_lineState.joinToString("\n") { it.asSimpleString }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLogs() {
|
||||||
|
_isLoading.value = true
|
||||||
|
|
||||||
|
logJob?.cancel()
|
||||||
|
logJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val lines = logOutput()
|
||||||
|
_lineState.addAll(lines)
|
||||||
|
_isLoading.value = false
|
||||||
|
|
||||||
|
logJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,24 +79,6 @@ fun GeodeLauncherTheme(
|
|||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
val view = LocalView.current
|
|
||||||
|
|
||||||
if (!view.isInEditMode) {
|
|
||||||
SideEffect {
|
|
||||||
with ((view.context as Activity).window) {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(this, false)
|
|
||||||
|
|
||||||
statusBarColor = Color.Transparent.toArgb()
|
|
||||||
navigationBarColor = Color.Transparent.toArgb()
|
|
||||||
|
|
||||||
WindowCompat.getInsetsController(this, view).apply {
|
|
||||||
isAppearanceLightStatusBars = theme == Theme.LIGHT
|
|
||||||
isAppearanceLightNavigationBars = theme == Theme.LIGHT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography,
|
typography = Typography,
|
||||||
|
|||||||
+22
-3
@@ -1,4 +1,4 @@
|
|||||||
package com.geode.launcher.api
|
package com.geode.launcher.updater
|
||||||
|
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
@@ -23,7 +23,8 @@ class Release(
|
|||||||
val tagName: String,
|
val tagName: String,
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
val publishedAt: Instant,
|
val publishedAt: Instant,
|
||||||
val assets: List<Asset>
|
val assets: List<Asset>,
|
||||||
|
val htmlUrl: String
|
||||||
) {
|
) {
|
||||||
fun getDescription(): String {
|
fun getDescription(): String {
|
||||||
if (tagName == "nightly") {
|
if (tagName == "nightly") {
|
||||||
@@ -42,7 +43,7 @@ class Release(
|
|||||||
return createdAt.epochSeconds
|
return createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAndroidDownload(): Asset? {
|
fun getGeodeDownload(): Asset? {
|
||||||
// try to find an asset that matches the architecture first
|
// try to find an asset that matches the architecture first
|
||||||
val platform = LaunchUtils.platformName
|
val platform = LaunchUtils.platformName
|
||||||
|
|
||||||
@@ -51,4 +52,22 @@ class Release(
|
|||||||
it.name.endsWith(releaseSuffix)
|
it.name.endsWith(releaseSuffix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLauncherDownload(): Asset? {
|
||||||
|
val platform = LaunchUtils.platformName
|
||||||
|
val use32BitPlatform = platform == "android32"
|
||||||
|
|
||||||
|
return assets.find { asset ->
|
||||||
|
/* you know it's good when you pull out the truth table
|
||||||
|
* u32bp | contains | found
|
||||||
|
* 1 | 1 | 1
|
||||||
|
* 0 | 1 | 0
|
||||||
|
* 1 | 0 | 0
|
||||||
|
* 0 | 0 | 1
|
||||||
|
* surprise! it's an xnor
|
||||||
|
*/
|
||||||
|
|
||||||
|
(asset.name.contains("android32")) == use32BitPlatform
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+66
-22
@@ -1,9 +1,11 @@
|
|||||||
package com.geode.launcher.utils
|
package com.geode.launcher.updater
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.geode.launcher.api.Release
|
import com.geode.launcher.BuildConfig
|
||||||
import com.geode.launcher.api.ReleaseRepository
|
import com.geode.launcher.utils.DownloadUtils
|
||||||
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -15,6 +17,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InterruptedIOException
|
import java.io.InterruptedIOException
|
||||||
|
|
||||||
@@ -30,7 +34,7 @@ class ReleaseManager private constructor(
|
|||||||
private lateinit var managerInstance: ReleaseManager
|
private lateinit var managerInstance: ReleaseManager
|
||||||
|
|
||||||
fun get(context: Context): ReleaseManager {
|
fun get(context: Context): ReleaseManager {
|
||||||
if (!::managerInstance.isInitialized) {
|
if (!Companion::managerInstance.isInitialized) {
|
||||||
val applicationContext = context.applicationContext
|
val applicationContext = context.applicationContext
|
||||||
|
|
||||||
val httpClient = OkHttpClient.Builder()
|
val httpClient = OkHttpClient.Builder()
|
||||||
@@ -74,6 +78,9 @@ class ReleaseManager private constructor(
|
|||||||
val isInUpdate: Boolean
|
val isInUpdate: Boolean
|
||||||
get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished
|
get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished
|
||||||
|
|
||||||
|
private val _availableLauncherUpdate = MutableStateFlow<Release?>(null)
|
||||||
|
val availableLauncherUpdate = _availableLauncherUpdate.asStateFlow()
|
||||||
|
|
||||||
private fun sendError(e: Exception) {
|
private fun sendError(e: Exception) {
|
||||||
_uiState.value = ReleaseManagerState.Failure(e)
|
_uiState.value = ReleaseManagerState.Failure(e)
|
||||||
|
|
||||||
@@ -84,21 +91,24 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dismissLauncherUpdate() {
|
||||||
|
val update = availableLauncherUpdate.value ?: return
|
||||||
|
|
||||||
|
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||||
|
sharedPreferences.setString(PreferenceUtils.Key.LAST_DISMISSED_UPDATE, update.tagName)
|
||||||
|
|
||||||
|
_availableLauncherUpdate.value = null
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getLatestRelease(): Release? {
|
private suspend fun getLatestRelease(): Release? {
|
||||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||||
val useNightly = sharedPreferences.getBoolean(PreferenceUtils.Key.RELEASE_CHANNEL)
|
val useNightly = sharedPreferences.getBoolean(PreferenceUtils.Key.RELEASE_CHANNEL)
|
||||||
|
|
||||||
val latestRelease = if (useNightly) {
|
return releaseRepository.getLatestGeodeRelease(useNightly)
|
||||||
releaseRepository.getLatestNightlyRelease()
|
|
||||||
} else {
|
|
||||||
releaseRepository.getLatestRelease()
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestRelease
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun performUpdate(release: Release) {
|
private suspend fun performUpdate(release: Release) {
|
||||||
val releaseAsset = release.getAndroidDownload()
|
val releaseAsset = release.getGeodeDownload()
|
||||||
if (releaseAsset == null) {
|
if (releaseAsset == null) {
|
||||||
val noAssetException = Exception("missing Android download")
|
val noAssetException = Exception("missing Android download")
|
||||||
_uiState.value = ReleaseManagerState.Failure(noAssetException)
|
_uiState.value = ReleaseManagerState.Failure(noAssetException)
|
||||||
@@ -110,7 +120,10 @@ class ReleaseManager private constructor(
|
|||||||
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size.toLong())
|
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size.toLong())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val fileStream = DownloadUtils.downloadStream(httpClient, releaseAsset.browserDownloadUrl) { progress, outOf ->
|
val fileStream = DownloadUtils.downloadStream(
|
||||||
|
httpClient,
|
||||||
|
releaseAsset.browserDownloadUrl
|
||||||
|
) { progress, outOf ->
|
||||||
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
|
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +158,30 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||||
|
val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED)
|
||||||
|
?: return false
|
||||||
|
|
||||||
val fileLastModified = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED)
|
val currentFileHash = computeFileHash(geodeFile)
|
||||||
if (fileLastModified == 0L) {
|
|
||||||
return false
|
return originalFileHash != currentFileHash
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileLastModified != geodeFile.lastModified()
|
private fun checkLauncherUpdate(launcherUpdate: Release) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
println("ignoring launcher update check - in debug mode!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||||
|
val lastDismissedVersion = sharedPreferences.getString(PreferenceUtils.Key.LAST_DISMISSED_UPDATE)
|
||||||
|
|
||||||
|
if (!lastDismissedVersion.isNullOrEmpty() && launcherUpdate.tagName == lastDismissedVersion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launcherUpdate.tagName != BuildConfig.VERSION_NAME) {
|
||||||
|
_availableLauncherUpdate.value = launcherUpdate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) {
|
private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) {
|
||||||
@@ -162,6 +192,17 @@ class ReleaseManager private constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val launcherRelease = try {
|
||||||
|
releaseRepository.getLatestLauncherRelease()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
sendError(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launcherRelease != null) {
|
||||||
|
checkLauncherUpdate(launcherRelease)
|
||||||
|
}
|
||||||
|
|
||||||
if (release == null) {
|
if (release == null) {
|
||||||
_uiState.value = ReleaseManagerState.Finished()
|
_uiState.value = ReleaseManagerState.Finished()
|
||||||
return
|
return
|
||||||
@@ -176,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
|
||||||
}
|
}
|
||||||
@@ -203,13 +244,16 @@ class ReleaseManager private constructor(
|
|||||||
release.getDescriptor()
|
release.getDescriptor()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// store hash as modified time is unreliable (don't know why)
|
||||||
val outputFile = getGeodeOutputPath()
|
val outputFile = getGeodeOutputPath()
|
||||||
sharedPreferences.setLong(
|
val fileHash = computeFileHash(outputFile)
|
||||||
PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED,
|
|
||||||
outputFile.lastModified()
|
sharedPreferences.setString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED, fileHash)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun computeFileHash(file: File): String =
|
||||||
|
file.source().buffer().readByteString().md5().hex()
|
||||||
|
|
||||||
private fun getGeodeOutputPath(): File {
|
private fun getGeodeOutputPath(): File {
|
||||||
val geodeName = LaunchUtils.geodeFilename
|
val geodeName = LaunchUtils.geodeFilename
|
||||||
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||||
+11
-7
@@ -1,4 +1,4 @@
|
|||||||
package com.geode.launcher.api
|
package com.geode.launcher.updater
|
||||||
|
|
||||||
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
|
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
@@ -12,20 +12,24 @@ import java.net.URL
|
|||||||
|
|
||||||
class ReleaseRepository(private val httpClient: OkHttpClient) {
|
class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val GITHUB_API_BASE = "https://api.github.com/repos/geode-sdk/geode"
|
private const val GITHUB_API_BASE = "https://api.github.com"
|
||||||
private const val GITHUB_API_HEADER = "X-GitHub-Api-Version"
|
private const val GITHUB_API_HEADER = "X-GitHub-Api-Version"
|
||||||
private const val GITHUB_API_VERSION = "2022-11-28"
|
private const val GITHUB_API_VERSION = "2022-11-28"
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getLatestNightlyRelease(): Release? {
|
suspend fun getLatestLauncherRelease(): Release? {
|
||||||
val nightlyPath = "$GITHUB_API_BASE/releases/tags/nightly"
|
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
|
||||||
val url = URL(nightlyPath)
|
|
||||||
|
val url = URL(releasePath)
|
||||||
|
|
||||||
return getReleaseByUrl(url)
|
return getReleaseByUrl(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getLatestRelease(): Release? {
|
suspend fun getLatestGeodeRelease(isNightly: Boolean = false): Release? {
|
||||||
val releasePath = "$GITHUB_API_BASE/releases/latest"
|
val geodeBaseUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases"
|
||||||
|
val releasePath = if (isNightly) "$geodeBaseUrl/tags/nightly"
|
||||||
|
else "$geodeBaseUrl/latest"
|
||||||
|
|
||||||
val url = URL(releasePath)
|
val url = URL(releasePath)
|
||||||
|
|
||||||
return getReleaseByUrl(url)
|
return getReleaseByUrl(url)
|
||||||
+12
-4
@@ -1,4 +1,4 @@
|
|||||||
package com.geode.launcher.api
|
package com.geode.launcher.updater
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.AP
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import com.geode.launcher.utils.ReleaseManager
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -53,6 +53,8 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
|
|||||||
val isInUpdate
|
val isInUpdate
|
||||||
get() = ReleaseManager.get(application).isInUpdate
|
get() = ReleaseManager.get(application).isInUpdate
|
||||||
|
|
||||||
|
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
|
||||||
|
|
||||||
var hasPerformedCheck = false
|
var hasPerformedCheck = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
|
|||||||
fun useGlobalCheckState() {
|
fun useGlobalCheckState() {
|
||||||
hasPerformedCheck = true
|
hasPerformedCheck = true
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val releaseFlow = ReleaseManager.get(application)
|
val releaseFlow = ReleaseManager.get(application)
|
||||||
.uiState
|
.uiState
|
||||||
|
|
||||||
@@ -89,11 +91,17 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
|
|||||||
fun runReleaseCheck(isManual: Boolean = false) {
|
fun runReleaseCheck(isManual: Boolean = false) {
|
||||||
hasPerformedCheck = true
|
hasPerformedCheck = true
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val releaseFlow = ReleaseManager.get(application)
|
val releaseFlow = ReleaseManager.get(application)
|
||||||
.checkForUpdates(isManual)
|
.checkForUpdates(isManual)
|
||||||
|
|
||||||
syncUiState(releaseFlow)
|
syncUiState(releaseFlow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dismissLauncherUpdate() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
ReleaseManager.get(application).dismissLauncherUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,8 @@ object Constants {
|
|||||||
@SuppressLint("SdCardPath")
|
@SuppressLint("SdCardPath")
|
||||||
const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}"
|
const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}"
|
||||||
|
|
||||||
const val SUPPORTED_VERSION_CODE = 37L
|
const val SUPPORTED_VERSION_CODE = 38L
|
||||||
const val SUPPORTED_VERSION_STRING = "2.2.13"
|
const val SUPPORTED_VERSION_STRING = "2.205"
|
||||||
|
|
||||||
const val LAUNCHER_KEY_RETURN_TITLE = "return_title"
|
const val LAUNCHER_KEY_RETURN_TITLE = "return_title"
|
||||||
const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message"
|
const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message"
|
||||||
|
|||||||
@@ -116,19 +116,13 @@ object DownloadUtils {
|
|||||||
fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
|
fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
|
||||||
// gotta love copying
|
// gotta love copying
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
FileUtils.copy(inputStream, outputStream)
|
inputStream.use { input -> outputStream.use { output ->
|
||||||
|
FileUtils.copy(input, output)
|
||||||
|
}}
|
||||||
} else {
|
} else {
|
||||||
inputStream.use { input ->
|
inputStream.use { input -> outputStream.use { output ->
|
||||||
outputStream.use { output ->
|
input.copyTo(output)
|
||||||
val buffer = ByteArray(4 * 1024)
|
}}
|
||||||
while (true) {
|
|
||||||
val byteCount = input.read(buffer)
|
|
||||||
if (byteCount < 0) break
|
|
||||||
output.write(buffer, 0, byteCount)
|
|
||||||
}
|
|
||||||
output.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,62 +1,77 @@
|
|||||||
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) {
|
|
||||||
val data = result.data
|
|
||||||
if (data != null) {
|
|
||||||
val uri = data.data
|
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
val path = getRealPathFromURI(activity, uri)
|
val path = FileUtils.getRealPathFromURI(activity, uri)
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
selectFileCallback(path)
|
selectFileCallback(path)
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
failedCallback()
|
||||||
}
|
}
|
||||||
|
openDirectoryResultLauncher = activity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
|
||||||
|
if (it != null) {
|
||||||
|
val path = FileUtils.getRealPathFromURI(activity, it)
|
||||||
|
if (path != null)
|
||||||
|
selectFileCallback(path)
|
||||||
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
failedCallback()
|
failedCallback()
|
||||||
}
|
}
|
||||||
filesResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
openFilesResultLauncher = activity.registerForActivityResult(GeodeOpenFilesActivityResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.isEmpty()) {
|
||||||
val data = result.data
|
failedCallback()
|
||||||
if (data != null) {
|
return@registerForActivityResult
|
||||||
val clipData = data.clipData
|
}
|
||||||
if (clipData != null) {
|
val paths: Array<String> = Array(result.size) {"n = $it"}
|
||||||
val paths = Array(clipData.itemCount) { "" }
|
for (i in result.indices) {
|
||||||
for (i in 0 until clipData.itemCount) {
|
val path = FileUtils.getRealPathFromURI(activity, result[i])
|
||||||
val path = getRealPathFromURI(activity, clipData.getItemAt(i).uri)
|
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
paths[i] = path
|
paths[i] = path
|
||||||
}
|
}
|
||||||
@@ -64,24 +79,56 @@ object GeodeUtils {
|
|||||||
selectFilesCallback(paths)
|
selectFilesCallback(paths)
|
||||||
return@registerForActivityResult
|
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,125 +158,202 @@ 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 {
|
||||||
return false
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
external fun selectFileCallback(path: String)
|
type = "*/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
external fun selectFilesCallback(paths: Array<String>)
|
return try {
|
||||||
|
context.startActivity(intent)
|
||||||
|
true
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
external fun failedCallback()
|
private external fun selectFileCallback(path: String)
|
||||||
|
|
||||||
|
private external fun selectFilesCallback(paths: Array<String>)
|
||||||
|
|
||||||
|
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)
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
DocumentFile.fromFile(File(path)).also {
|
DocumentFile.fromFile(File(path)).also {
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
|
uri = it.uri
|
||||||
}
|
}
|
||||||
intent.setType("*/*")
|
|
||||||
|
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
return try {
|
||||||
print("Geode Selectfile")
|
checkForFilePermissions(
|
||||||
fileResultLauncher.launch(intent)
|
onSuccess = {
|
||||||
print("Geode launched ")
|
openFileResultLauncher.launch(GeodeOpenFileActivityResult.OpenFileParams(arrayOf("*/*"), uri))
|
||||||
return true
|
},
|
||||||
|
onFailure = { failedCallback() }
|
||||||
|
)
|
||||||
|
|
||||||
|
true
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun selectFiles(path: String): Boolean {
|
fun selectFiles(path: String): Boolean {
|
||||||
activity.get()?.run {
|
var uri: Uri?
|
||||||
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 {
|
DocumentFile.fromFile(File(path)).also {
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
|
uri = it.uri
|
||||||
}
|
}
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
||||||
intent.setType("*/*")
|
|
||||||
|
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
return try {
|
||||||
filesResultLauncher.launch(intent)
|
checkForFilePermissions(
|
||||||
return true
|
onSuccess = {
|
||||||
|
openFilesResultLauncher.launch(GeodeOpenFilesActivityResult.OpenFileParams(arrayOf("*/*"), uri))
|
||||||
|
},
|
||||||
|
onFailure = { failedCallback() }
|
||||||
|
)
|
||||||
|
|
||||||
|
true
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun selectFolder(path: String): Boolean {
|
fun selectFolder(path: String): Boolean {
|
||||||
activity.get()?.run {
|
var uri: Uri?
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
DocumentFile.fromFile(File(path)).also {
|
DocumentFile.fromFile(File(path)).also {
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
|
uri = it.uri
|
||||||
}
|
}
|
||||||
intent.setType("*/*")
|
|
||||||
|
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
return try {
|
||||||
fileResultLauncher.launch(intent)
|
checkForFilePermissions(
|
||||||
return true
|
onSuccess = {
|
||||||
|
openDirectoryResultLauncher.launch(uri)
|
||||||
|
},
|
||||||
|
onFailure = { failedCallback() }
|
||||||
|
)
|
||||||
|
|
||||||
|
true
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return 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)
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
DocumentFile.fromFile(File(path)).also {
|
DocumentFile.fromFile(File(path)).also {
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
|
uri = it.uri
|
||||||
}
|
}
|
||||||
intent.setType("*/*")
|
|
||||||
|
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
return try {
|
||||||
fileResultLauncher.launch(intent)
|
checkForFilePermissions(
|
||||||
return true
|
onSuccess = {
|
||||||
|
saveFileResultLauncher.launch(GeodeSaveFileActivityResult.SaveFileParams(null, uri))
|
||||||
|
},
|
||||||
|
onFailure = { failedCallback() }
|
||||||
|
)
|
||||||
|
|
||||||
|
true
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getBaseDirectory(): String {
|
fun getBaseDirectory(): String {
|
||||||
val activity = activity.get()!!
|
val activity = activity.get()!!
|
||||||
return LaunchUtils.getBaseDirectory(activity).path
|
return LaunchUtils.getBaseDirectory(activity).canonicalPath
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getInternalDirectory(): String {
|
||||||
|
val activity = activity.get()!!
|
||||||
|
return activity.filesDir.canonicalPath
|
||||||
}
|
}
|
||||||
|
|
||||||
private val gameVersionMap = mapOf(
|
private val gameVersionMap = mapOf(
|
||||||
37L to "2.200"
|
37L to "2.200",
|
||||||
|
38L to "2.205"
|
||||||
)
|
)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@@ -243,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)
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,9 @@ import android.content.pm.PackageInfo
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object LaunchUtils {
|
object LaunchUtils {
|
||||||
@@ -34,13 +36,62 @@ object LaunchUtils {
|
|||||||
return game.versionName
|
return game.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean {
|
||||||
|
val abi = applicationArchitecture
|
||||||
|
val isExtracted =
|
||||||
|
packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS
|
||||||
|
val isSplit = (packageInfo.applicationInfo.splitSourceDirs?.size ?: 0) > 1
|
||||||
|
|
||||||
|
val metadata =
|
||||||
|
"Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi"
|
||||||
|
Log.i("GeodeLauncher", metadata)
|
||||||
|
|
||||||
|
// easiest check! these messages are hardcoded in bionic/linker/linker_phdr.cpp
|
||||||
|
if (loadException.message?.contains("32-bit instead of 64-bit") == true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadException.message?.contains("64-bit instead of 32-bit") == true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine if it can find gd libraries for the opposite architecture
|
||||||
|
val gdBinaryName = "lib${Constants.COCOS_LIB_NAME}.so"
|
||||||
|
val oppositeArchitecture = if (abi == "arm64-v8a") "armeabi-v7a" else "arm64-v8a"
|
||||||
|
|
||||||
|
// detect issues using native library directory (only works on extracted libraries)
|
||||||
|
val nativeLibraryDir = packageInfo.applicationInfo.nativeLibraryDir
|
||||||
|
|
||||||
|
if (nativeLibraryDir.contains(oppositeArchitecture)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// android has some odd behavior where 32bit libraries get installed to the arm directory instead of armeabi-v7a
|
||||||
|
// detect it, ig
|
||||||
|
if (oppositeArchitecture == "armeabi-v7a" && nativeLibraryDir.contains("/lib/arm/")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isExtracted) {
|
||||||
|
try {
|
||||||
|
context.assets.openNonAssetFd("lib/$oppositeArchitecture/$gdBinaryName")
|
||||||
|
return true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// this is good, actually!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// supposedly CPU_ABI returns the current arch for the running application
|
// supposedly CPU_ABI returns the current arch for the running application
|
||||||
// despite being deprecated, this is also one of the few ways to get this information
|
// despite being deprecated, this is also one of the few ways to get this information
|
||||||
@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"
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.core.content.edit
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import com.geode.launcher.BuildConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension object for SharedPreferences to add better key safety and default values.
|
* Extension object for SharedPreferences to add better key safety and default values.
|
||||||
@@ -23,36 +24,69 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun useBooleanPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> {
|
fun useBooleanPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> {
|
||||||
return usePreference(preferenceKey, lifecycleOwner) { p, k ->
|
return usePreference(
|
||||||
p.getBoolean(k)
|
preferenceKey,
|
||||||
}
|
lifecycleOwner,
|
||||||
|
preferenceGet = { p, k -> p.getBoolean(k) },
|
||||||
|
preferenceSet = { p, k, v -> p.setBoolean(k, v) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun useStringPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<String?> {
|
fun useStringPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<String?> {
|
||||||
return usePreference(preferenceKey, lifecycleOwner) { p, k ->
|
return usePreference(
|
||||||
p.getString(k)
|
preferenceKey,
|
||||||
}
|
lifecycleOwner,
|
||||||
|
preferenceGet = { p, k -> p.getString(k) },
|
||||||
|
preferenceSet = { p, k, v -> p.setString(k, v) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun useIntPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Int> {
|
fun useIntPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Int> {
|
||||||
return usePreference(preferenceKey, lifecycleOwner) { p, k ->
|
return usePreference(
|
||||||
p.getInt(k)
|
preferenceKey,
|
||||||
}
|
lifecycleOwner,
|
||||||
|
preferenceGet = { p, k -> p.getInt(k) },
|
||||||
|
preferenceSet = { p, k, v -> p.setInt(k, v) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun <T> usePreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, preferenceGet: (PreferenceUtils, Key) -> T): MutableState<T> {
|
fun useLongPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Long> {
|
||||||
|
return usePreference(
|
||||||
|
preferenceKey,
|
||||||
|
lifecycleOwner,
|
||||||
|
preferenceGet = { p, k -> p.getLong(k) },
|
||||||
|
preferenceSet = { p, k, v -> p.setLong(k, v) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun <T> usePreference(
|
||||||
|
preferenceKey: Key,
|
||||||
|
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
|
||||||
|
preferenceGet: (PreferenceUtils, Key) -> T,
|
||||||
|
preferenceSet: (PreferenceUtils, Key, T) -> Unit
|
||||||
|
): MutableState<T> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE)
|
val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
val preferences = get(sharedPreferences)
|
val preferences = get(sharedPreferences)
|
||||||
|
|
||||||
val preferenceValue = remember {
|
val preferenceValue = remember {
|
||||||
mutableStateOf(
|
val state = mutableStateOf(preferenceGet(preferences, preferenceKey))
|
||||||
preferenceGet(preferences, preferenceKey)
|
object : MutableState<T> by state {
|
||||||
)
|
override var value: T
|
||||||
|
get() = state.value
|
||||||
|
set(value) {
|
||||||
|
// ignore setting the same thing (messes up default preferences)
|
||||||
|
if (state.value == value) { return }
|
||||||
|
state.value = value
|
||||||
|
|
||||||
|
preferenceSet(preferences, preferenceKey, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener {
|
val listener = SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
@@ -98,11 +132,16 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
THEME,
|
THEME,
|
||||||
BLACK_BACKGROUND,
|
BLACK_BACKGROUND,
|
||||||
CURRENT_RELEASE_MODIFIED,
|
CURRENT_RELEASE_MODIFIED,
|
||||||
|
LAST_DISMISSED_UPDATE,
|
||||||
|
DISMISSED_GJ_UPDATE,
|
||||||
|
LAUNCH_ARGUMENTS,
|
||||||
|
IGNORE_LOAD_FAILURE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||||
return when (key) {
|
return when (key) {
|
||||||
Key.UPDATE_AUTOMATICALLY, Key.RELEASE_CHANNEL -> true
|
Key.UPDATE_AUTOMATICALLY -> true
|
||||||
|
Key.RELEASE_CHANNEL -> BuildConfig.DEBUG
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +156,11 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor"
|
Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor"
|
||||||
Key.THEME -> "PreferenceTheme"
|
Key.THEME -> "PreferenceTheme"
|
||||||
Key.BLACK_BACKGROUND -> "PreferenceBlackBackground"
|
Key.BLACK_BACKGROUND -> "PreferenceBlackBackground"
|
||||||
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModified"
|
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash"
|
||||||
|
Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
|
||||||
|
Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate"
|
||||||
|
Key.LAUNCH_ARGUMENTS -> "PreferenceLaunchArguments"
|
||||||
|
Key.IGNORE_LOAD_FAILURE -> "PreferenceIgnoreLoadFailure"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +171,13 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
return sharedPreferences.getBoolean(keyName, defaultValue)
|
return sharedPreferences.getBoolean(keyName, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setBoolean(key: Key, value: Boolean) {
|
||||||
|
val keyName = keyToName(key)
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putBoolean(keyName, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleBoolean(key: Key): Boolean {
|
fun toggleBoolean(key: Key): Boolean {
|
||||||
val currentValue = getBoolean(key)
|
val currentValue = getBoolean(key)
|
||||||
val keyName = keyToName(key)
|
val keyName = keyToName(key)
|
||||||
@@ -144,7 +194,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
return sharedPreferences.getString(keyName, null)
|
return sharedPreferences.getString(keyName, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setString(key: Key, value: String) {
|
fun setString(key: Key, value: String?) {
|
||||||
val keyName = keyToName(key)
|
val keyName = keyToName(key)
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putString(keyName, value)
|
putString(keyName, value)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M400,720L400,640L560,640L560,720L400,720ZM240,520L240,440L720,440L720,520L240,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M840,280L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L680,120L840,280ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM240,400L600,400L600,240L240,240L240,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/>
|
||||||
|
</vector>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<string name="launcher_launch_icon_alt">play icon</string>
|
<string name="launcher_launch_icon_alt">play icon</string>
|
||||||
<string name="launcher_download_icon_alt">download icon</string>
|
<string name="launcher_download_icon_alt">download icon</string>
|
||||||
<string name="launcher_settings_icon_alt">settings icon</string>
|
<string name="launcher_settings_icon_alt">settings icon</string>
|
||||||
|
<string name="launcher_retry_icon_alt">retry icon</string>
|
||||||
<string name="geode_download_title">Geode is not installed.</string>
|
<string name="geode_download_title">Geode is not installed.</string>
|
||||||
<string name="geode_download_description">Geode is downloading. Please wait.</string>
|
<string name="geode_download_description">Geode is downloading. Please wait.</string>
|
||||||
<plurals name="automatically_load_countdown">
|
<plurals name="automatically_load_countdown">
|
||||||
@@ -21,9 +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_unsupported_version_description">
|
<string name="launcher_outdated_version_description">
|
||||||
The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s).\nLaunch will continue, but crashes may occur!
|
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>
|
||||||
|
<string name="launcher_unsupported_version_description">
|
||||||
|
The current version of Geometry Dash is not compatible with the version supported by the launcher. A launcher update is required to fix this.\nLaunch will continue, but crashes may occur!
|
||||||
|
</string>
|
||||||
|
<string name="launcher_update_available">A new launcher update is available!</string>
|
||||||
|
<string name="launcher_update_dismiss">Dismiss</string>
|
||||||
|
<string name="safe_mode_enable_title">Enable safe mode?</string>
|
||||||
|
<string name="safe_mode_enable_description">Safe mode disables loading of all mods, allowing one to remove problematic mods or diagnose crashes. If you\'re not experiencing issues with Geode, you should probably not be using safe mode.</string>
|
||||||
|
|
||||||
<!-- Updater strings -->
|
<!-- Updater strings -->
|
||||||
<string name="release_fetch_failed">Update failed.\n%1$s</string>
|
<string name="release_fetch_failed">Update failed.\n%1$s</string>
|
||||||
@@ -40,18 +48,22 @@
|
|||||||
|
|
||||||
<!-- 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. Please report this issue!</string>
|
<string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture or version of the launcher and Geometry Dash do not match, or from a system bug.\nCheck application logs for more details.</string>
|
||||||
|
<string name="load_failed_abi_error_need_64bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 64-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
|
||||||
|
<string name="load_failed_abi_error_need_32bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 32-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
|
||||||
<string name="load_failed_generic_error">Load failed</string>
|
<string name="load_failed_generic_error">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>
|
||||||
<string name="settings_back_icon_alt">back arrow icon</string>
|
<string name="back_icon_alt">back</string>
|
||||||
|
|
||||||
<string name="export_folder_tag">Launcher External Folder</string>
|
<string name="export_folder_tag">Launcher External Folder</string>
|
||||||
<string name="export_folder_copied">Copied!</string>
|
<string name="text_copied">Copied to clipboard.</string>
|
||||||
|
|
||||||
<string name="preference_category_testing">General</string>
|
<string name="preference_category_testing">General</string>
|
||||||
<string name="preference_load_testing_name">Load files from /test</string>
|
<string name="preference_load_testing_name">Load files from /test</string>
|
||||||
@@ -59,6 +71,7 @@
|
|||||||
<string name="preference_load_automatically_description">Launches the game after a short delay.</string>
|
<string name="preference_load_automatically_description">Launches the game after a short delay.</string>
|
||||||
<string name="preferences_copy_external_button">Copy external files directory</string>
|
<string name="preferences_copy_external_button">Copy external files directory</string>
|
||||||
<string name="preferences_open_file_manager">Open file manager</string>
|
<string name="preferences_open_file_manager">Open file manager</string>
|
||||||
|
<string name="preferences_view_logs">View application logs</string>
|
||||||
|
|
||||||
<string name="preference_theme_name">Theme</string>
|
<string name="preference_theme_name">Theme</string>
|
||||||
<string name="preference_theme_select">Select theme</string>
|
<string name="preference_theme_select">Select theme</string>
|
||||||
@@ -72,11 +85,33 @@
|
|||||||
<string name="preference_release_channel_name">Use nightly release channel</string>
|
<string name="preference_release_channel_name">Use nightly release channel</string>
|
||||||
|
|
||||||
<string name="preference_check_for_updates_button">Check for updates</string>
|
<string name="preference_check_for_updates_button">Check for updates</string>
|
||||||
<string name="preference_check_for_updates_description">Current version: %1$s</string>
|
<string name="preference_check_for_updates_description">Current loader version: %1$s</string>
|
||||||
|
<string name="preference_launcher_version">Geode Launcher v%1$s</string>
|
||||||
<string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string>
|
<string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string>
|
||||||
<string name="preference_check_for_updates_none_found">No updates found.</string>
|
<string name="preference_check_for_updates_none_found">No updates found.</string>
|
||||||
<string name="preference_check_for_updates_success">Geode has been updated!</string>
|
<string name="preference_check_for_updates_success">Geode has been updated!</string>
|
||||||
<string name="preference_check_for_updates_downloading">Downloading update…</string>
|
<string name="preference_check_for_updates_downloading">Downloading update…</string>
|
||||||
<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 -->
|
||||||
|
<string name="title_activity_application_logs">Application Logs</string>
|
||||||
|
<string name="application_logs_no_logs">No logs available.</string>
|
||||||
|
<string name="application_logs_delete">Clear All</string>
|
||||||
|
<string name="application_logs_share">Share</string>
|
||||||
|
<string name="application_log_copy_label">Log Message</string>
|
||||||
|
<string name="application_logs_more">More</string>
|
||||||
|
<string name="application_logs_export">Save To</string>
|
||||||
|
<string name="application_logs_level">Set Log Level</string>
|
||||||
|
<string name="application_logs_level_dialog_name">Set minimum log level</string>
|
||||||
|
<string name="application_logs_export_completed">Log export complete.</string>
|
||||||
|
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
|
||||||
|
<string name="application_logs_crash_only">Show Crash Buffer</string>
|
||||||
|
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -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