mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-21 08:19:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1658072b1a | ||
|
|
b6e88b20cb | ||
|
|
f4af50b2cb | ||
|
|
00c47f4c70 | ||
|
|
11649a236a | ||
|
|
ff712196cc | ||
|
|
6c57f3f261 | ||
|
|
626f8d00d1 | ||
|
|
45ecc71749 | ||
|
|
3d60bd3d53 | ||
|
|
e40af4c49b | ||
|
|
e0a31694af | ||
|
|
c8994c2484 | ||
|
|
35c8428f08 | ||
|
|
bca761f26a | ||
|
|
79c06ca629 | ||
|
|
3194dff7ff | ||
|
|
9e86bbe355 | ||
|
|
eef9160970 | ||
|
|
3b4951def1 | ||
|
|
ef44d48ed0 | ||
|
|
aae2aa2290 | ||
|
|
0a62082519 | ||
|
|
758d5ad432 | ||
|
|
b3a776ce7c | ||
|
|
72f74fa084 | ||
|
|
0dba4f0606 | ||
|
|
2016aa4e90 |
No files matched your search
@@ -12,9 +12,15 @@ jobs:
|
|||||||
config:
|
config:
|
||||||
- name: Release
|
- name: Release
|
||||||
lower: release
|
lower: release
|
||||||
|
flavor: standard
|
||||||
|
|
||||||
- name: Debug
|
- name: Debug
|
||||||
lower: debug
|
lower: debug
|
||||||
|
flavor: standard
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
lower: release
|
||||||
|
flavor: googlePlay
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -34,9 +40,19 @@ jobs:
|
|||||||
- name: Fix gradle permissions
|
- name: Fix gradle permissions
|
||||||
run: chmod +x ./gradlew
|
run: chmod +x ./gradlew
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project (standard)
|
||||||
|
if: ${{ matrix.config.flavor == 'standard' }}
|
||||||
run: |
|
run: |
|
||||||
./gradlew app:assemble${{ matrix.config.name }} \
|
./gradlew app:assembleStandard${{ matrix.config.name }} \
|
||||||
|
-Pandroid.injected.signing.store.file="$HOME/release.keystore" \
|
||||||
|
-Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \
|
||||||
|
-Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \
|
||||||
|
-Pandroid.injected.signing.key.password="${{secrets.KEY_PASSWORD}}"
|
||||||
|
|
||||||
|
- name: Build project (Google Play)
|
||||||
|
if: ${{ matrix.config.flavor == 'googlePlay' }}
|
||||||
|
run: |
|
||||||
|
./gradlew app:bundleGooglePlayRelease -PdisableAbiSplits \
|
||||||
-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}}" \
|
||||||
@@ -44,35 +60,57 @@ jobs:
|
|||||||
|
|
||||||
- name: Rename universal apk
|
- name: Rename universal apk
|
||||||
id: rename_apk_universal
|
id: rename_apk_universal
|
||||||
|
if: ${{ matrix.config.flavor == 'standard' }}
|
||||||
run: |
|
run: |
|
||||||
SHORT_SHA=${GITHUB_SHA::7}
|
SHORT_SHA=${GITHUB_SHA::7}
|
||||||
APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-universal-${{ matrix.config.lower }}.apk
|
APK_PATH=${{github.workspace}}/app/build/outputs/apk/standard/${{ matrix.config.lower }}/app-standard-universal-${{ matrix.config.lower }}.apk
|
||||||
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.apk
|
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.apk
|
||||||
mv $APK_PATH $OUT_PATH
|
mv $APK_PATH $OUT_PATH
|
||||||
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Rename apk
|
- name: Rename apk
|
||||||
id: rename_apk
|
id: rename_apk
|
||||||
|
if: ${{ matrix.config.flavor == 'standard' }}
|
||||||
run: |
|
run: |
|
||||||
SHORT_SHA=${GITHUB_SHA::7}
|
SHORT_SHA=${GITHUB_SHA::7}
|
||||||
APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-armeabi-v7a-${{ matrix.config.lower }}.apk
|
APK_PATH=${{github.workspace}}/app/build/outputs/apk/standard/${{ matrix.config.lower }}/app-standard-armeabi-v7a-${{ matrix.config.lower }}.apk
|
||||||
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-android32-$SHORT_SHA.apk
|
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-android32-$SHORT_SHA.apk
|
||||||
mv $APK_PATH $OUT_PATH
|
mv $APK_PATH $OUT_PATH
|
||||||
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Rename mappings
|
- name: Rename mappings
|
||||||
id: rename_mappings
|
id: rename_mappings
|
||||||
if: ${{ matrix.config.lower == 'release' }}
|
if: ${{ matrix.config.flavor == 'standard' && matrix.config.lower == 'release' }}
|
||||||
run: |
|
run: |
|
||||||
SHORT_SHA=${GITHUB_SHA::7}
|
SHORT_SHA=${GITHUB_SHA::7}
|
||||||
MAPPING_PATH=${{github.workspace}}/app/build/outputs/mapping/${{ matrix.config.lower }}/mapping.txt
|
MAPPING_PATH=${{github.workspace}}/app/build/outputs/mapping/standardRelease/mapping.txt
|
||||||
|
OUT_PATH=${{github.workspace}}/mapping-${{ matrix.config.lower }}-$SHORT_SHA.txt
|
||||||
|
mv $MAPPING_PATH $OUT_PATH
|
||||||
|
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Rename AAB
|
||||||
|
id: rename_aab
|
||||||
|
if: ${{ matrix.config.flavor == 'googlePlay' }}
|
||||||
|
run: |
|
||||||
|
SHORT_SHA=${GITHUB_SHA::7}
|
||||||
|
AAB_PATH=${{github.workspace}}/app/build/outputs/bundle/googlePlayRelease/app-googlePlay-release.aab
|
||||||
|
OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.aab
|
||||||
|
mv $AAB_PATH $OUT_PATH
|
||||||
|
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Rename AAB mappings
|
||||||
|
id: rename_aab_mappings
|
||||||
|
if: ${{ matrix.config.flavor == 'googlePlay' }}
|
||||||
|
run: |
|
||||||
|
SHORT_SHA=${GITHUB_SHA::7}
|
||||||
|
MAPPING_PATH=${{github.workspace}}/app/build/outputs/mapping/googlePlayRelease/mapping.txt
|
||||||
OUT_PATH=${{github.workspace}}/mapping-${{ matrix.config.lower }}-$SHORT_SHA.txt
|
OUT_PATH=${{github.workspace}}/mapping-${{ matrix.config.lower }}-$SHORT_SHA.txt
|
||||||
mv $MAPPING_PATH $OUT_PATH
|
mv $MAPPING_PATH $OUT_PATH
|
||||||
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload release build
|
- name: Upload release build
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
if: ${{ matrix.config.lower == 'release' }}
|
if: ${{ matrix.config.flavor == 'standard' && matrix.config.lower == 'release' }}
|
||||||
with:
|
with:
|
||||||
name: geode-launcher-${{ matrix.config.lower }}
|
name: geode-launcher-${{ matrix.config.lower }}
|
||||||
path: |
|
path: |
|
||||||
@@ -82,9 +120,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload debug build
|
- name: Upload debug build
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
if: ${{ matrix.config.lower == 'debug' }}
|
if: ${{ matrix.config.flavor == 'standard' && matrix.config.lower == 'debug' }}
|
||||||
with:
|
with:
|
||||||
name: geode-launcher-${{ matrix.config.lower }}
|
name: geode-launcher-${{ matrix.config.lower }}
|
||||||
path: |
|
path: |
|
||||||
${{ steps.rename_apk_universal.outputs.path }}
|
${{ steps.rename_apk_universal.outputs.path }}
|
||||||
${{ steps.rename_apk.outputs.path }}
|
${{ steps.rename_apk.outputs.path }}
|
||||||
|
|
||||||
|
- name: Upload Google Play build
|
||||||
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
|
if: ${{ matrix.config.flavor == 'googlePlay' }}
|
||||||
|
with:
|
||||||
|
name: geode-launcher-google-play
|
||||||
|
path: |
|
||||||
|
${{ steps.rename_aab.outputs.path }}
|
||||||
|
${{ steps.rename_aab_mappings.outputs.path }}
|
||||||
+282
-22
@@ -1,4 +1,7 @@
|
|||||||
val composeBOM: String by rootProject.extra
|
import groovy.json.JsonSlurper
|
||||||
|
import java.net.URI
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
@@ -12,15 +15,241 @@ plugins {
|
|||||||
id("org.jetbrains.kotlin.plugin.compose")
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val currentGeodeVersion = "5.11.2"
|
||||||
|
|
||||||
|
abstract class DownloadVersionMetadataTask : DefaultTask() {
|
||||||
|
@get:Input
|
||||||
|
abstract val geodeVersion: Property<String>
|
||||||
|
|
||||||
|
@get:OutputFile
|
||||||
|
abstract val targetFile: RegularFileProperty
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun execute() {
|
||||||
|
val downloadGeodeVersion = geodeVersion.get()
|
||||||
|
val destinationFile = targetFile.get().asFile
|
||||||
|
|
||||||
|
val releaseUrl = "https://api.geode-sdk.org/v1/loader/versions/$downloadGeodeVersion"
|
||||||
|
URI.create(releaseUrl).toURL().openStream().use { response ->
|
||||||
|
destinationFile.outputStream().use { destination ->
|
||||||
|
response.copyTo(destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class DownloadGeodeFileTask : DefaultTask() {
|
||||||
|
@get:Input
|
||||||
|
abstract val platform: Property<String>
|
||||||
|
|
||||||
|
@get:InputFile
|
||||||
|
abstract val metadata: RegularFileProperty
|
||||||
|
|
||||||
|
private fun downloadAndVerifyFile(filename: String, url: String, hash: String?): File {
|
||||||
|
logger.info("Downloading $filename from $url")
|
||||||
|
|
||||||
|
val tempFile = File(temporaryDir, filename)
|
||||||
|
|
||||||
|
URI.create(url).toURL().openStream().use { response ->
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
response.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash != null) {
|
||||||
|
val downloadedHash = MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(tempFile.readBytes())
|
||||||
|
.toHexString()
|
||||||
|
|
||||||
|
if (downloadedHash != hash) {
|
||||||
|
throw GradleException("Hash check failed for file $filename, expected $hash but found $downloadedHash.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempFile
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadZip(fallbackUrl: String?): File {
|
||||||
|
val downloadPlatform = platform.get()
|
||||||
|
|
||||||
|
val metadataFile = metadata.get().asFile
|
||||||
|
val json = JsonSlurper().parse(metadataFile) as Map<*, *>
|
||||||
|
|
||||||
|
val payload = json["payload"] as? Map<*, *>
|
||||||
|
|
||||||
|
val downloads = payload?.get("downloads") as? Map<*, *>
|
||||||
|
val platformInfo = downloads?.get(downloadPlatform) as? Map<*, *>
|
||||||
|
|
||||||
|
val downloadUrl = platformInfo?.get("url") as? String ?: fallbackUrl
|
||||||
|
if (downloadUrl == null) {
|
||||||
|
throw GradleException("Failed to fetch download url for platform $platform.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectedHash = platformInfo?.get("hash") as? String
|
||||||
|
|
||||||
|
return downloadAndVerifyFile("$downloadPlatform.zip", downloadUrl, expectedHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class DownloadAndUnzipGeodeTask : DownloadGeodeFileTask() {
|
||||||
|
@get:Input
|
||||||
|
abstract val geodeVersion: Property<String>
|
||||||
|
|
||||||
|
@get:OutputFile
|
||||||
|
abstract val targetFile: RegularFileProperty
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun execute() {
|
||||||
|
val downloadGeodeVersion = geodeVersion.get()
|
||||||
|
val downloadPlatform = platform.get()
|
||||||
|
val destinationFile = targetFile.get().asFile
|
||||||
|
|
||||||
|
val tempOutput = this.downloadZip("https://github.com/geode-sdk/geode/releases/download/v$downloadGeodeVersion/geode-v$downloadGeodeVersion-$downloadPlatform.zip")
|
||||||
|
|
||||||
|
val expectedFile = "Geode.$downloadPlatform.so"
|
||||||
|
|
||||||
|
ZipInputStream(tempOutput.inputStream()).use { zip ->
|
||||||
|
var entry = zip.nextEntry
|
||||||
|
while (entry != null) {
|
||||||
|
if (entry.name == expectedFile) {
|
||||||
|
destinationFile.outputStream().use {
|
||||||
|
zip.copyTo(it)
|
||||||
|
}
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = zip.nextEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
throw GradleException("Could not find Geode release.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class DownloadAndUnzipGeodeResourcesTask : DownloadGeodeFileTask() {
|
||||||
|
init {
|
||||||
|
platform.set("resources")
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Input
|
||||||
|
abstract val geodeVersion: Property<String>
|
||||||
|
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val outputDir: DirectoryProperty
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun execute() {
|
||||||
|
val downloadGeodeVersion = geodeVersion.get()
|
||||||
|
|
||||||
|
val tempOutput = this.downloadZip("https://github.com/geode-sdk/geode/releases/download/v$downloadGeodeVersion/resources.zip")
|
||||||
|
|
||||||
|
val baseOutput = outputDir.get().asFile
|
||||||
|
if (baseOutput.exists()) baseOutput.deleteRecursively()
|
||||||
|
|
||||||
|
val output = File(baseOutput, "geode.loader")
|
||||||
|
output.mkdirs()
|
||||||
|
|
||||||
|
val canonicalOutput = output.canonicalPath
|
||||||
|
|
||||||
|
ZipInputStream(tempOutput.inputStream()).use { zipStream ->
|
||||||
|
var entry = zipStream.nextEntry
|
||||||
|
while (entry != null) {
|
||||||
|
val destination = File(output, entry.name)
|
||||||
|
if (!destination.canonicalPath.startsWith(canonicalOutput)) {
|
||||||
|
throw GradleException("attempted copy to outside of output directory: ${entry.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destination.isDirectory) {
|
||||||
|
destination.mkdirs()
|
||||||
|
} else {
|
||||||
|
destination.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
destination.outputStream().use { destinationStream ->
|
||||||
|
zipStream.copyTo(destinationStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = zipStream.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fetchGeodeMetadataTask = tasks.register<DownloadVersionMetadataTask>("fetchGeodeMetadata") {
|
||||||
|
description = "Fetches and saves a Geode version's metadata"
|
||||||
|
inputs.property("geodeVersion", currentGeodeVersion)
|
||||||
|
|
||||||
|
geodeVersion = currentGeodeVersion
|
||||||
|
targetFile = temporaryDir.resolve("metadata.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadGeode32Task = tasks.register<DownloadAndUnzipGeodeTask>("downloadGeode32Binary") {
|
||||||
|
description = "Downloads the 32bit Geode release archive"
|
||||||
|
inputs.property("geodeVersion", currentGeodeVersion)
|
||||||
|
|
||||||
|
geodeVersion = currentGeodeVersion
|
||||||
|
platform = "android32"
|
||||||
|
metadata = fetchGeodeMetadataTask.flatMap { it.targetFile }
|
||||||
|
targetFile = temporaryDir.resolve("libgeode.so")
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadGeode64Task = tasks.register<DownloadAndUnzipGeodeTask>("downloadGeode64Binary") {
|
||||||
|
description = "Downloads the 64bit Geode release archive"
|
||||||
|
inputs.property("geodeVersion", currentGeodeVersion)
|
||||||
|
|
||||||
|
geodeVersion = currentGeodeVersion
|
||||||
|
platform = "android64"
|
||||||
|
metadata = fetchGeodeMetadataTask.flatMap { it.targetFile }
|
||||||
|
targetFile = temporaryDir.resolve("libgeode.so")
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadGeodeResourcesTask = tasks.register<DownloadAndUnzipGeodeResourcesTask>("downloadGeodeResources") {
|
||||||
|
description = "Downloads Geode resources"
|
||||||
|
inputs.property("geodeVersion", currentGeodeVersion)
|
||||||
|
|
||||||
|
geodeVersion = currentGeodeVersion
|
||||||
|
metadata = fetchGeodeMetadataTask.flatMap { it.targetFile }
|
||||||
|
outputDir = layout.buildDirectory.dir("geode-resources")
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class MergeGeodeFilesTask : DefaultTask() {
|
||||||
|
@get:InputFile
|
||||||
|
abstract val binary32: RegularFileProperty
|
||||||
|
|
||||||
|
@get:InputFile
|
||||||
|
abstract val binary64: RegularFileProperty
|
||||||
|
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val outputDir: DirectoryProperty
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun execute() {
|
||||||
|
val out = outputDir.get().asFile
|
||||||
|
out.deleteRecursively()
|
||||||
|
out.mkdirs()
|
||||||
|
|
||||||
|
binary32.get().asFile.copyTo(outputDir.file("armeabi-v7a/libgeode.so").get().asFile)
|
||||||
|
binary64.get().asFile.copyTo(outputDir.file("arm64-v8a/libgeode.so").get().asFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fullDownloadTask = tasks.register<MergeGeodeFilesTask>("downloadGeodeFull") {
|
||||||
|
description = "Downloads both Geode releases"
|
||||||
|
|
||||||
|
binary32 = downloadGeode32Task.flatMap { it.targetFile }
|
||||||
|
binary64 = downloadGeode64Task.flatMap { it.targetFile }
|
||||||
|
outputDir = layout.buildDirectory.dir("geode-prebuilt")
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 36
|
compileSdk = 37
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.geode.launcher"
|
applicationId = "com.geode.launcher"
|
||||||
minSdk = 23
|
minSdk = 23
|
||||||
targetSdk = 36
|
targetSdk = 37
|
||||||
versionCode = 29
|
versionCode = 31
|
||||||
versionName = "1.8.0"
|
versionName = "1.9.0"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
@@ -39,7 +268,7 @@ android {
|
|||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
isEnable = true
|
isEnable = !project.hasProperty("disableAbiSplits")
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
//noinspection ChromeOsAbiSupport. i'm sorry!
|
//noinspection ChromeOsAbiSupport. i'm sorry!
|
||||||
@@ -49,6 +278,24 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions += "distribution"
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
create("standard") {
|
||||||
|
dimension = "distribution"
|
||||||
|
isDefault = true
|
||||||
|
buildConfigField("boolean", "GOOGLE_PLAY_BUILD", "false")
|
||||||
|
buildConfigField("String", "PREBUNDLED_GEODE", "null")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("googlePlay") {
|
||||||
|
dimension = "distribution"
|
||||||
|
applicationId = "com.geode.launcher.play"
|
||||||
|
buildConfigField("boolean", "GOOGLE_PLAY_BUILD", "true")
|
||||||
|
buildConfigField("String", "PREBUNDLED_GEODE", "\"$currentGeodeVersion\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
@@ -64,8 +311,8 @@ android {
|
|||||||
// enables a polyfill for java Instant on api levels < 26 (used for updater)
|
// enables a polyfill for java Instant on api levels < 26 (used for updater)
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
@@ -83,32 +330,45 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
namespace = "com.geode.launcher"
|
namespace = "com.geode.launcher"
|
||||||
ndkVersion = "29.0.14206865"
|
ndkVersion = "30.0.16248370"
|
||||||
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants(selector().withFlavor("distribution" to "googlePlay")) { variant ->
|
||||||
|
variant.sources.jniLibs?.addGeneratedSourceDirectory(
|
||||||
|
fullDownloadTask,
|
||||||
|
MergeGeodeFilesTask::outputDir
|
||||||
|
)
|
||||||
|
variant.sources.assets?.addGeneratedSourceDirectory(
|
||||||
|
downloadGeodeResourcesTask,
|
||||||
|
DownloadAndUnzipGeodeResourcesTask::outputDir
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation (platform("androidx.compose:compose-bom:$composeBOM"))
|
implementation (platform("androidx.compose:compose-bom:2026.09.00"))
|
||||||
implementation ("androidx.core:core-ktx:1.18.0")
|
implementation ("androidx.core:core-ktx:1.19.1")
|
||||||
implementation ("androidx.compose.ui:ui")
|
implementation ("androidx.compose.ui:ui")
|
||||||
implementation ("androidx.compose.material3:material3")
|
implementation ("androidx.compose.material3:material3")
|
||||||
implementation ("androidx.compose.ui:ui-tooling-preview")
|
implementation ("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
|
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0")
|
||||||
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0")
|
||||||
implementation ("androidx.activity:activity-compose:1.13.0")
|
implementation ("androidx.activity:activity-compose:1.13.0")
|
||||||
implementation ("androidx.activity:activity-ktx:1.13.0")
|
implementation ("androidx.activity:activity-ktx:1.13.0")
|
||||||
implementation ("androidx.appcompat:appcompat:1.7.1")
|
implementation ("androidx.appcompat:appcompat:1.8.0")
|
||||||
implementation ("androidx.documentfile:documentfile:1.1.0")
|
implementation ("androidx.documentfile:documentfile:1.1.0")
|
||||||
|
|
||||||
implementation ("com.squareup.okio:okio:3.17.0")
|
implementation ("com.squareup.okio:okio:3.18.2")
|
||||||
implementation ("com.squareup.okhttp3:okhttp:5.3.2")
|
implementation ("com.squareup.okhttp3:okhttp:5.5.0")
|
||||||
implementation ("com.squareup.okhttp3:okhttp-coroutines:5.3.2")
|
implementation ("com.squareup.okhttp3:okhttp-coroutines:5.5.0")
|
||||||
|
|
||||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
|
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
|
||||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.10.0")
|
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.11.0")
|
||||||
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
|
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
|
||||||
|
|
||||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.2")
|
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.46.0")
|
||||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2")
|
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.46.0")
|
||||||
implementation ("androidx.browser:browser:1.10.0")
|
implementation ("androidx.browser:browser:1.10.0")
|
||||||
debugImplementation ("androidx.compose.ui:ui-tooling")
|
debugImplementation ("androidx.compose.ui:ui-tooling")
|
||||||
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
|
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -17,8 +17,6 @@
|
|||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
<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_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-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
@@ -127,7 +125,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".GeometryDashActivity"
|
android:name=".GeometryDashActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard"
|
android:configChanges="fontScale|layoutDirection|density|smallestScreenSize|screenSize|uiMode|screenLayout|orientation|navigation|keyboardHidden|keyboard|locale"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:screenOrientation="landscape"
|
android:screenOrientation="landscape"
|
||||||
@@ -235,10 +233,30 @@
|
|||||||
<data android:scheme="geode-launcher" android:host="main" />
|
<data android:scheme="geode-launcher" android:host="main" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
<activity-alias
|
||||||
|
android:name=".MainActivityGreen"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="true"
|
||||||
|
android:icon="@mipmap/ic_launcher_green"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:targetActivity=".MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="geode-launcher" android:host="main" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".UserDirectoryProvider"
|
android:name=".UserDirectoryProvider"
|
||||||
android:authorities="com.geode.launcher.user"
|
android:authorities="${applicationId}.user"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:grantUriPermissions="true"
|
android:grantUriPermissions="true"
|
||||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
@@ -248,7 +266,7 @@
|
|||||||
</provider>
|
</provider>
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="com.geode.launcher.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
@@ -2,33 +2,106 @@ package com.customRobTop
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.*
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.OpenableColumns
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import com.customRobTop.contracts.ImportFileActivityContract
|
||||||
|
import com.customRobTop.contracts.ShareFileActivityContract
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
|
import com.geode.launcher.utils.DownloadUtils
|
||||||
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
|
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
|
||||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard
|
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard
|
||||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard
|
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
@Suppress("unused", "UNUSED_PARAMETER")
|
@Suppress("unused", "UNUSED_PARAMETER")
|
||||||
object BaseRobTopActivity {
|
object BaseRobTopActivity {
|
||||||
|
const val KEYBOARD_INPUT_MULTILINE = 1
|
||||||
|
const val KEYBOARD_INPUT_NUMERIC = 4
|
||||||
|
const val KEYBOARD_INPUT_PASSWORD = 2
|
||||||
|
const val LEVEL_EXTENSION: String = ".gdlvl"
|
||||||
|
|
||||||
private var isLoaded = false
|
private var isLoaded = false
|
||||||
var blockBackButton = false
|
var blockBackButton = false
|
||||||
private var keyboardActive = false
|
private var keyboardActive = false
|
||||||
|
var curKeyboardInputFlags = 0
|
||||||
|
|
||||||
var isPaused = false
|
var isPaused = false
|
||||||
|
|
||||||
lateinit var me: WeakReference<Activity>
|
lateinit var me: WeakReference<Activity>
|
||||||
private var shouldResumeSound = true
|
private var shouldResumeSound = true
|
||||||
|
|
||||||
fun setCurrentActivity(currentActivity: Activity) {
|
private var pendingExportPaths = emptyList<String>()
|
||||||
|
|
||||||
|
private lateinit var importFileActivityContract: ActivityResultLauncher<Unit>
|
||||||
|
private lateinit var shareFileActivityContract: ActivityResultLauncher<String>
|
||||||
|
private lateinit var shareFolderActivityContract: ActivityResultLauncher<Uri?>
|
||||||
|
|
||||||
|
fun setCurrentActivity(currentActivity: AppCompatActivity) {
|
||||||
me = WeakReference(currentActivity)
|
me = WeakReference(currentActivity)
|
||||||
|
|
||||||
|
importFileActivityContract = currentActivity.registerForActivityResult(ImportFileActivityContract()) { uris ->
|
||||||
|
val filePaths = copyPickedFiles(uris)
|
||||||
|
|
||||||
|
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
|
||||||
|
JniToCpp.filesPicked(filePaths.toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shareFileActivityContract = currentActivity.registerForActivityResult(ShareFileActivityContract()) { uri ->
|
||||||
|
val currentlyPending = pendingExportPaths
|
||||||
|
pendingExportPaths = emptyList()
|
||||||
|
|
||||||
|
val exportPath = currentlyPending.firstOrNull()
|
||||||
|
if (exportPath == null || uri == null) {
|
||||||
|
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
|
||||||
|
JniToCpp.fileShareFinished(false)
|
||||||
|
}
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = saveToDocument(exportPath, uri)
|
||||||
|
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
|
||||||
|
JniToCpp.fileShareFinished(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shareFolderActivityContract = currentActivity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||||
|
val currentlyPending = pendingExportPaths
|
||||||
|
pendingExportPaths = emptyList()
|
||||||
|
|
||||||
|
if (uri == null) {
|
||||||
|
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
|
||||||
|
JniToCpp.fileShareFinished(false)
|
||||||
|
}
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = saveToFolder(currentlyPending, uri) > 0
|
||||||
|
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
|
||||||
|
JniToCpp.fileShareFinished(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("HardwareIds")
|
@SuppressLint("HardwareIds")
|
||||||
@@ -87,6 +160,18 @@ object BaseRobTopActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getClipboardString(): String {
|
||||||
|
me.get()?.run {
|
||||||
|
val manager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = manager.primaryClip
|
||||||
|
if (clip != null && clip.itemCount > 0) {
|
||||||
|
return clip.getItemAt(0).coerceToText(this).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun sendMail(subject: String, body: String, to: String) {
|
fun sendMail(subject: String, body: String, to: String) {
|
||||||
me.get()?.runOnUiThread {
|
me.get()?.runOnUiThread {
|
||||||
@@ -264,4 +349,164 @@ object BaseRobTopActivity {
|
|||||||
level = DeprecationLevel.HIDDEN
|
level = DeprecationLevel.HIDDEN
|
||||||
)
|
)
|
||||||
fun logEvent(event: String) {}
|
fun logEvent(event: String) {}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setKeyboardInputFlags(flags: Int) {
|
||||||
|
curKeyboardInputFlags = flags
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun pickImportFiles() {
|
||||||
|
try {
|
||||||
|
importFileActivityContract.launch(Unit)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
me.get()?.apply {
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
R.string.no_activity_found,
|
||||||
|
Toast.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyPickedFiles(uris: List<Uri>): List<String> {
|
||||||
|
val me = me.get() ?: return emptyList()
|
||||||
|
|
||||||
|
val importsDir = File(LaunchUtils.getSaveDirectory(me), "imports")
|
||||||
|
importsDir.mkdirs()
|
||||||
|
|
||||||
|
return uris.mapIndexedNotNull { index, uri ->
|
||||||
|
val displayName = displayName(uri)
|
||||||
|
?: "import_$index$LEVEL_EXTENSION"
|
||||||
|
if (!displayName.endsWith(LEVEL_EXTENSION)) {
|
||||||
|
return@mapIndexedNotNull null
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputFile = File(importsDir, displayName)
|
||||||
|
var counter = 2
|
||||||
|
while (outputFile.exists()) {
|
||||||
|
outputFile = File(importsDir, "${counter}_$displayName")
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
me.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
outputFile.outputStream().use { output ->
|
||||||
|
DownloadUtils.copyFile(input, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFile.absolutePath
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
outputFile.delete()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayName(uri: Uri): String? {
|
||||||
|
me.get()?.contentResolver?.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursorQuery ->
|
||||||
|
if (cursorQuery.moveToFirst()) {
|
||||||
|
val displayName = cursorQuery.getString(0)
|
||||||
|
if (displayName.isNotEmpty()) {
|
||||||
|
return File(displayName).name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun shareFiles(files: Array<String>) {
|
||||||
|
pendingExportPaths = files.toList()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (files.size == 1) {
|
||||||
|
shareFileActivityContract.launch(files.first())
|
||||||
|
return
|
||||||
|
} else if (files.size > 1) {
|
||||||
|
shareFolderActivityContract.launch(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
me.get()?.apply {
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
R.string.no_activity_found,
|
||||||
|
Toast.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent {
|
||||||
|
JniToCpp.fileShareFinished(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveToFolder(exportPaths: List<String>, treeUri: Uri): Int {
|
||||||
|
val uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri))
|
||||||
|
|
||||||
|
val me = me.get() ?: return 0
|
||||||
|
|
||||||
|
return exportPaths.count { path ->
|
||||||
|
val filename = File(path).name
|
||||||
|
val documentUri = DocumentsContract.createDocument(me.contentResolver, uri, "application/octet-stream", filename)
|
||||||
|
?: return@count false
|
||||||
|
|
||||||
|
saveToDocument(path, documentUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveToDocument(exportPath: String, uri: Uri): Boolean {
|
||||||
|
try {
|
||||||
|
FileInputStream(exportPath).use { inputStream ->
|
||||||
|
openOutput(uri).use { outputStream ->
|
||||||
|
if (outputStream != null) {
|
||||||
|
DownloadUtils.copyFile(inputStream, outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureLevelExtension(uri)
|
||||||
|
return true
|
||||||
|
} catch (_: IOException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openOutput(uri: Uri): OutputStream? {
|
||||||
|
val me = me.get() ?: return null
|
||||||
|
return try {
|
||||||
|
me.contentResolver.openOutputStream(uri, "wt")
|
||||||
|
} catch (_: IOException) {
|
||||||
|
me.contentResolver.openOutputStream(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureLevelExtension(uri: Uri) {
|
||||||
|
val displayName = displayName(uri)
|
||||||
|
if (displayName == null || displayName.endsWith(LEVEL_EXTENSION)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val me = me.get() ?: return
|
||||||
|
|
||||||
|
val extensionIdx = displayName.lastIndexOf(".gdlvl.")
|
||||||
|
val substring = if (extensionIdx < 0) {
|
||||||
|
displayName + LEVEL_EXTENSION
|
||||||
|
} else {
|
||||||
|
displayName.substring(0, extensionIdx + 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DocumentsContract.renameDocument(me.contentResolver, uri, substring)
|
||||||
|
} catch (_: IOException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -55,4 +55,16 @@ object JniToCpp {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
external fun videoAdShowed()
|
external fun videoAdShowed()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun filesPicked(files: Array<String>)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun fileShareFinished(success: Boolean)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun textInputCaretToEnd()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun textInputDone()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.customRobTop.contracts
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
|
||||||
|
class ImportFileActivityContract : ActivityResultContract<Unit, List<Uri>>() {
|
||||||
|
override fun createIntent(
|
||||||
|
context: Context,
|
||||||
|
input: Unit
|
||||||
|
): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "*/*"
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(
|
||||||
|
resultCode: Int,
|
||||||
|
intent: Intent?
|
||||||
|
): List<Uri> {
|
||||||
|
if (resultCode != Activity.RESULT_OK || intent == null) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val clipData = intent.clipData
|
||||||
|
return if (clipData != null) {
|
||||||
|
List(clipData.itemCount) { i -> clipData.getItemAt(i).uri }
|
||||||
|
} else {
|
||||||
|
val intentData = intent.data
|
||||||
|
if (intentData != null)
|
||||||
|
listOf(intentData)
|
||||||
|
else
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.customRobTop.contracts
|
||||||
|
|
||||||
|
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
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ShareFileActivityContract : ActivityResultContract<String, Uri?>() {
|
||||||
|
override fun createIntent(
|
||||||
|
context: Context,
|
||||||
|
input: String
|
||||||
|
): Intent {
|
||||||
|
return Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "application/octet-stream"
|
||||||
|
putExtra(Intent.EXTRA_TITLE, File(input).name)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
putExtra(DocumentsContract.EXTRA_INITIAL_URI,
|
||||||
|
DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Download")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(
|
||||||
|
resultCode: Int,
|
||||||
|
intent: Intent?
|
||||||
|
): Uri? {
|
||||||
|
if (resultCode != Activity.RESULT_OK || intent == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return intent.data
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -171,7 +171,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
|
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
|
||||||
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
|
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
|
||||||
|
|
||||||
if (GamePackageUtils.getGameVersionCode(packageManager) >= 39L) {
|
val gameVersionCode = GamePackageUtils.getGameVersionCode(packageManager)
|
||||||
|
if (gameVersionCode >= 39L) {
|
||||||
// this fix requires geode v3, which is 2.206+
|
// this fix requires geode v3, which is 2.206+
|
||||||
// there is a short period in which 2.206 users will still have geode v2, but whatever. ig
|
// there is a short period in which 2.206 users will still have geode v2, but whatever. ig
|
||||||
LauncherFix.enableExceptionsRenaming()
|
LauncherFix.enableExceptionsRenaming()
|
||||||
@@ -182,6 +183,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
loadInternalMods()
|
loadInternalMods()
|
||||||
|
|
||||||
val baseView = createView()
|
val baseView = createView()
|
||||||
|
if (gameVersionCode >= 42L) {
|
||||||
|
mGLSurfaceView?.sendNewCocosEvents = true
|
||||||
|
}
|
||||||
|
|
||||||
if (!GeodeUtils.handleSafeArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
|
if (!GeodeUtils.handleSafeArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
|
||||||
val frameLayoutParams = ViewGroup.LayoutParams(
|
val frameLayoutParams = ViewGroup.LayoutParams(
|
||||||
@@ -205,12 +209,22 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
left = 0, top = 0, right = 0, bottom = 0
|
left = 0, top = 0, right = 0, bottom = 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cocos2dxGLSurfaceView.onKeyboardInsets(insets)
|
||||||
|
|
||||||
WindowInsets.CONSUMED
|
WindowInsets.CONSUMED
|
||||||
}
|
}
|
||||||
|
|
||||||
frameLayout.addView(baseView)
|
frameLayout.addView(baseView)
|
||||||
setContentView(frameLayout)
|
setContentView(frameLayout)
|
||||||
} else {
|
} else {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
baseView.setOnApplyWindowInsetsListener { view, insets ->
|
||||||
|
Cocos2dxGLSurfaceView.onKeyboardInsets(insets)
|
||||||
|
return@setOnApplyWindowInsetsListener view.onApplyWindowInsets(insets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(baseView)
|
setContentView(baseView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,53 +393,73 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
return "$sourceDir!/lib/$architecture/"
|
return "$sourceDir!/lib/$architecture/"
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
|
||||||
private fun loadGeodeLibrary() {
|
private fun loadGeodeLibrary() {
|
||||||
// Load Geode if exists
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
// bundling the object with the application allows for nicer backtraces
|
if (BuildConfig.PREBUNDLED_GEODE != null) {
|
||||||
try {
|
// attempt to load an externally provided Geode first
|
||||||
// put libgeode.so in jniLibs/armeabi-v7a to get this
|
if (!loadExternalGeode()) {
|
||||||
System.loadLibrary("geode")
|
copyGeodeResources()
|
||||||
return
|
System.loadLibrary("geode")
|
||||||
} catch (e: UnsatisfiedLinkError) {
|
}
|
||||||
// but users may prefer it stored with data
|
} else {
|
||||||
val geodeFilename = LaunchUtils.geodeFilename
|
// Load Geode if exists
|
||||||
val geodePath = File(filesDir.path, "launcher/$geodeFilename")
|
// bundling the object with the application allows for nicer backtraces
|
||||||
if (geodePath.exists()) {
|
try {
|
||||||
System.load(geodePath.path)
|
// put libgeode.so in jniLibs/armeabi-v7a to get this
|
||||||
|
System.loadLibrary("geode")
|
||||||
return
|
return
|
||||||
}
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
|
if (!loadExternalGeode()) {
|
||||||
// you know zmx i have 0 clue what this does so im
|
throw e
|
||||||
// just gonna like copy the binary from external
|
|
||||||
// also i get 20 million permission denied errors
|
|
||||||
val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!!
|
|
||||||
|
|
||||||
val copiedPath = File(filesDir.path, "copied")
|
|
||||||
if (copiedPath.exists()) {
|
|
||||||
copiedPath.deleteRecursively()
|
|
||||||
}
|
|
||||||
copiedPath.mkdir()
|
|
||||||
|
|
||||||
val copiedGeodePath = File(copiedPath.path, "Geode.so")
|
|
||||||
|
|
||||||
if (externalGeodePath.exists()) {
|
|
||||||
DownloadUtils.copyFile(
|
|
||||||
FileInputStream(externalGeodePath),
|
|
||||||
FileOutputStream(copiedGeodePath)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (copiedGeodePath.exists()) {
|
|
||||||
println("Loading Geode from ${externalGeodePath.name}")
|
|
||||||
System.load(copiedGeodePath.path)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||||
|
private fun loadExternalGeode(): Boolean {
|
||||||
|
// but users may prefer it stored with data
|
||||||
|
val geodeFilename = LaunchUtils.geodeFilename
|
||||||
|
val geodePath = File(filesDir.path, "launcher/$geodeFilename")
|
||||||
|
if (geodePath.exists()) {
|
||||||
|
println("Loading Geode from ${geodePath.path}")
|
||||||
|
System.load(geodePath.path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// you know zmx i have 0 clue what this does so im
|
||||||
|
// just gonna like copy the binary from external
|
||||||
|
// also i get 20 million permission denied errors
|
||||||
|
val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)
|
||||||
|
?: return false
|
||||||
|
|
||||||
|
val copiedPath = File(filesDir.path, "copied")
|
||||||
|
if (copiedPath.exists()) {
|
||||||
|
copiedPath.deleteRecursively()
|
||||||
|
}
|
||||||
|
copiedPath.mkdir()
|
||||||
|
|
||||||
|
val copiedGeodePath = File(copiedPath.path, "Geode.so")
|
||||||
|
|
||||||
|
if (externalGeodePath.exists()) {
|
||||||
|
DownloadUtils.copyFile(
|
||||||
|
FileInputStream(externalGeodePath),
|
||||||
|
FileOutputStream(copiedGeodePath)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (copiedGeodePath.exists()) {
|
||||||
|
// android 17 fix
|
||||||
|
copiedGeodePath.setWritable(false)
|
||||||
|
|
||||||
|
println("Loading Geode from ${externalGeodePath.name}")
|
||||||
|
System.load(copiedGeodePath.path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun createView(): FrameLayout {
|
private fun createView(): FrameLayout {
|
||||||
val frameLayoutParams = ViewGroup.LayoutParams(
|
val frameLayoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
@@ -477,6 +511,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||||
glSurfaceView.cocos2dxEditText = editText
|
glSurfaceView.cocos2dxEditText = editText
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
glSurfaceView.defaultFocusHighlightEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
return frameLayout
|
return frameLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,6 +701,56 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun copyGeodeResources() {
|
||||||
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
|
if (BuildConfig.PREBUNDLED_GEODE == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val resourcesDirectory = LaunchUtils.getGeodeResourcesDirectory(this)
|
||||||
|
|
||||||
|
val pathBase = "geode.loader"
|
||||||
|
val versionFilename = "version"
|
||||||
|
|
||||||
|
val bundledVersion = try {
|
||||||
|
assets.open("$pathBase/$versionFilename").bufferedReader().use {
|
||||||
|
it.readText()
|
||||||
|
}.trimEnd()
|
||||||
|
} catch (_: IOException) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val versionFile = File(resourcesDirectory, versionFilename)
|
||||||
|
val resourcesVersion = if (versionFile.exists()) {
|
||||||
|
versionFile.readText().trimEnd()
|
||||||
|
} else null
|
||||||
|
|
||||||
|
if (bundledVersion == resourcesVersion) {
|
||||||
|
println("copyGeodeResources: found $resourcesVersion, expected $bundledVersion. No work needs to be done!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourcesDirectory.exists()) {
|
||||||
|
resourcesDirectory.deleteRecursively()
|
||||||
|
}
|
||||||
|
resourcesDirectory.mkdirs()
|
||||||
|
|
||||||
|
println("copyGeodeResources: found $resourcesVersion, expected $bundledVersion. Copying assets")
|
||||||
|
|
||||||
|
val fileListing = try {
|
||||||
|
assets.list(pathBase)
|
||||||
|
} catch (_: IOException) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileListing?.forEach { fileName ->
|
||||||
|
val resourceOutput = File(resourcesDirectory, fileName)
|
||||||
|
|
||||||
|
val resource = assets.open("$pathBase/$fileName")
|
||||||
|
DownloadUtils.copyFile(resource, resourceOutput.outputStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class EGLConfigChooser : GLSurfaceView.EGLConfigChooser {
|
class EGLConfigChooser : GLSurfaceView.EGLConfigChooser {
|
||||||
// this comes from EGL14, but is unavailable on EGL10
|
// this comes from EGL14, but is unavailable on EGL10
|
||||||
// also EGL14 is incompatible with EGL10. so whatever
|
// also EGL14 is incompatible with EGL10. so whatever
|
||||||
|
|||||||
@@ -104,13 +104,25 @@ class MainActivity : ComponentActivity() {
|
|||||||
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
||||||
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
|
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
|
||||||
|
|
||||||
if (gdInstalled && geodeInstalled && intent.getBooleanExtra("restarted", false)) {
|
val restartImmediately = intent.getBooleanExtra("restarted", false)
|
||||||
onLaunch(this)
|
if (gdInstalled && geodeInstalled && restartImmediately) {
|
||||||
|
val withSafeMode = intent.getBooleanExtra("safe-mode", false)
|
||||||
|
onLaunch(this, withSafeMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
clearDownloadedApks(this)
|
clearDownloadedApks(this)
|
||||||
|
|
||||||
|
// remove old Geode for the like 4 people that downloaded the Play Store build
|
||||||
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
|
if (BuildConfig.PREBUNDLED_GEODE != null) {
|
||||||
|
val preferences = PreferenceUtils.get(this)
|
||||||
|
if (!preferences.getBoolean(PreferenceUtils.Key.PREBUNDLED_GEODE_MIGRATION_PERFORMED)) {
|
||||||
|
LaunchUtils.getInstalledGeodePath(this)?.delete()
|
||||||
|
preferences.setBoolean(PreferenceUtils.Key.PREBUNDLED_GEODE_MIGRATION_PERFORMED, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
|
val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
|
||||||
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
|
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
|
||||||
|
|
||||||
@@ -207,7 +219,7 @@ fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): Lau
|
|||||||
)
|
)
|
||||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
|
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
|
||||||
title = stringResource(R.string.launcher_cancelled_error),
|
title = stringResource(R.string.launcher_cancelled_error),
|
||||||
details = stringResource(R.string.launcher_cancelled_outdated)
|
details = stringResource(R.string.launcher_cancelled_outdated, GamePackageUtils.getSupportedUnifiedVersion())
|
||||||
)
|
)
|
||||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
|
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
|
||||||
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
|
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
|
||||||
@@ -256,7 +268,7 @@ fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Bool
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
||||||
return mapCancelReasonToInfo(state.reason)
|
mapCancelReasonToInfo(state.reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -716,6 +728,9 @@ fun GeodeUpdateIndicator(snackbarHostState: SnackbarHostState, onRetry: () -> Un
|
|||||||
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
||||||
R.string.release_fetch_external_in_use_short
|
R.string.release_fetch_external_in_use_short
|
||||||
)
|
)
|
||||||
|
ReleaseManager.UpdateException.Reason.HASH_VALIDATION_FAILED -> stringResource(
|
||||||
|
R.string.release_fetch_hash_validation_failed_short
|
||||||
|
)
|
||||||
else -> stringResource(R.string.release_fetch_generic_short)
|
else -> stringResource(R.string.release_fetch_generic_short)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -825,10 +840,11 @@ fun AltMainScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// only show launcher update in a case where a user won't see it ingame
|
// only show launcher update in a case where a user won't see it ingame
|
||||||
if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
|
val currentUIState = launchUIState
|
||||||
|
if (currentUIState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||||
val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) }
|
val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) }
|
||||||
|
|
||||||
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING && currentUIState.reason != LaunchViewModel.LaunchCancelReason.GAME_OUTDATED) {
|
||||||
UnsupportedVersionWarning()
|
UnsupportedVersionWarning()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ 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 androidx.annotation.RequiresApi
|
||||||
|
import com.geode.launcher.utils.ApplicationIcon
|
||||||
|
import com.geode.launcher.utils.IconUtils
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
|
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
|
||||||
@@ -60,8 +63,17 @@ class UserDirectoryProvider : DocumentsProvider() {
|
|||||||
DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
|
DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
|
||||||
DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
|
DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
|
||||||
)
|
)
|
||||||
add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.app_name))
|
|
||||||
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.geode_logo)
|
val selectedIconKey = PreferenceUtils.get(context).getString(PreferenceUtils.Key.SELECTED_ICON)
|
||||||
|
?: "geode"
|
||||||
|
val selectedIcon = ApplicationIcon.fromId(selectedIconKey)
|
||||||
|
val iconDetails = IconUtils.getIconDetails(selectedIcon)
|
||||||
|
|
||||||
|
val nameId = if (selectedIcon == ApplicationIcon.SAPPHIRE) R.string.application_icon_sapphire else R.string.app_name
|
||||||
|
val logoId = if (selectedIcon == ApplicationIcon.GEODE) R.drawable.geode_logo else iconDetails.iconId
|
||||||
|
|
||||||
|
add(DocumentsContract.Root.COLUMN_TITLE, context.getString(nameId))
|
||||||
|
add(DocumentsContract.Root.COLUMN_ICON, logoId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -202,7 +202,11 @@ fun onShowLogs(context: Context) {
|
|||||||
fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
|
fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val currentRelease = remember {
|
val currentRelease = remember {
|
||||||
PreferenceUtils.get(context).getString(PreferenceUtils.Key.CURRENT_VERSION_TAG)
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
|
if (BuildConfig.PREBUNDLED_GEODE != null)
|
||||||
|
"${BuildConfig.PREBUNDLED_GEODE}*"
|
||||||
|
else
|
||||||
|
PreferenceUtils.get(context).getString(PreferenceUtils.Key.CURRENT_VERSION_TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
val gameVersion = remember {
|
val gameVersion = remember {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.geode.launcher.utils.GeodeUtils
|
|||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -142,7 +143,7 @@ fun onSettings(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, enabled: Boolean = true, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit) {
|
fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, content: @Composable RowScope.() -> Unit) {
|
||||||
// compose apis don't provide a good way of adding long press to a button
|
// compose apis don't provide a good way of adding long press to a button
|
||||||
var isLongPress by remember { mutableStateOf(false) }
|
var isLongPress by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -154,11 +155,11 @@ fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, enabled: Boole
|
|||||||
interactionSource.interactions.collectLatest { interaction ->
|
interactionSource.interactions.collectLatest { interaction ->
|
||||||
when (interaction) {
|
when (interaction) {
|
||||||
is PressInteraction.Press -> {
|
is PressInteraction.Press -> {
|
||||||
delay(viewConfiguration.longPressTimeoutMillis)
|
delay(viewConfiguration.longPressTimeoutMillis.milliseconds)
|
||||||
|
|
||||||
// perform a second delay to make the action more obvious
|
// perform a second delay to make the action more obvious
|
||||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
delay(viewConfiguration.longPressTimeoutMillis)
|
delay(viewConfiguration.longPressTimeoutMillis.milliseconds)
|
||||||
|
|
||||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
|
||||||
@@ -186,6 +187,8 @@ fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, enabled: Boole
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
|
fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
|
||||||
|
val gameVersion = GamePackageUtils.getSupportedUnifiedVersion()
|
||||||
|
|
||||||
InlineDialog(
|
InlineDialog(
|
||||||
headline = {
|
headline = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -201,7 +204,7 @@ fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
|
|||||||
},
|
},
|
||||||
body = {
|
body = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.launcher_game_update_required),
|
stringResource(R.string.launcher_game_update_required, gameVersion),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = modifier.padding(8.dp)
|
modifier = modifier.padding(8.dp)
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import com.geode.launcher.utils.GamePackageUtils
|
|||||||
import com.geode.launcher.utils.GeodeUtils
|
import com.geode.launcher.utils.GeodeUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
enum class LaunchNotificationType {
|
enum class LaunchNotificationType {
|
||||||
GEODE_UPDATED, LAUNCHER_UPDATE_AVAILABLE, UPDATE_FAILED, UNSUPPORTED_VERSION, FAILED_LOAD;
|
GEODE_UPDATED, LAUNCHER_UPDATE_AVAILABLE, UPDATE_FAILED, UNSUPPORTED_VERSION, FAILED_LOAD;
|
||||||
@@ -87,7 +89,7 @@ fun NotificationCardFromType(type: LaunchNotificationType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AnimatedNotificationCard(
|
AnimatedNotificationCard(
|
||||||
displayLength = 5000L,
|
displayLength = 5.seconds,
|
||||||
onClick = {
|
onClick = {
|
||||||
showInfoDialog = true
|
showInfoDialog = true
|
||||||
}
|
}
|
||||||
@@ -140,15 +142,15 @@ fun LaunchNotification(cards: List<LaunchNotificationType>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimatedNotificationCard(modifier: Modifier = Modifier, visibilityDelay: Long = 0L, displayLength: Long = 3000L, onClick: (() -> Unit)? = null, contents: @Composable () -> Unit) {
|
fun AnimatedNotificationCard(modifier: Modifier = Modifier, visibilityDelay: Duration = Duration.ZERO, displayLength: Duration = 3.seconds, onClick: (() -> Unit)? = null, contents: @Composable () -> Unit) {
|
||||||
val state = remember {
|
val state = remember {
|
||||||
MutableTransitionState(false).apply {
|
MutableTransitionState(false).apply {
|
||||||
targetState = visibilityDelay <= 0
|
targetState = visibilityDelay <= Duration.ZERO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
if (visibilityDelay > 0) {
|
if (visibilityDelay > Duration.ZERO) {
|
||||||
delay(visibilityDelay)
|
delay(visibilityDelay)
|
||||||
state.targetState = true
|
state.targetState = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
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.BuildConfig
|
||||||
import com.geode.launcher.updater.ReleaseManager
|
import com.geode.launcher.updater.ReleaseManager
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
import com.geode.launcher.utils.GamePackageUtils
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
@@ -182,7 +183,9 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GamePackageUtils.getGameVersionCode(packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN) {
|
// play store build only comes with the one build of geode
|
||||||
|
val minVersionCode = if (BuildConfig.GOOGLE_PLAY_BUILD) Constants.SUPPORTED_VERSION_CODE else Constants.SUPPORTED_VERSION_CODE_MIN
|
||||||
|
if (GamePackageUtils.getGameVersionCode(packageManager) < minVersionCode) {
|
||||||
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
|
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -248,25 +251,31 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
initReadyTimer()
|
initReadyTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
val releaseManager = ReleaseManager.get(application)
|
if (!BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
|
val releaseManager = ReleaseManager.get(application)
|
||||||
|
|
||||||
val hasGeode = LaunchUtils.isGeodeInstalled(application)
|
val hasGeode = LaunchUtils.isGeodeInstalled(application)
|
||||||
val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
||||||
|
|
||||||
if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) {
|
if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) {
|
||||||
releaseManager.checkForUpdates(false)
|
releaseManager.checkForUpdates(false)
|
||||||
} else {
|
} else {
|
||||||
releaseManager.afterUseCachedUpdate()
|
releaseManager.afterUseCachedUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRestart) {
|
if (isRestart) {
|
||||||
releaseManager.dismissedLauncherUpdate = true
|
releaseManager.dismissedLauncherUpdate = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preReadyCheck()
|
preReadyCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retryUpdate() {
|
fun retryUpdate() {
|
||||||
|
if (BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ReleaseManager.get(application).checkForUpdates(true)
|
ReleaseManager.get(application).checkForUpdates(true)
|
||||||
if (ReleaseManager.get(application).isInUpdate) {
|
if (ReleaseManager.get(application).isInUpdate) {
|
||||||
restoreUpdateState = _uiState.value
|
restoreUpdateState = _uiState.value
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.os.Build
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -409,7 +408,8 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ElevatedCard(modifier) {
|
// there's like a single pixel of the shadow that gets clipped without this...
|
||||||
|
ElevatedCard(modifier = modifier.padding(bottom = 1.dp)) {
|
||||||
Column(
|
Column(
|
||||||
// buttons add enough padding already, lower to compensate
|
// buttons add enough padding already, lower to compensate
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ fun onSelectIcon(context: Context, selectedIcon: ApplicationIconDetails) {
|
|||||||
val iconData = IconUtils.getIconDetails(it)
|
val iconData = IconUtils.getIconDetails(it)
|
||||||
|
|
||||||
packageManager.setComponentEnabledSetting(
|
packageManager.setComponentEnabledSetting(
|
||||||
ComponentName(context, "${context.packageName}.${iconData.component}"),
|
ComponentName(context, "${context.packageName.removeSuffix(".play")}.${iconData.component}"),
|
||||||
if (iconData.id == selectedIcon.id)
|
if (iconData.id == selectedIcon.id)
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||||
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.res.pluralStringResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.geode.launcher.BuildConfig
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
import com.geode.launcher.preferences.components.OptionsButton
|
import com.geode.launcher.preferences.components.OptionsButton
|
||||||
import com.geode.launcher.preferences.components.OptionsGroup
|
import com.geode.launcher.preferences.components.OptionsGroup
|
||||||
@@ -204,28 +205,30 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
if (!BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
val context = LocalContext.current
|
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
SettingsSelectCard(
|
SettingsSelectCard(
|
||||||
title = stringResource(R.string.preference_release_channel_tag_name),
|
title = stringResource(R.string.preference_release_channel_tag_name),
|
||||||
dialogTitle = stringResource(R.string.preference_release_channel_select),
|
dialogTitle = stringResource(R.string.preference_release_channel_select),
|
||||||
preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL_TAG,
|
preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL_TAG,
|
||||||
options = linkedMapOf(
|
options = linkedMapOf(
|
||||||
0 to stringResource(R.string.preference_release_channel_stable),
|
0 to stringResource(R.string.preference_release_channel_stable),
|
||||||
1 to stringResource(R.string.preference_release_channel_beta),
|
1 to stringResource(R.string.preference_release_channel_beta),
|
||||||
2 to stringResource(R.string.preference_release_channel_nightly),
|
2 to stringResource(R.string.preference_release_channel_nightly),
|
||||||
),
|
),
|
||||||
extraSelectBehavior = {
|
extraSelectBehavior = {
|
||||||
PreferenceUtils.get(context)
|
PreferenceUtils.get(context)
|
||||||
.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, 0L)
|
.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, 0L)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = stringResource(R.string.preference_disable_update_cache),
|
title = stringResource(R.string.preference_disable_update_cache),
|
||||||
preferenceKey = PreferenceUtils.Key.DISABLE_UPDATE_CACHE
|
preferenceKey = PreferenceUtils.Key.DISABLE_UPDATE_CACHE
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.geode.launcher.BuildConfig
|
import com.geode.launcher.BuildConfig
|
||||||
import com.geode.launcher.MainActivity
|
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
import com.geode.launcher.UserDirectoryProvider
|
import com.geode.launcher.UserDirectoryProvider
|
||||||
import com.geode.launcher.preferences.components.BackupButton
|
import com.geode.launcher.preferences.components.BackupButton
|
||||||
@@ -510,6 +509,10 @@ fun GameplaySettingsGroup() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) {
|
fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) {
|
||||||
|
if (BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val updateStatus by releaseViewModel.uiState.collectAsState()
|
val updateStatus by releaseViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
OptionsGroup(stringResource(R.string.preference_category_updater)) {
|
OptionsGroup(stringResource(R.string.preference_category_updater)) {
|
||||||
@@ -661,11 +664,14 @@ fun AboutSettingsGroup(developerModeEnabled: Boolean) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
OptionsLabel(
|
OptionsLabel(
|
||||||
stringResource(R.string.preference_loader_version_name),
|
stringResource(R.string.preference_loader_version_name),
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.preference_loader_version_description,
|
R.string.preference_loader_version_description,
|
||||||
currentRelease ?: "unknown"
|
if (BuildConfig.PREBUNDLED_GEODE != null)
|
||||||
|
"${BuildConfig.PREBUNDLED_GEODE}*"
|
||||||
|
else (currentRelease ?: "unknown")
|
||||||
),
|
),
|
||||||
displayInline = true
|
displayInline = true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ package com.geode.launcher.updater
|
|||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Asset @OptIn(ExperimentalTime::class) constructor(
|
data class Asset(
|
||||||
val url: String,
|
val url: String,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -14,10 +13,11 @@ data class Asset @OptIn(ExperimentalTime::class) constructor(
|
|||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
val updatedAt: Instant,
|
val updatedAt: Instant,
|
||||||
val browserDownloadUrl: String,
|
val browserDownloadUrl: String,
|
||||||
|
val digest: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Release @OptIn(ExperimentalTime::class) constructor(
|
data class Release(
|
||||||
val url: String,
|
val url: String,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val targetCommitish: String,
|
val targetCommitish: String,
|
||||||
@@ -31,12 +31,29 @@ data class Release @OptIn(ExperimentalTime::class) constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LoaderVersion @OptIn(ExperimentalTime::class) constructor(
|
data class LoaderPlatformDownload(
|
||||||
|
val url: String,
|
||||||
|
val hash: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoaderDownload(
|
||||||
|
val win: LoaderPlatformDownload,
|
||||||
|
val mac: LoaderPlatformDownload,
|
||||||
|
val android32: LoaderPlatformDownload,
|
||||||
|
val android64: LoaderPlatformDownload,
|
||||||
|
val ios: LoaderPlatformDownload,
|
||||||
|
val resources: LoaderPlatformDownload,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoaderVersion(
|
||||||
val tag: String,
|
val tag: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
val commitHash: String,
|
val commitHash: String,
|
||||||
val prerelease: Boolean
|
val prerelease: Boolean,
|
||||||
|
val downloads: LoaderDownload,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -45,7 +62,25 @@ data class LoaderPayload<T>(
|
|||||||
val error: String
|
val error: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DownloadableAsset(val url: String, val filename: String, val size: Long? = null)
|
data class DownloadableAsset(
|
||||||
|
val url: String,
|
||||||
|
val filename: String,
|
||||||
|
val size: Long? = null,
|
||||||
|
val hash: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun mapDownload(download: Asset): DownloadableAsset {
|
||||||
|
val hash = if (download.digest?.startsWith("sha256:") == true)
|
||||||
|
download.digest.removePrefix("sha256:")
|
||||||
|
else null
|
||||||
|
|
||||||
|
return DownloadableAsset(
|
||||||
|
url = download.browserDownloadUrl,
|
||||||
|
filename = download.name,
|
||||||
|
size = download.size.toLong(),
|
||||||
|
hash = hash,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
abstract class Downloadable {
|
abstract class Downloadable {
|
||||||
/**
|
/**
|
||||||
@@ -83,7 +118,6 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
|
|||||||
return release.tagName
|
return release.tagName
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
override fun getDescriptor(): Long {
|
override fun getDescriptor(): Long {
|
||||||
return release.createdAt.epochSeconds
|
return release.createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
@@ -100,11 +134,7 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
|
|||||||
|
|
||||||
override fun getDownload(): DownloadableAsset? {
|
override fun getDownload(): DownloadableAsset? {
|
||||||
val download = getGitHubDownload() ?: return null
|
val download = getGitHubDownload() ?: return null
|
||||||
return DownloadableAsset(
|
return mapDownload(download)
|
||||||
url = download.browserDownloadUrl,
|
|
||||||
filename = download.name,
|
|
||||||
size = download.size.toLong()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResourcesDownload(): DownloadableAsset? {
|
override fun getResourcesDownload(): DownloadableAsset? {
|
||||||
@@ -112,11 +142,7 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
|
|||||||
it.name == "resources.zip"
|
it.name == "resources.zip"
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
||||||
return DownloadableAsset(
|
return mapDownload(download)
|
||||||
url = download.browserDownloadUrl,
|
|
||||||
filename = download.name,
|
|
||||||
size = download.size.toLong()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +151,6 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
|
|||||||
return release.tagName
|
return release.tagName
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
override fun getDescriptor(): Long {
|
override fun getDescriptor(): Long {
|
||||||
return release.createdAt.epochSeconds
|
return release.createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
@@ -149,11 +174,7 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
|
|||||||
|
|
||||||
override fun getDownload(): DownloadableAsset? {
|
override fun getDownload(): DownloadableAsset? {
|
||||||
val download = getGitHubDownload() ?: return null
|
val download = getGitHubDownload() ?: return null
|
||||||
return DownloadableAsset(
|
return mapDownload(download)
|
||||||
url = download.browserDownloadUrl,
|
|
||||||
filename = download.name,
|
|
||||||
size = download.size.toLong()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResourcesDownload(): DownloadableAsset? {
|
override fun getResourcesDownload(): DownloadableAsset? {
|
||||||
@@ -166,24 +187,27 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab
|
|||||||
return version.tag
|
return version.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
override fun getDescriptor(): Long {
|
override fun getDescriptor(): Long {
|
||||||
return version.createdAt.epochSeconds
|
return version.createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDownload(): DownloadableAsset {
|
override fun getDownload(): DownloadableAsset {
|
||||||
|
val data = if (LaunchUtils.is64bit) version.downloads.android64 else version.downloads.android32
|
||||||
val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
|
val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
|
||||||
return DownloadableAsset(
|
return DownloadableAsset(
|
||||||
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename",
|
url = data.url,
|
||||||
filename = filename
|
filename = filename,
|
||||||
|
hash = data.hash,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResourcesDownload(): DownloadableAsset {
|
override fun getResourcesDownload(): DownloadableAsset {
|
||||||
|
val data = version.downloads.resources
|
||||||
val filename = "resources.zip"
|
val filename = "resources.zip"
|
||||||
return DownloadableAsset(
|
return DownloadableAsset(
|
||||||
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/resources.zip",
|
url = data.url,
|
||||||
filename = filename
|
filename = filename,
|
||||||
|
hash = data.hash,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,8 @@ import okio.sink
|
|||||||
import okio.source
|
import okio.source
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InterruptedIOException
|
import java.io.InterruptedIOException
|
||||||
|
import java.io.IOException
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
@@ -75,7 +75,8 @@ class ReleaseManager private constructor(
|
|||||||
|
|
||||||
class UpdateException(reason: Reason? = null, cause: Throwable? = null) : Exception(reason?.name, cause) {
|
class UpdateException(reason: Reason? = null, cause: Throwable? = null) : Exception(reason?.name, cause) {
|
||||||
enum class Reason {
|
enum class Reason {
|
||||||
EXTERNAL_FILE_IN_USE
|
EXTERNAL_FILE_IN_USE,
|
||||||
|
HASH_VALIDATION_FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason: Reason? = reason
|
var reason: Reason? = reason
|
||||||
@@ -155,17 +156,17 @@ class ReleaseManager private constructor(
|
|||||||
val outputFile = File(outputDirectory, download.filename)
|
val outputFile = File(outputDirectory, download.filename)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DownloadUtils.downloadStream(
|
val hash = DownloadUtils.downloadFile(
|
||||||
httpClient,
|
httpClient,
|
||||||
download.url,
|
download.url,
|
||||||
onResponse = { body ->
|
outputFile
|
||||||
body.source().use { source ->
|
|
||||||
outputFile.sink().buffer().use { sink ->
|
|
||||||
sink.writeAll(source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (download.hash != null && download.hash != hash) {
|
||||||
|
Log.w("ReleaseManager", "downloadLauncherUpdate failed: found $hash but expected ${download.hash}")
|
||||||
|
outputFile.delete()
|
||||||
|
throw UpdateException(UpdateException.Reason.HASH_VALIDATION_FAILED)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return Result.failure(e)
|
return Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -183,42 +184,52 @@ class ReleaseManager private constructor(
|
|||||||
|
|
||||||
val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext)
|
val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext)
|
||||||
|
|
||||||
|
val downloadFile = getTempFile("resources.zip")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!outputDir.exists()) {
|
if (!outputDir.exists()) {
|
||||||
outputDir.mkdirs()
|
outputDir.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadUtils.downloadStream(
|
val hash = DownloadUtils.downloadFile(
|
||||||
httpClient,
|
httpClient,
|
||||||
resourceAsset.url,
|
resourceAsset.url,
|
||||||
|
downloadFile,
|
||||||
onProgress = { progress, outOf ->
|
onProgress = { progress, outOf ->
|
||||||
if (!skipStateUpdate)
|
if (!skipStateUpdate)
|
||||||
_uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize)
|
_uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize)
|
||||||
},
|
},
|
||||||
onResponse = { body ->
|
|
||||||
DownloadUtils.copyZipStreamToDirectory(
|
|
||||||
body.byteStream(),
|
|
||||||
outputDir
|
|
||||||
)
|
|
||||||
|
|
||||||
if (finalDir.exists()) {
|
|
||||||
finalDir.deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!outputDir.renameTo(finalDir)) {
|
|
||||||
println("Failed to rename temporary directory!")
|
|
||||||
DownloadUtils.copyDirectory(outputDir, finalDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (resourceAsset.hash != null && resourceAsset.hash != hash) {
|
||||||
|
Log.w("ReleaseManager", "performResourceDownload failed: found $hash but expected ${resourceAsset.hash}")
|
||||||
|
throw UpdateException(UpdateException.Reason.HASH_VALIDATION_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile.inputStream().use { zip ->
|
||||||
|
DownloadUtils.copyZipStreamToDirectory(
|
||||||
|
zip,
|
||||||
|
outputDir
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalDir.exists()) {
|
||||||
|
finalDir.deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputDir.renameTo(finalDir)) {
|
||||||
|
println("Failed to rename temporary directory!")
|
||||||
|
DownloadUtils.copyDirectory(outputDir, finalDir)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
sendError(e)
|
sendError(e)
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
val tempPathClone = File(outputPath)
|
val tempPathClone = File(outputPath)
|
||||||
runCatching {
|
try {
|
||||||
|
if (downloadFile.exists()) downloadFile.delete()
|
||||||
if (tempPathClone.exists()) tempPathClone.deleteRecursively()
|
if (tempPathClone.exists()) tempPathClone.deleteRecursively()
|
||||||
}
|
} catch (_: IOException) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -244,12 +255,14 @@ class ReleaseManager private constructor(
|
|||||||
val initialSize = if (releaseAsset.size == null && resourcesAsset?.size == null) null else releaseSize + resourcesSize
|
val initialSize = if (releaseAsset.size == null && resourcesAsset?.size == null) null else releaseSize + resourcesSize
|
||||||
_uiState.value = ReleaseManagerState.InDownload(0, initialSize)
|
_uiState.value = ReleaseManagerState.InDownload(0, initialSize)
|
||||||
|
|
||||||
val outputFile = getTempFile()
|
val outputFile = getTempFile(LaunchUtils.geodeFilename)
|
||||||
// clone the file instance as renameTo may move the original file
|
// clone the file instance as renameTo may move the original file
|
||||||
val tempFilePath = outputFile.path
|
val tempFilePath = outputFile.path
|
||||||
|
|
||||||
val geodeFile = getGeodeOutputPath()
|
val geodeFile = getGeodeOutputPath()
|
||||||
|
|
||||||
|
val downloadFile = getTempFile("geode.zip")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// tempFile should be in same path as geodeFile
|
// tempFile should be in same path as geodeFile
|
||||||
val geodeParent = geodeFile.parentFile
|
val geodeParent = geodeFile.parentFile
|
||||||
@@ -257,45 +270,53 @@ class ReleaseManager private constructor(
|
|||||||
geodeParent.mkdirs()
|
geodeParent.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadUtils.downloadStream(
|
val hash = DownloadUtils.downloadFile(
|
||||||
httpClient,
|
httpClient,
|
||||||
releaseAsset.url,
|
releaseAsset.url,
|
||||||
|
downloadFile,
|
||||||
onProgress = { progress, outOf ->
|
onProgress = { progress, outOf ->
|
||||||
if (!skipStateUpdate)
|
if (!skipStateUpdate)
|
||||||
_uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize)
|
_uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize)
|
||||||
|
|
||||||
releaseSize = max(outOf, releaseSize)
|
releaseSize = max(outOf, releaseSize)
|
||||||
},
|
|
||||||
onResponse = { body ->
|
|
||||||
DownloadUtils.extractFileFromZipStream(
|
|
||||||
body.byteStream(),
|
|
||||||
outputFile,
|
|
||||||
geodeFile.name
|
|
||||||
)
|
|
||||||
|
|
||||||
// work around a permission issue from adb push
|
|
||||||
if (geodeFile.exists()) {
|
|
||||||
geodeFile.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
val renameSuccessful = runCatching {
|
|
||||||
outputFile.renameTo(geodeFile)
|
|
||||||
}.getOrDefault(false)
|
|
||||||
|
|
||||||
if (!renameSuccessful) {
|
|
||||||
// attempt a manual copy if rename fails for whatever reason
|
|
||||||
DownloadUtils.copyFile(outputFile, geodeFile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (releaseAsset.hash != null && releaseAsset.hash != hash) {
|
||||||
|
Log.w("ReleaseManager", "performUpdate failed: found $hash but expected ${releaseAsset.hash}")
|
||||||
|
throw UpdateException(UpdateException.Reason.HASH_VALIDATION_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile.inputStream().use { zip ->
|
||||||
|
DownloadUtils.extractFileFromZipStream(
|
||||||
|
zip,
|
||||||
|
outputFile,
|
||||||
|
geodeFile.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// work around a permission issue from adb push
|
||||||
|
if (geodeFile.exists()) {
|
||||||
|
geodeFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
val renameSuccessful = runCatching {
|
||||||
|
outputFile.renameTo(geodeFile)
|
||||||
|
}.getOrDefault(false)
|
||||||
|
|
||||||
|
if (!renameSuccessful) {
|
||||||
|
// attempt a manual copy if rename fails for whatever reason
|
||||||
|
DownloadUtils.copyFile(outputFile, geodeFile)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
sendError(e)
|
sendError(e)
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
val tempFileClone = File(tempFilePath)
|
val tempFileClone = File(tempFilePath)
|
||||||
runCatching {
|
try {
|
||||||
|
if (downloadFile.exists()) downloadFile.delete()
|
||||||
if (tempFileClone.exists()) tempFileClone.delete()
|
if (tempFileClone.exists()) tempFileClone.delete()
|
||||||
}
|
} catch (_: IOException) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourcesAsset != null) {
|
if (resourcesAsset != null) {
|
||||||
@@ -331,6 +352,11 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getLatestLauncherUpdate(): DownloadableLauncherRelease? {
|
private suspend fun getLatestLauncherUpdate(): DownloadableLauncherRelease? {
|
||||||
|
if (BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
|
// don't check for updates on Google Play builds
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
releaseRepository.getLatestLauncherRelease()
|
releaseRepository.getLatestLauncherRelease()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -340,6 +366,8 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkLauncherUpdate() {
|
suspend fun checkLauncherUpdate() {
|
||||||
|
if (BuildConfig.GOOGLE_PLAY_BUILD) return
|
||||||
|
|
||||||
val launcherRelease = getLatestLauncherUpdate() ?: return
|
val launcherRelease = getLatestLauncherUpdate() ?: return
|
||||||
|
|
||||||
// unconditionally track it, we're not using it for checks and it might be good to have
|
// unconditionally track it, we're not using it for checks and it might be good to have
|
||||||
@@ -356,6 +384,8 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun checkCachedLauncherUpdate() {
|
fun checkCachedLauncherUpdate() {
|
||||||
|
if (BuildConfig.GOOGLE_PLAY_BUILD) return
|
||||||
|
|
||||||
val lastUpdate = PreferenceUtils.get(applicationContext)
|
val lastUpdate = PreferenceUtils.get(applicationContext)
|
||||||
.getString(PreferenceUtils.Key.LAST_LAUNCHER_UPDATE) ?: return
|
.getString(PreferenceUtils.Key.LAST_LAUNCHER_UPDATE) ?: return
|
||||||
|
|
||||||
@@ -365,6 +395,12 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadLatestLauncherUpdate(): Result<File> {
|
suspend fun downloadLatestLauncherUpdate(): Result<File> {
|
||||||
|
if (BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
|
return Result.failure(UnsupportedOperationException(
|
||||||
|
"Launcher updates are not supported on Google Play builds"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
val update = _availableLauncherUpdateDetails.value
|
val update = _availableLauncherUpdateDetails.value
|
||||||
?: getLatestLauncherUpdate()
|
?: getLatestLauncherUpdate()
|
||||||
?: return Result.failure(FileNotFoundException())
|
?: return Result.failure(FileNotFoundException())
|
||||||
@@ -373,6 +409,10 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun shouldUseCache(): Boolean {
|
fun shouldUseCache(): Boolean {
|
||||||
|
if (BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||||
if (sharedPreferences.getBoolean(PreferenceUtils.Key.DISABLE_UPDATE_CACHE)) {
|
if (sharedPreferences.getBoolean(PreferenceUtils.Key.DISABLE_UPDATE_CACHE)) {
|
||||||
return false
|
return false
|
||||||
@@ -391,6 +431,11 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkForNewRelease(isManual: Boolean = false) {
|
private suspend fun checkForNewRelease(isManual: Boolean = false) {
|
||||||
|
if (BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
|
_uiState.value = ReleaseManagerState.Finished()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||||
|
|
||||||
if (!isManual && shouldUseCache()) {
|
if (!isManual && shouldUseCache()) {
|
||||||
@@ -481,15 +526,14 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTempFile(): File {
|
private fun getTempFile(filename: String): File {
|
||||||
val geodeName = LaunchUtils.geodeFilename
|
|
||||||
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||||
|
|
||||||
// warning!! while File::createTempFile may look tempting, a certain brand of phones has a messed up implementation of it
|
// warning!! while File::createTempFile may look tempting, a certain brand of phones has a messed up implementation of it
|
||||||
// so we're making a temp file manually (as long as it doesn't collide with the geode download, it's okay)
|
// so we're making a temp file manually (as long as it doesn't collide with the geode download, it's okay)
|
||||||
|
|
||||||
val suffix = createRandomString()
|
val suffix = createRandomString()
|
||||||
val tmpName = "tmp-$suffix.$geodeName"
|
val tmpName = "tmp-$suffix.$filename"
|
||||||
|
|
||||||
val tempFile = File(geodeDirectory, tmpName)
|
val tempFile = File(geodeDirectory, tmpName)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +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.BuildConfig
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -89,6 +90,10 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun runReleaseCheck(isManual: Boolean = false) {
|
fun runReleaseCheck(isManual: Boolean = false) {
|
||||||
|
if (BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
hasPerformedCheck = true
|
hasPerformedCheck = true
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ import okio.BufferedSource
|
|||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
import okio.ForwardingSink
|
import okio.ForwardingSink
|
||||||
import okio.ForwardingSource
|
import okio.ForwardingSource
|
||||||
|
import okio.HashingSink
|
||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
import okio.Sink
|
import okio.Sink
|
||||||
import okio.Source
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -32,12 +34,31 @@ import java.util.zip.ZipInputStream
|
|||||||
typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit
|
typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit
|
||||||
|
|
||||||
object DownloadUtils {
|
object DownloadUtils {
|
||||||
|
suspend fun downloadFile(
|
||||||
|
httpClient: OkHttpClient,
|
||||||
|
url: String,
|
||||||
|
outputFile: File,
|
||||||
|
onProgress: ProgressCallback? = null,
|
||||||
|
): String = downloadStream(
|
||||||
|
httpClient,
|
||||||
|
url,
|
||||||
|
onProgress = onProgress
|
||||||
|
).use { body ->
|
||||||
|
val hashingSink = HashingSink.sha256(outputFile.sink().buffer())
|
||||||
|
hashingSink.use { sink ->
|
||||||
|
body.source().use { source ->
|
||||||
|
source.readAll(sink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hashingSink.hash.hex()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun downloadStream(
|
suspend fun downloadStream(
|
||||||
httpClient: OkHttpClient,
|
httpClient: OkHttpClient,
|
||||||
url: String,
|
url: String,
|
||||||
onProgress: ProgressCallback? = null,
|
onProgress: ProgressCallback? = null,
|
||||||
onResponse: suspend (ResponseBody) -> Unit,
|
): ResponseBody {
|
||||||
) {
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.build()
|
.build()
|
||||||
@@ -64,12 +85,15 @@ object DownloadUtils {
|
|||||||
val progressClient = progressClientBuilder.build()
|
val progressClient = progressClientBuilder.build()
|
||||||
|
|
||||||
val call = progressClient.newCall(request)
|
val call = progressClient.newCall(request)
|
||||||
call.executeAsync().use { response ->
|
val response = call.executeAsync()
|
||||||
when (response.code) {
|
|
||||||
200 -> onResponse(response.body)
|
val responseCode = response.code
|
||||||
else -> throw IOException("unexpected response ${response.code}")
|
if (responseCode != 200) {
|
||||||
}
|
response.close()
|
||||||
|
throw IOException("unexpected response ${response.code}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response.body
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible {
|
suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.os.Environment
|
|||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
class FileUtils {
|
class FileUtils {
|
||||||
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
|
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
|
||||||
@@ -86,7 +87,7 @@ class FileUtils {
|
|||||||
val file = File(id)
|
val file = File(id)
|
||||||
if (file.exists()) return id
|
if (file.exists()) return id
|
||||||
}
|
}
|
||||||
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
|
val contentUri = ContentUris.withAppendedId("content://downloads/public_downloads".toUri(), java.lang.Long.valueOf(id))
|
||||||
return getDataColumn(context, contentUri, null, null)
|
return getDataColumn(context, contentUri, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ object GamePackageUtils {
|
|||||||
39L to "2.206",
|
39L to "2.206",
|
||||||
40L to "2.2074", // ok
|
40L to "2.2074", // ok
|
||||||
41L to "2.2081",
|
41L to "2.2081",
|
||||||
|
42L to "2.209",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +64,10 @@ object GamePackageUtils {
|
|||||||
return gameVersionMap[versionCode] ?: getGameVersionString(packageManager)
|
return gameVersionMap[versionCode] ?: getGameVersionString(packageManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSupportedUnifiedVersion(): String {
|
||||||
|
return gameVersionMap[Constants.SUPPORTED_VERSION_CODE] ?: Constants.SUPPORTED_VERSION_STRING
|
||||||
|
}
|
||||||
|
|
||||||
fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean {
|
fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean {
|
||||||
val abi = LaunchUtils.applicationArchitecture
|
val abi = LaunchUtils.applicationArchitecture
|
||||||
val isExtracted =
|
val isExtracted =
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import androidx.core.net.toUri
|
|||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.geode.launcher.BuildConfig
|
import com.geode.launcher.BuildConfig
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
import com.geode.launcher.UserDirectoryProvider
|
|
||||||
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
|
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
|
||||||
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
|
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
|
||||||
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
|
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
|
||||||
@@ -192,10 +191,16 @@ object GeodeUtils {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun restartGame() {
|
fun restartGame() {
|
||||||
|
restartGame(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun restartGame(safeMode: Boolean) {
|
||||||
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)
|
||||||
|
mainIntent.putExtra("safe-mode", safeMode)
|
||||||
startActivity(mainIntent)
|
startActivity(mainIntent)
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
@@ -215,13 +220,15 @@ object GeodeUtils {
|
|||||||
val relativePath = pathFile.relativeTo(baseDirectory)
|
val relativePath = pathFile.relativeTo(baseDirectory)
|
||||||
val uri = DocumentsContract.buildDocumentUri(
|
val uri = DocumentsContract.buildDocumentUri(
|
||||||
"com.android.externalstorage.documents",
|
"com.android.externalstorage.documents",
|
||||||
"primary:Android/media/${context.packageName}/$relativePath"
|
if (BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
|
"primary:Android/data/${context.packageName}/$relativePath"
|
||||||
|
} else {
|
||||||
|
"primary:Android/media/${context.packageName}/$relativePath"
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Intent(Intent.ACTION_VIEW).apply {
|
Intent(Intent.ACTION_VIEW).apply {
|
||||||
setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR)
|
setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR)
|
||||||
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
@@ -409,7 +416,7 @@ object GeodeUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isGeodeUri(uri: Uri): Boolean {
|
fun isGeodeUri(uri: Uri): Boolean {
|
||||||
return "com.geode.launcher.user" == uri.authority
|
return "${BuildConfig.APPLICATION_ID}.user" == uri.authority
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal"
|
private const val INTERNAL_PERMISSION_PREFIX = "geode.permission_internal"
|
||||||
@@ -428,7 +435,7 @@ object GeodeUtils {
|
|||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
)
|
)
|
||||||
|
|
||||||
return permissions.all {
|
permissions.all {
|
||||||
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -793,7 +800,7 @@ object GeodeUtils {
|
|||||||
|
|
||||||
external fun internalKeyEvent(timestamp: Long, deviceId: Int, eventSource: Int, keyCode: Int, modifiers: Int, isDown: Boolean, repeatCount: Int)
|
external fun internalKeyEvent(timestamp: Long, deviceId: Int, eventSource: Int, keyCode: Int, modifiers: Int, isDown: Boolean, repeatCount: Int)
|
||||||
external fun internalTouchEvent(timestamp: Long, deviceId: Int, eventSource: Int, eventType: Int, ids: IntArray, xs: FloatArray, ys: FloatArray)
|
external fun internalTouchEvent(timestamp: Long, deviceId: Int, eventSource: Int, eventType: Int, ids: IntArray, xs: FloatArray, ys: FloatArray)
|
||||||
external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float)
|
external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float, mouseX: Float, mouseY: Float)
|
||||||
external fun internalJoystickEvent(timestamp: Long, deviceId: Int, eventSource: Int, leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray)
|
external fun internalJoystickEvent(timestamp: Long, deviceId: Int, eventSource: Int, leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray)
|
||||||
|
|
||||||
external fun inputDeviceAdded(deviceId: Int)
|
external fun inputDeviceAdded(deviceId: Int)
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
import kotlinx.datetime.Month
|
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.datetime.todayIn
|
|
||||||
import kotlin.time.Clock
|
|
||||||
|
|
||||||
// https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486
|
// https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486
|
||||||
@Composable
|
@Composable
|
||||||
@@ -48,6 +44,7 @@ enum class ApplicationIcon {
|
|||||||
GEODE,
|
GEODE,
|
||||||
PRIDE,
|
PRIDE,
|
||||||
TRANS,
|
TRANS,
|
||||||
|
GREEN,
|
||||||
SAPPHIRE;
|
SAPPHIRE;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -57,6 +54,7 @@ enum class ApplicationIcon {
|
|||||||
"pride" -> PRIDE
|
"pride" -> PRIDE
|
||||||
"trans" -> TRANS
|
"trans" -> TRANS
|
||||||
"sapphire" -> SAPPHIRE
|
"sapphire" -> SAPPHIRE
|
||||||
|
"green" -> GREEN
|
||||||
else -> DEFAULT
|
else -> DEFAULT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,6 +65,7 @@ enum class ApplicationIcon {
|
|||||||
DEFAULT -> "default"
|
DEFAULT -> "default"
|
||||||
TRANS -> "trans"
|
TRANS -> "trans"
|
||||||
SAPPHIRE -> "sapphire"
|
SAPPHIRE -> "sapphire"
|
||||||
|
GREEN -> "green"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,5 +108,11 @@ object IconUtils {
|
|||||||
iconId = R.mipmap.ic_launcher_sapphire,
|
iconId = R.mipmap.ic_launcher_sapphire,
|
||||||
nameId = R.string.application_icon_sapphire,
|
nameId = R.string.application_icon_sapphire,
|
||||||
)
|
)
|
||||||
|
ApplicationIcon.GREEN -> ApplicationIconDetails(
|
||||||
|
id = id.toId(),
|
||||||
|
component = "MainActivityGreen",
|
||||||
|
iconId = R.mipmap.ic_launcher_green,
|
||||||
|
nameId = R.string.application_icon_green,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.geode.launcher.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import com.geode.launcher.BuildConfig
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
@@ -41,6 +42,9 @@ object LaunchUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isGeodeInstalled(context: Context): Boolean {
|
fun isGeodeInstalled(context: Context): Boolean {
|
||||||
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
|
if (BuildConfig.PREBUNDLED_GEODE != null) return true
|
||||||
|
|
||||||
return getInstalledGeodePath(context) != null
|
return getInstalledGeodePath(context) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,10 +52,14 @@ object LaunchUtils {
|
|||||||
* Returns the directory that Geode/the game should base itself off of.
|
* Returns the directory that Geode/the game should base itself off of.
|
||||||
*/
|
*/
|
||||||
fun getBaseDirectory(context: Context, ignoreProfile: Boolean = false): File {
|
fun getBaseDirectory(context: Context, ignoreProfile: Boolean = false): File {
|
||||||
// deprecated, but seems to be the best choice of directory (i forced mat to test it)
|
val dir = if (BuildConfig.GOOGLE_PLAY_BUILD) {
|
||||||
// also, is getting the first item the correct choice here?? what do they mean
|
context.getExternalFilesDir(null)!!.parentFile!!
|
||||||
@Suppress("DEPRECATION")
|
} else {
|
||||||
val dir = context.externalMediaDirs.first()
|
// deprecated, but seems to be the best choice of directory (i forced mat to test it)
|
||||||
|
// also, is getting the first item the correct choice here?? what do they mean
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
context.externalMediaDirs.first()
|
||||||
|
}
|
||||||
|
|
||||||
// prevent having resources added to system gallery
|
// prevent having resources added to system gallery
|
||||||
// accessing this file every time the directory is read may be a little wasteful...
|
// accessing this file every time the directory is read may be a little wasteful...
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
LAST_UPDATE_CHECK_TIME,
|
LAST_UPDATE_CHECK_TIME,
|
||||||
LAST_LAUNCHER_UPDATE,
|
LAST_LAUNCHER_UPDATE,
|
||||||
DISABLE_UPDATE_CACHE,
|
DISABLE_UPDATE_CACHE,
|
||||||
|
PREBUNDLED_GEODE_MIGRATION_PERFORMED,
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||||
@@ -199,6 +200,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
Key.LAST_UPDATE_CHECK_TIME -> "PreferenceLastUpdateCheckTime"
|
Key.LAST_UPDATE_CHECK_TIME -> "PreferenceLastUpdateCheckTime"
|
||||||
Key.LAST_LAUNCHER_UPDATE -> "PreferenceLastLauncherUpdate"
|
Key.LAST_LAUNCHER_UPDATE -> "PreferenceLastLauncherUpdate"
|
||||||
Key.DISABLE_UPDATE_CACHE -> "PreferenceDisableUpdateCache"
|
Key.DISABLE_UPDATE_CACHE -> "PreferenceDisableUpdateCache"
|
||||||
|
Key.PREBUNDLED_GEODE_MIGRATION_PERFORMED -> "PreferencePrebundledGeodeMigrationPerformed"
|
||||||
// Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel"
|
// Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel"
|
||||||
// Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
|
// Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
|
||||||
// Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
|
// Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
const val MS_TO_SEC = 1000L
|
const val MS_TO_SEC = 1000L
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun useCountdownTimer(
|
fun useCountdownTimer(
|
||||||
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
|
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
|
||||||
time: Long,
|
time: Duration,
|
||||||
onCountdownFinish: () -> Unit
|
onCountdownFinish: () -> Unit
|
||||||
): Long {
|
): Long {
|
||||||
var millisUntilFinished by remember {
|
var millisUntilFinished by remember {
|
||||||
mutableLongStateOf(time)
|
mutableStateOf(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldBeCounting by remember {
|
var shouldBeCounting by remember {
|
||||||
@@ -28,9 +30,9 @@ fun useCountdownTimer(
|
|||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
if (millisUntilFinished > 0) {
|
if (millisUntilFinished > Duration.ZERO) {
|
||||||
delay(MS_TO_SEC)
|
delay(1.seconds)
|
||||||
millisUntilFinished -= MS_TO_SEC
|
millisUntilFinished -= 1.seconds
|
||||||
} else {
|
} else {
|
||||||
onCountdownFinish()
|
onCountdownFinish()
|
||||||
}
|
}
|
||||||
@@ -58,5 +60,5 @@ fun useCountdownTimer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return millisUntilFinished / MS_TO_SEC
|
return millisUntilFinished.inWholeSeconds
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,45 @@
|
|||||||
package org.cocos2dx.lib
|
package org.cocos2dx.lib
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.*
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
import android.graphics.Paint.FontMetricsInt
|
import android.graphics.Paint.FontMetricsInt
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import java.util.*
|
import java.util.LinkedList
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
private const val HORIZONTALALIGN_LEFT = 1
|
||||||
|
private const val HORIZONTALALIGN_RIGHT = 2
|
||||||
|
private const val HORIZONTALALIGN_CENTER = 3
|
||||||
|
private const val VERTICALALIGN_TOP = 1
|
||||||
|
private const val VERTICALALIGN_BOTTOM = 2
|
||||||
|
private const val VERTICALALIGN_CENTER = 3
|
||||||
|
|
||||||
@Suppress("KotlinJniMissingFunction")
|
@Suppress("KotlinJniMissingFunction")
|
||||||
object Cocos2dxBitmap {
|
object Cocos2dxBitmap {
|
||||||
private lateinit var context: WeakReference<Context>
|
private lateinit var context: WeakReference<Context>
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeInitBitmapDC(pWidth: Int, pHeight: Int, pPixels: ByteArray)
|
external fun nativeInitBitmapDC(pWidth: Int, pHeight: Int, pPixels: ByteArray)
|
||||||
|
|
||||||
fun setContext(context: Context) {
|
fun setContext(context: Context) {
|
||||||
this.context = WeakReference(context)
|
this.context = WeakReference(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
@JvmStatic
|
||||||
|
@Suppress("unused")
|
||||||
fun createTextBitmap(
|
fun createTextBitmap(
|
||||||
string: String,
|
string: String,
|
||||||
fontName: String,
|
fontName: String,
|
||||||
@@ -55,7 +69,6 @@ object Cocos2dxBitmap {
|
|||||||
1.0f
|
1.0f
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@@ -79,19 +92,18 @@ object Cocos2dxBitmap {
|
|||||||
strokeB: Float,
|
strokeB: Float,
|
||||||
strokeSize: Float
|
strokeSize: Float
|
||||||
) {
|
) {
|
||||||
val bitmapTotalHeight: Int
|
val horizontalAlignment = alignment and 0x0F
|
||||||
val horizontalAlignment = alignment and 15
|
val verticalAlignment = (alignment shr 4) and 0x0F
|
||||||
val verticalAlignment = alignment shr 4 and 15
|
|
||||||
val pString2 = refactorString(string)
|
val pString2 = refactorString(string)
|
||||||
val paint = newPaint(fontName, fontSize, horizontalAlignment)
|
val paint = newPaint(fontName, fontSize, horizontalAlignment)
|
||||||
paint.setARGB(
|
paint.setARGB(
|
||||||
MotionEvent.ACTION_MASK,
|
255,
|
||||||
(255.0 * fontTintR.toDouble()).toInt(),
|
(255.0 * fontTintR).toInt(),
|
||||||
(255.0 * fontTintG.toDouble()).toInt(),
|
(255.0 * fontTintG).toInt(),
|
||||||
(255.0 * fontTintB.toDouble()).toInt()
|
(255.0 * fontTintB).toInt()
|
||||||
)
|
)
|
||||||
val textProperty = computeTextProperty(pString2, width, height, paint)
|
val textProperty = computeTextProperty(pString2, width, height, paint)
|
||||||
bitmapTotalHeight = if (height == 0) {
|
val bitmapTotalHeight = if (height == 0) {
|
||||||
textProperty.totalHeight
|
textProperty.totalHeight
|
||||||
} else {
|
} else {
|
||||||
height
|
height
|
||||||
@@ -101,7 +113,8 @@ object Cocos2dxBitmap {
|
|||||||
var renderTextDeltaX = 0.0f
|
var renderTextDeltaX = 0.0f
|
||||||
var renderTextDeltaY = 0.0f
|
var renderTextDeltaY = 0.0f
|
||||||
if (shadow) {
|
if (shadow) {
|
||||||
paint.setShadowLayer(shadowBlur, shadowDX, shadowDY, -8553091)
|
val shadowColor = 0xff7d7d7du.toInt()
|
||||||
|
paint.setShadowLayer(shadowBlur, shadowDX, shadowDY, shadowColor)
|
||||||
bitmapPaddingX = abs(shadowDX)
|
bitmapPaddingX = abs(shadowDX)
|
||||||
bitmapPaddingY = abs(shadowDY)
|
bitmapPaddingY = abs(shadowDY)
|
||||||
if (shadowDX.toDouble() < 0.0) {
|
if (shadowDX.toDouble() < 0.0) {
|
||||||
@@ -111,56 +124,47 @@ object Cocos2dxBitmap {
|
|||||||
renderTextDeltaY = bitmapPaddingY
|
renderTextDeltaY = bitmapPaddingY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val bitmap = Bitmap.createBitmap(
|
|
||||||
textProperty.maxWidth + bitmapPaddingX.toInt(),
|
val bitmap = try {
|
||||||
bitmapPaddingY.toInt() + bitmapTotalHeight,
|
createBitmap(
|
||||||
Bitmap.Config.ARGB_8888
|
textProperty.maxWidth + bitmapPaddingX.toInt(),
|
||||||
)
|
bitmapTotalHeight + bitmapPaddingY.toInt()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// bandaid fix, hopefully someone checks logs to see this
|
||||||
|
Log.e("Cocos2dxBitmap", "Error creating bitmap: $e")
|
||||||
|
nativeInitBitmapDC(0, 0, ByteArray(0))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val canvas = Canvas(bitmap)
|
val canvas = Canvas(bitmap)
|
||||||
val fontMetricsInt = paint.fontMetricsInt
|
val fontMetricsInt = paint.fontMetricsInt
|
||||||
var y = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment)
|
var y = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment)
|
||||||
val lines = textProperty.lines
|
val lines = textProperty.lines
|
||||||
val length = lines.size
|
|
||||||
for (i in 0 until length) {
|
for (line in lines) {
|
||||||
val line = lines[i]
|
val x = computeX(line, textProperty.maxWidth, horizontalAlignment)
|
||||||
canvas.drawText(
|
canvas.drawText(line, x + renderTextDeltaX, y + renderTextDeltaY, paint)
|
||||||
line!!,
|
|
||||||
computeX(
|
|
||||||
line,
|
|
||||||
textProperty.maxWidth,
|
|
||||||
horizontalAlignment
|
|
||||||
).toFloat() + renderTextDeltaX,
|
|
||||||
y.toFloat() + renderTextDeltaY,
|
|
||||||
paint
|
|
||||||
)
|
|
||||||
y += textProperty.heightPerLine
|
y += textProperty.heightPerLine
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stroke) {
|
if (stroke) {
|
||||||
val paintStroke = newPaint(fontName, fontSize, horizontalAlignment)
|
val paintStroke = newPaint(fontName, fontSize, horizontalAlignment)
|
||||||
paintStroke.style = Paint.Style.STROKE
|
paintStroke.style = Paint.Style.STROKE
|
||||||
paintStroke.strokeWidth = 0.5f * strokeSize
|
paintStroke.strokeWidth = strokeSize * 0.5f
|
||||||
paintStroke.setARGB(
|
paintStroke.setARGB(
|
||||||
MotionEvent.ACTION_MASK,
|
255,
|
||||||
strokeR.toInt() * MotionEvent.ACTION_MASK,
|
strokeR.toInt() * MotionEvent.ACTION_MASK,
|
||||||
strokeG.toInt() * MotionEvent.ACTION_MASK,
|
strokeG.toInt() * MotionEvent.ACTION_MASK,
|
||||||
strokeB.toInt() * MotionEvent.ACTION_MASK
|
strokeB.toInt() * MotionEvent.ACTION_MASK
|
||||||
)
|
)
|
||||||
var y2 = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment)
|
var y = computeY(fontMetricsInt, height, textProperty.totalHeight, verticalAlignment)
|
||||||
val lines2 = textProperty.lines
|
val lines = textProperty.lines
|
||||||
val length2 = lines2.size
|
|
||||||
for (i2 in 0 until length2) {
|
for (line in lines) {
|
||||||
val line2 = lines2[i2]
|
val x = computeX(line, textProperty.maxWidth, horizontalAlignment)
|
||||||
canvas.drawText(
|
canvas.drawText(line, x + renderTextDeltaX, y + renderTextDeltaY, paintStroke)
|
||||||
line2!!,
|
y += textProperty.heightPerLine
|
||||||
computeX(
|
|
||||||
line2,
|
|
||||||
textProperty.maxWidth,
|
|
||||||
horizontalAlignment
|
|
||||||
).toFloat() + renderTextDeltaX,
|
|
||||||
y2.toFloat() + renderTextDeltaY,
|
|
||||||
paintStroke
|
|
||||||
)
|
|
||||||
y2 += textProperty.heightPerLine
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initNativeObject(bitmap)
|
initNativeObject(bitmap)
|
||||||
@@ -168,24 +172,24 @@ object Cocos2dxBitmap {
|
|||||||
|
|
||||||
private fun newPaint(fontName: String, fontSize: Int, horizontalAlignment: Int): Paint {
|
private fun newPaint(fontName: String, fontSize: Int, horizontalAlignment: Int): Paint {
|
||||||
val paint = Paint()
|
val paint = Paint()
|
||||||
paint.color = -1
|
paint.color = Color.WHITE
|
||||||
paint.textSize = fontSize.toFloat()
|
paint.textSize = fontSize.toFloat()
|
||||||
paint.isAntiAlias = true
|
paint.isAntiAlias = true
|
||||||
if (fontName.endsWith(".ttf")) {
|
if (fontName.endsWith(".ttf")) {
|
||||||
try {
|
try {
|
||||||
context.get()?.apply {
|
context.get()!!.apply {
|
||||||
paint.typeface = Cocos2dxTypefaces[this, fontName]
|
paint.typeface = Cocos2dxTypefaces[this, fontName]
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Cocos2dxBitmap", "error to create ttf type face: $fontName")
|
Log.e("Cocos2dxBitmap", "error to create ttf type face: $fontName - $e")
|
||||||
paint.typeface = Typeface.create(fontName, Typeface.NORMAL)
|
paint.typeface = Typeface.create(fontName, Typeface.NORMAL)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
paint.typeface = Typeface.create(fontName, Typeface.NORMAL)
|
paint.typeface = Typeface.create(fontName, Typeface.NORMAL)
|
||||||
}
|
}
|
||||||
when (horizontalAlignment) {
|
when (horizontalAlignment) {
|
||||||
2 -> paint.textAlign = Paint.Align.RIGHT
|
HORIZONTALALIGN_RIGHT -> paint.textAlign = Paint.Align.RIGHT
|
||||||
3 -> paint.textAlign = Paint.Align.CENTER
|
HORIZONTALALIGN_CENTER -> paint.textAlign = Paint.Align.CENTER
|
||||||
else -> paint.textAlign = Paint.Align.LEFT
|
else -> paint.textAlign = Paint.Align.LEFT
|
||||||
}
|
}
|
||||||
return paint
|
return paint
|
||||||
@@ -198,14 +202,14 @@ object Cocos2dxBitmap {
|
|||||||
paint: Paint
|
paint: Paint
|
||||||
): TextProperty {
|
): TextProperty {
|
||||||
val fm = paint.fontMetricsInt
|
val fm = paint.fontMetricsInt
|
||||||
val h = ceil((fm.bottom - fm.top).toDouble()).toInt()
|
val h = fm.bottom - fm.top
|
||||||
var maxContentWidth = 0
|
var maxContentWidth = 0
|
||||||
val lines = splitString(string, width, height, paint)
|
val lines = splitString(string, width, height, paint)
|
||||||
if (width != 0) {
|
if (width != 0) {
|
||||||
maxContentWidth = width
|
maxContentWidth = width
|
||||||
} else {
|
} else {
|
||||||
for (line in lines) {
|
for (line in lines) {
|
||||||
val temp = ceil(paint.measureText(line, 0, line!!.length).toDouble()).toInt()
|
val temp = ceil(paint.measureText(line, 0, line.length)).toInt()
|
||||||
if (temp > maxContentWidth) {
|
if (temp > maxContentWidth) {
|
||||||
maxContentWidth = temp
|
maxContentWidth = temp
|
||||||
}
|
}
|
||||||
@@ -214,10 +218,10 @@ object Cocos2dxBitmap {
|
|||||||
return TextProperty(maxContentWidth, h, lines)
|
return TextProperty(maxContentWidth, h, lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun computeX(@Suppress("UNUSED_PARAMETER") text: String?, maxWidth: Int, horizontalAlignment: Int): Int {
|
private fun computeX(@Suppress("UNUSED_PARAMETER") text: String, maxWidth: Int, horizontalAlignment: Int): Int {
|
||||||
return when (horizontalAlignment) {
|
return when (horizontalAlignment) {
|
||||||
2 -> maxWidth
|
HORIZONTALALIGN_RIGHT -> maxWidth
|
||||||
3 -> maxWidth / 2
|
HORIZONTALALIGN_CENTER -> maxWidth / 2
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,13 +233,15 @@ object Cocos2dxBitmap {
|
|||||||
verticalAlignment: Int
|
verticalAlignment: Int
|
||||||
): Int {
|
): Int {
|
||||||
val y = -fontMetricsInt.top
|
val y = -fontMetricsInt.top
|
||||||
return if (constrainHeight <= totalHeight) {
|
return if (constrainHeight > totalHeight) {
|
||||||
|
when (verticalAlignment) {
|
||||||
|
VERTICALALIGN_TOP -> -fontMetricsInt.top
|
||||||
|
VERTICALALIGN_CENTER -> -fontMetricsInt.top + (constrainHeight - totalHeight) / 2
|
||||||
|
VERTICALALIGN_BOTTOM -> -fontMetricsInt.top + (constrainHeight - totalHeight)
|
||||||
|
else -> y
|
||||||
|
}
|
||||||
|
} else {
|
||||||
y
|
y
|
||||||
} else when (verticalAlignment) {
|
|
||||||
1 -> -fontMetricsInt.top
|
|
||||||
2 -> -fontMetricsInt.top + (constrainHeight - totalHeight)
|
|
||||||
3 -> -fontMetricsInt.top + (constrainHeight - totalHeight) / 2
|
|
||||||
else -> y
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,56 +250,56 @@ object Cocos2dxBitmap {
|
|||||||
maxWidth: Int,
|
maxWidth: Int,
|
||||||
maxHeight: Int,
|
maxHeight: Int,
|
||||||
paint: Paint
|
paint: Paint
|
||||||
): Array<String?> {
|
): List<String> {
|
||||||
val lines: Array<String?> = string.split("\\n").toTypedArray()
|
val lines = string.split("\n")
|
||||||
val fm = paint.fontMetricsInt
|
val fm = paint.fontMetricsInt
|
||||||
val maxLines = maxHeight / ceil((fm.bottom - fm.top).toDouble()).toInt()
|
val heightPerLine = fm.bottom - fm.top
|
||||||
|
val maxLines = maxHeight / heightPerLine
|
||||||
return if (maxWidth != 0) {
|
return if (maxWidth != 0) {
|
||||||
val strList = LinkedList<String?>()
|
val strList = LinkedList<String>()
|
||||||
for (line in lines) {
|
for (line in lines) {
|
||||||
if (ceil(paint.measureText(line).toDouble()).toInt() > maxWidth) {
|
val lineWidth = ceil(paint.measureText(line)).toInt()
|
||||||
|
|
||||||
|
if (lineWidth > maxWidth) {
|
||||||
strList.addAll(divideStringWithMaxWidth(line, maxWidth, paint))
|
strList.addAll(divideStringWithMaxWidth(line, maxWidth, paint))
|
||||||
} else {
|
} else {
|
||||||
strList.add(line)
|
strList.add(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxLines > 0 && strList.size >= maxLines) {
|
if (maxLines > 0 && strList.size >= maxLines) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxLines > 0 && strList.size > maxLines) {
|
if (maxLines > 0 && strList.size > maxLines) {
|
||||||
while (strList.size > maxLines) {
|
while (strList.size > maxLines) {
|
||||||
strList.removeLast()
|
strList.removeLast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val ret = arrayOfNulls<String>(strList.size)
|
|
||||||
strList.toArray(ret)
|
strList
|
||||||
ret
|
} else if (maxHeight != 0 && lines.size > maxLines) {
|
||||||
} else if (maxHeight == 0 || lines.size <= maxLines) {
|
lines.take(maxLines)
|
||||||
lines
|
|
||||||
} else {
|
} else {
|
||||||
val strList2 = LinkedList<String?>()
|
lines
|
||||||
for (i in 0 until maxLines) {
|
|
||||||
strList2.add(lines[i])
|
|
||||||
}
|
|
||||||
val ret2 = arrayOfNulls<String>(strList2.size)
|
|
||||||
strList2.toArray(ret2)
|
|
||||||
ret2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun divideStringWithMaxWidth(
|
private fun divideStringWithMaxWidth(
|
||||||
string: String?,
|
string: String,
|
||||||
maxWidth: Int,
|
maxWidth: Int,
|
||||||
paint: Paint
|
paint: Paint
|
||||||
): LinkedList<String?> {
|
): LinkedList<String> {
|
||||||
val charLength = string!!.length
|
val charLength = string.length
|
||||||
var start = 0
|
var start = 0
|
||||||
val strList = LinkedList<String?>()
|
val strList = LinkedList<String>()
|
||||||
|
|
||||||
var i = 1
|
var i = 1
|
||||||
while (i <= charLength) {
|
while (i <= charLength) {
|
||||||
val tempWidth = ceil(paint.measureText(string, start, i).toDouble()).toInt()
|
val tempWidth = ceil(paint.measureText(string, start, i)).toInt()
|
||||||
if (tempWidth >= maxWidth) {
|
if (tempWidth >= maxWidth) {
|
||||||
val lastIndexOfSpace = string.substring(0, i).lastIndexOf(" ")
|
val lastIndexOfSpace = string.substring(0, i).lastIndexOf(" ")
|
||||||
|
|
||||||
if (lastIndexOfSpace != -1 && lastIndexOfSpace > start) {
|
if (lastIndexOfSpace != -1 && lastIndexOfSpace > start) {
|
||||||
strList.add(string.substring(start, lastIndexOfSpace))
|
strList.add(string.substring(start, lastIndexOfSpace))
|
||||||
i = lastIndexOfSpace + 1
|
i = lastIndexOfSpace + 1
|
||||||
@@ -303,16 +309,20 @@ object Cocos2dxBitmap {
|
|||||||
} else {
|
} else {
|
||||||
strList.add(string.substring(start, i))
|
strList.add(string.substring(start, i))
|
||||||
}
|
}
|
||||||
|
|
||||||
while (i < charLength && string[i] == ' ') {
|
while (i < charLength && string[i] == ' ') {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
start = i
|
start = i
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start < charLength) {
|
if (start < charLength) {
|
||||||
strList.add(string.substring(start))
|
strList.add(string.substring(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
return strList
|
return strList
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,19 +330,22 @@ object Cocos2dxBitmap {
|
|||||||
if (pString.compareTo("") == 0) {
|
if (pString.compareTo("") == 0) {
|
||||||
return " "
|
return " "
|
||||||
}
|
}
|
||||||
|
|
||||||
val strBuilder = StringBuilder(pString)
|
val strBuilder = StringBuilder(pString)
|
||||||
var start = 0
|
var start = 0
|
||||||
var index = strBuilder.indexOf("\n")
|
var index = strBuilder.indexOf("\n")
|
||||||
while (index != -1) {
|
while (index != -1) {
|
||||||
start = if (index == 0 || strBuilder[index - 1].code == 10) {
|
start = if (index == 0 || strBuilder[index - 1] == '\n') {
|
||||||
strBuilder.insert(start, " ")
|
strBuilder.insert(start, " ")
|
||||||
index + 2
|
index + 2
|
||||||
} else {
|
} else {
|
||||||
index + 1
|
index + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start > strBuilder.length || index == strBuilder.length) {
|
if (start > strBuilder.length || index == strBuilder.length) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
index = strBuilder.indexOf("\n", start)
|
index = strBuilder.indexOf("\n", start)
|
||||||
}
|
}
|
||||||
return strBuilder.toString()
|
return strBuilder.toString()
|
||||||
@@ -340,15 +353,10 @@ object Cocos2dxBitmap {
|
|||||||
|
|
||||||
private fun initNativeObject(pBitmap: Bitmap) {
|
private fun initNativeObject(pBitmap: Bitmap) {
|
||||||
val pixels = getPixels(pBitmap)
|
val pixels = getPixels(pBitmap)
|
||||||
if (pixels != null) {
|
nativeInitBitmapDC(pBitmap.width, pBitmap.height, pixels)
|
||||||
nativeInitBitmapDC(pBitmap.width, pBitmap.height, pixels)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPixels(pBitmap: Bitmap?): ByteArray? {
|
private fun getPixels(pBitmap: Bitmap): ByteArray {
|
||||||
if (pBitmap == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val pixels = ByteArray(pBitmap.width * pBitmap.height * 4)
|
val pixels = ByteArray(pBitmap.width * pBitmap.height * 4)
|
||||||
val buf = ByteBuffer.wrap(pixels)
|
val buf = ByteBuffer.wrap(pixels)
|
||||||
buf.order(ByteOrder.nativeOrder())
|
buf.order(ByteOrder.nativeOrder())
|
||||||
@@ -361,13 +369,18 @@ object Cocos2dxBitmap {
|
|||||||
private fun getFontSizeAccordingHeight(height: Int): Int {
|
private fun getFontSizeAccordingHeight(height: Int): Int {
|
||||||
val paint = Paint()
|
val paint = Paint()
|
||||||
val bounds = Rect()
|
val bounds = Rect()
|
||||||
|
|
||||||
paint.typeface = Typeface.DEFAULT
|
paint.typeface = Typeface.DEFAULT
|
||||||
var incrTextSize = 1
|
var incrTextSize = 1
|
||||||
var foundDesiredSize = false
|
var foundDesiredSize = false
|
||||||
|
|
||||||
while (!foundDesiredSize) {
|
while (!foundDesiredSize) {
|
||||||
paint.textSize = incrTextSize.toFloat()
|
paint.textSize = incrTextSize.toFloat()
|
||||||
paint.getTextBounds("SghMNy", 0, "SghMNy".length, bounds)
|
val text = "SghMNy"
|
||||||
|
paint.getTextBounds(text, 0, text.length, bounds)
|
||||||
|
|
||||||
incrTextSize++
|
incrTextSize++
|
||||||
|
|
||||||
if (height - bounds.height() <= 2) {
|
if (height - bounds.height() <= 2) {
|
||||||
foundDesiredSize = true
|
foundDesiredSize = true
|
||||||
}
|
}
|
||||||
@@ -382,23 +395,19 @@ object Cocos2dxBitmap {
|
|||||||
if (TextUtils.isEmpty(string)) {
|
if (TextUtils.isEmpty(string)) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
val paint = TextPaint()
|
val paint = TextPaint()
|
||||||
paint.typeface = Typeface.DEFAULT
|
paint.typeface = Typeface.DEFAULT
|
||||||
paint.textSize = fontSize
|
paint.textSize = fontSize
|
||||||
|
|
||||||
return TextUtils.ellipsize(string, paint, width, TextUtils.TruncateAt.END).toString()
|
return TextUtils.ellipsize(string, paint, width, TextUtils.TruncateAt.END).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TextProperty constructor(
|
private class TextProperty(
|
||||||
val maxWidth: Int,
|
val maxWidth: Int,
|
||||||
val heightPerLine: Int, pLines: Array<String?>
|
val heightPerLine: Int,
|
||||||
|
val lines: Collection<String>
|
||||||
) {
|
) {
|
||||||
val lines: Array<String?>
|
val totalHeight: Int = lines.size * heightPerLine
|
||||||
|
|
||||||
val totalHeight: Int
|
|
||||||
|
|
||||||
init {
|
|
||||||
totalHeight = pLines.size * heightPerLine
|
|
||||||
lines = pLines
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,13 @@ import android.os.Build
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Message
|
import android.os.Message
|
||||||
import android.util.Log
|
import android.text.InputType
|
||||||
import android.view.InputDevice
|
import android.view.InputDevice
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
@@ -25,12 +27,54 @@ private const val HANDLER_CLOSE_IME_KEYBOARD = 3
|
|||||||
private const val MS_TO_NS = 1_000_000
|
private const val MS_TO_NS = 1_000_000
|
||||||
private const val SEC_TO_NS = 1_000_000_000.0
|
private const val SEC_TO_NS = 1_000_000_000.0
|
||||||
|
|
||||||
|
fun inputTypeForFlags(i: Int): Int {
|
||||||
|
if ((i and BaseRobTopActivity.KEYBOARD_INPUT_NUMERIC) != 0) {
|
||||||
|
return InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED or InputType.TYPE_CLASS_NUMBER
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((i and BaseRobTopActivity.KEYBOARD_INPUT_PASSWORD) != 0) {
|
||||||
|
return InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((i and BaseRobTopActivity.KEYBOARD_INPUT_MULTILINE) != 0) {
|
||||||
|
return InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_CLASS_TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
return InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_CLASS_TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun usesInlineKeyboard(): Boolean {
|
||||||
|
return Build.VERSION.SDK_INT >= 30
|
||||||
|
}
|
||||||
|
|
||||||
|
fun imeOptionsForFlags(i: Int): Int {
|
||||||
|
val i2 = if ((i and BaseRobTopActivity.KEYBOARD_INPUT_MULTILINE) == 0 || !usesInlineKeyboard())
|
||||||
|
EditorInfo.IME_ACTION_DONE else EditorInfo.IME_ACTION_NONE
|
||||||
|
|
||||||
|
return if (usesInlineKeyboard())
|
||||||
|
EditorInfo.IME_FLAG_NO_FULLSCREEN or EditorInfo.IME_FLAG_NO_EXTRACT_UI or i2
|
||||||
|
else i2
|
||||||
|
}
|
||||||
|
|
||||||
class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView
|
lateinit var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView
|
||||||
private lateinit var handler: Handler
|
private lateinit var handler: Handler
|
||||||
private lateinit var cocos2dxTextInputWrapper: Cocos2dxTextInputWrapper
|
private lateinit var cocos2dxTextInputWrapper: Cocos2dxTextInputWrapper
|
||||||
|
|
||||||
|
private var keyboardVisible = false
|
||||||
|
private var keyboardHeight = 0
|
||||||
|
|
||||||
|
private var keyboardHideCheck = Runnable {
|
||||||
|
if (keyboardVisible) {
|
||||||
|
return@Runnable
|
||||||
|
}
|
||||||
|
|
||||||
|
cocos2dxGLSurfaceView.queueEvent {
|
||||||
|
cocos2dxGLSurfaceView.cocos2dxRenderer.handleTextClosed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun openIMEKeyboard() {
|
fun openIMEKeyboard() {
|
||||||
val msg = Message()
|
val msg = Message()
|
||||||
@@ -50,6 +94,30 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
cocos2dxGLSurfaceView.queueEvent {
|
cocos2dxGLSurfaceView.queueEvent {
|
||||||
Cocos2dxAccelerometer.onSensorChanged(x, y, z, timestamp)
|
Cocos2dxAccelerometer.onSensorChanged(x, y, z, timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
fun onKeyboardInsets(windowInsets: WindowInsets) {
|
||||||
|
if (!cocos2dxGLSurfaceView.sendNewCocosEvents) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val visible = windowInsets.isVisible(WindowInsets.Type.ime())
|
||||||
|
val offsetHeight = if (visible) windowInsets.getInsets(WindowInsets.Type.ime()).bottom else 0
|
||||||
|
|
||||||
|
if (visible == keyboardVisible && offsetHeight == keyboardHeight) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardVisible = visible
|
||||||
|
keyboardHeight = offsetHeight
|
||||||
|
|
||||||
|
handler.removeCallbacks(keyboardHideCheck)
|
||||||
|
if (!visible) {
|
||||||
|
handler.postDelayed(keyboardHideCheck, 150L)
|
||||||
|
} else {
|
||||||
|
cocos2dxGLSurfaceView.cocos2dxRenderer.handleKeyboardWillShow(keyboardHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var cocos2dxRenderer: Cocos2dxRenderer
|
private lateinit var cocos2dxRenderer: Cocos2dxRenderer
|
||||||
@@ -65,6 +133,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
private set
|
private set
|
||||||
var supportsResizeEvents = false
|
var supportsResizeEvents = false
|
||||||
|
|
||||||
|
// 2.209+ events for keyboard and such
|
||||||
|
var sendNewCocosEvents = false
|
||||||
|
|
||||||
var cocos2dxEditText: AppCompatEditText? = null
|
var cocos2dxEditText: AppCompatEditText? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -85,9 +156,13 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
HANDLER_OPEN_IME_KEYBOARD -> {
|
HANDLER_OPEN_IME_KEYBOARD -> {
|
||||||
if (cocos2dxEditText?.requestFocus() == true) {
|
if (cocos2dxEditText?.requestFocus() == true) {
|
||||||
cocos2dxEditText?.apply {
|
cocos2dxEditText?.apply {
|
||||||
removeTextChangedListener(
|
removeTextChangedListener(cocos2dxTextInputWrapper)
|
||||||
cocos2dxTextInputWrapper
|
|
||||||
)
|
if (sendNewCocosEvents) {
|
||||||
|
inputType = inputTypeForFlags(BaseRobTopActivity.curKeyboardInputFlags)
|
||||||
|
imeOptions = imeOptionsForFlags(BaseRobTopActivity.curKeyboardInputFlags)
|
||||||
|
}
|
||||||
|
|
||||||
setText("")
|
setText("")
|
||||||
val text = msg.obj as String
|
val text = msg.obj as String
|
||||||
append(text)
|
append(text)
|
||||||
@@ -100,16 +175,13 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
cocos2dxEditText,
|
cocos2dxEditText,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
Log.d("GLSurfaceView", "showSoftInput")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
HANDLER_CLOSE_IME_KEYBOARD -> {
|
HANDLER_CLOSE_IME_KEYBOARD -> {
|
||||||
cocos2dxEditText?.apply {
|
cocos2dxEditText?.apply {
|
||||||
removeTextChangedListener(
|
removeTextChangedListener(cocos2dxTextInputWrapper)
|
||||||
cocos2dxTextInputWrapper
|
|
||||||
)
|
|
||||||
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(
|
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(
|
||||||
windowToken,
|
windowToken,
|
||||||
0
|
0
|
||||||
@@ -117,8 +189,10 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestFocus()
|
requestFocus()
|
||||||
Log.d("GLSurfaceView", "HideSoftInput")
|
|
||||||
cocos2dxRenderer.handleTextClosed()
|
queueEvent {
|
||||||
|
cocos2dxRenderer.handleTextClosed()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
else -> return
|
else -> return
|
||||||
@@ -297,6 +371,97 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun queueKeyboardEvent(keyCode: Int, event: KeyEvent, down: Boolean): Boolean {
|
||||||
|
val keypadCode = keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_BACK
|
||||||
|
if (event.isSystem && !keypadCode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keypadCode && BaseRobTopActivity.blockBackButton) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
queueEvent {
|
||||||
|
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useKeyboardEvents) {
|
||||||
|
if (down) {
|
||||||
|
queueEvent {
|
||||||
|
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queueEvent {
|
||||||
|
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllerEventsEnabled) {
|
||||||
|
queueEvent {
|
||||||
|
GeodeUtils.internalKeyEvent(
|
||||||
|
event.eventTime * MS_TO_NS,
|
||||||
|
event.deviceId,
|
||||||
|
event.source,
|
||||||
|
event.keyCode,
|
||||||
|
event.modifiers,
|
||||||
|
down,
|
||||||
|
event.repeatCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// geode events override native callbacks
|
||||||
|
if (useKeyboardEvents || controllerEventsEnabled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendNewCocosEvents) {
|
||||||
|
if (KeyEvent.isGamepadButton(keyCode)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_ESCAPE && BaseRobTopActivity.blockBackButton) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// messy logic
|
||||||
|
val isRepeat = event.repeatCount > 0
|
||||||
|
if (keypadCode) {
|
||||||
|
if (down) {
|
||||||
|
if (isRepeat || BaseRobTopActivity.blockBackButton) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
queueEvent {
|
||||||
|
cocos2dxRenderer.handleKeyDown(keyCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventTimestamp = event.eventTime / 1000.0
|
||||||
|
|
||||||
|
val shiftPressed = event.isShiftPressed
|
||||||
|
val ctrlPressed = event.isCtrlPressed
|
||||||
|
val altPressed = event.isAltPressed
|
||||||
|
|
||||||
|
queueEvent {
|
||||||
|
cocos2dxRenderer.handleKeyboardEvent(
|
||||||
|
keyCode, down, isRepeat, eventTimestamp,
|
||||||
|
shiftPressed, ctrlPressed, altPressed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun legacyKeyDown(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 -> {
|
||||||
@@ -354,65 +519,19 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
if (!queueKeyboardEvent(keyCode, event, true)) {
|
||||||
return legacyKeyDown(keyCode, event)
|
return legacyKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (keyCode) {
|
return true
|
||||||
// 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 {
|
|
||||||
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
|
||||||
|
|
||||||
if (useKeyboardEvents) {
|
|
||||||
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controllerEventsEnabled) {
|
|
||||||
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, true, event.repeatCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
if (!queueKeyboardEvent(keyCode, event, false)) {
|
||||||
return super.onKeyUp(keyCode, event)
|
return super.onKeyUp(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (keyCode) {
|
return true
|
||||||
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 {
|
|
||||||
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
|
||||||
|
|
||||||
if (useKeyboardEvents) {
|
|
||||||
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controllerEventsEnabled) {
|
|
||||||
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, false, event.repeatCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// i'm lazy.
|
// i'm lazy.
|
||||||
@@ -475,11 +594,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
|
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
|
||||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
val hasGeodeEvents = useKeyboardEvents || controllerEventsEnabled
|
||||||
|
if (!hasGeodeEvents && !sendNewCocosEvents) {
|
||||||
return super.onGenericMotionEvent(event)
|
return super.onGenericMotionEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event?.action == MotionEvent.ACTION_SCROLL) {
|
if (event?.action == MotionEvent.ACTION_SCROLL) {
|
||||||
|
val mouseX = event.x
|
||||||
|
val mouseY = event.y
|
||||||
|
|
||||||
val scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL)
|
val scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL)
|
||||||
val scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
|
val scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
|
||||||
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
@@ -493,7 +616,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (controllerEventsEnabled) {
|
if (controllerEventsEnabled) {
|
||||||
GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY)
|
GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY, mouseX, mouseY)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasGeodeEvents && sendNewCocosEvents) {
|
||||||
|
cocos2dxRenderer.handleScroll(mouseX, mouseY, scrollY, scrollX)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,29 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeKeyDown(keyCode: Int): Boolean
|
private external fun nativeKeyDown(keyCode: Int): Boolean
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun nativeKeyboardEvent(
|
||||||
|
keyCode: Int,
|
||||||
|
down: Boolean,
|
||||||
|
isRepeat: Boolean,
|
||||||
|
timestamp: Double,
|
||||||
|
shiftPressed: Boolean,
|
||||||
|
ctrlPressed: Boolean,
|
||||||
|
altPressed: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun nativeKeyboardWillShow(offset: Int)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeOnPause()
|
private external fun nativeOnPause()
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeOnResume()
|
private external fun nativeOnResume()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun nativeScroll(x: Float, y: Float, scrollY: Float, scrollX: Float)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeRender()
|
private external fun nativeRender()
|
||||||
|
|
||||||
@@ -133,6 +150,18 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
|
|||||||
nativeKeyDown(keyCode)
|
nativeKeyDown(keyCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleKeyboardWillShow(offset: Int) {
|
||||||
|
nativeKeyboardWillShow(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleKeyboardEvent(keyCode: Int, down: Boolean, isRepeat: Boolean, timestamp: Double, shiftPressed: Boolean, ctrlPressed: Boolean, altPressed: Boolean) {
|
||||||
|
nativeKeyboardEvent(keyCode, down, isRepeat, timestamp, shiftPressed, ctrlPressed, altPressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleScroll(x: Float, y: Float, scrollY: Float, scrollX: Float) {
|
||||||
|
nativeScroll(x, y, scrollY, scrollX)
|
||||||
|
}
|
||||||
|
|
||||||
fun handleOnPause() {
|
fun handleOnPause() {
|
||||||
nativeOnPause()
|
nativeOnPause()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import android.text.TextWatcher
|
|||||||
import android.widget.TextView.OnEditorActionListener
|
import android.widget.TextView.OnEditorActionListener
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import com.customRobTop.JniToCpp
|
||||||
|
|
||||||
class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView) : TextWatcher, OnEditorActionListener {
|
class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView) : TextWatcher, OnEditorActionListener {
|
||||||
private var originText: String? = null
|
private var originText: String? = null
|
||||||
@@ -19,21 +21,33 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
|
|||||||
this.originText = originText
|
this.originText = originText
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun afterTextChanged(s: Editable) {
|
override fun afterTextChanged(editable: Editable) {
|
||||||
if (!isFullScreenEdit) {
|
if (isFullScreenEdit) {
|
||||||
var nModified = s.length - text!!.length
|
return
|
||||||
if (nModified > 0) {
|
|
||||||
cocos2dxGLSurfaceView.insertText(
|
|
||||||
s.subSequence(text!!.length, s.length).toString()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
while (nModified < 0) {
|
|
||||||
cocos2dxGLSurfaceView.deleteBackward()
|
|
||||||
nModified++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text = s.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cText = text ?: return
|
||||||
|
|
||||||
|
val str = editable.toString()
|
||||||
|
val unchanged = str.commonPrefixWith(cText)
|
||||||
|
|
||||||
|
var unchangedUntil = unchanged.length
|
||||||
|
if (unchangedUntil > 0 && unchangedUntil < str.length && Character.isLowSurrogate(str[unchangedUntil])) {
|
||||||
|
unchangedUntil--
|
||||||
|
}
|
||||||
|
|
||||||
|
val codePointLength = cText.codePointCount(unchangedUntil, cText.length)
|
||||||
|
repeat(codePointLength) {
|
||||||
|
cocos2dxGLSurfaceView.deleteBackward()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unchangedUntil < str.length) {
|
||||||
|
cocos2dxGLSurfaceView.insertText(
|
||||||
|
str.substring(unchangedUntil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
text = str
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun beforeTextChanged(
|
override fun beforeTextChanged(
|
||||||
@@ -47,22 +61,45 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
|
|||||||
|
|
||||||
override fun onTextChanged(pCharSequence: CharSequence, start: Int, before: Int, count: Int) {}
|
override fun onTextChanged(pCharSequence: CharSequence, start: Int, before: Int, count: Int) {}
|
||||||
override fun onEditorAction(textView: TextView, actionID: Int, keyEvent: KeyEvent?): Boolean {
|
override fun onEditorAction(textView: TextView, actionID: Int, keyEvent: KeyEvent?): Boolean {
|
||||||
if (cocos2dxGLSurfaceView.cocos2dxEditText == textView && isFullScreenEdit) {
|
val fullscreenIme = cocos2dxGLSurfaceView.cocos2dxEditText == textView && isFullScreenEdit
|
||||||
for (i in originText!!.length downTo 1) {
|
if (fullscreenIme) {
|
||||||
cocos2dxGLSurfaceView.deleteBackward()
|
|
||||||
}
|
|
||||||
var text = textView.text.toString()
|
var text = textView.text.toString()
|
||||||
if (!text.endsWith('\n')) {
|
if (originText != text || !cocos2dxGLSurfaceView.sendNewCocosEvents) {
|
||||||
text += '\n'
|
if (cocos2dxGLSurfaceView.sendNewCocosEvents) {
|
||||||
}
|
cocos2dxGLSurfaceView.queueEvent {
|
||||||
|
JniToCpp.textInputCaretToEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cocos2dxGLSurfaceView.insertText(text)
|
repeat(originText!!.length) {
|
||||||
|
cocos2dxGLSurfaceView.deleteBackward()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cocos2dxGLSurfaceView.sendNewCocosEvents) {
|
||||||
|
if (!text.endsWith('\n')) {
|
||||||
|
text += '\n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
cocos2dxGLSurfaceView.insertText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (actionID != 6) {
|
|
||||||
return false
|
if (fullscreenIme || actionID == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
if (cocos2dxGLSurfaceView.sendNewCocosEvents) {
|
||||||
|
cocos2dxGLSurfaceView.queueEvent {
|
||||||
|
JniToCpp.textInputDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cocos2dxGLSurfaceView.requestFocus()
|
|
||||||
Cocos2dxGLSurfaceView.closeIMEKeyboard()
|
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
cocos2dxGLSurfaceView.requestFocus()
|
||||||
|
Cocos2dxGLSurfaceView.closeIMEKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="1024"
|
android:viewportWidth="1366"
|
||||||
android:viewportHeight="1024">
|
android:viewportHeight="1366">
|
||||||
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
<group>
|
||||||
<clip-path
|
<clip-path
|
||||||
android:pathData="M0,0h1024v1024h-1024z"/>
|
android:pathData="M0,0h1366v1366h-1366z"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M0,0h1024v1024h-1024z"
|
android:pathData="M0,0h1366v1366h-1366z"
|
||||||
android:fillColor="#F4D48E"/>
|
android:fillColor="#F4D48E"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
android:pathData="M598,683m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||||
android:fillColor="#F4D48E"/>
|
android:fillColor="#F4D48E"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
android:pathData="M873.8,459.7C752.5,412.3 642.7,323 397.2,323C151.8,323 18.6,418.7 4.3,463.8C-10.1,508.8 4.7,562.4 -1.7,561.4L-25,878H1382L1373,631.1C1346.8,597.8 1260.8,569.9 1217.2,562.4C1173.6,554.9 996.8,507.8 873.8,459.7Z"
|
||||||
android:fillColor="#F4A47B"/>
|
android:fillColor="#F4A47B"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
android:pathData="M647.9,685.4C569.9,647.4 412.4,573 277.3,573C142.1,573 80.5,611.7 39.1,632.4C-2.4,653.2 -9.5,656.1 -14.9,657.4L-25,978.5L650,985L647.9,685.4Z"
|
||||||
android:fillColor="#E27880"/>
|
android:fillColor="#E27880"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
android:pathData="M1362.1,728.7C1327,696.7 1194.3,624.7 888.5,623C725.4,622.1 492.3,679.9 354.7,757.1C228.9,827.6 68.9,867 -5.2,873.5L-152,1140H1583L1362.1,728.7Z"
|
||||||
android:fillColor="#B9588F"/>
|
android:fillColor="#B9588F"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
android:pathData="M1107,1027.2C885.5,1027.2 739,943.6 544.9,943.6C350.9,943.6 228.8,1031.7 186.1,1040.2L-8,1120.8V1385L1376,1379L1367,909C1361.5,929 1287.6,1027.2 1107,1027.2Z"
|
||||||
android:fillColor="#714A9A"/>
|
android:fillColor="#714A9A"/>
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="1366"
|
||||||
|
android:viewportHeight="1366">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h1366v1366h-1366z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h1366v1366h-1366z"
|
||||||
|
android:fillColor="#9EF4C3"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M598,683m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||||
|
android:fillColor="#9EF4C3"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M873.8,459.7C752.5,412.3 642.7,323 397.2,323C151.8,323 18.6,418.7 4.3,463.8C-10.1,508.8 4.7,562.4 -1.7,561.4L-25,878H1382L1373,631.1C1346.8,597.8 1260.8,569.9 1217.2,562.4C1173.6,554.9 996.8,507.8 873.8,459.7Z"
|
||||||
|
android:fillColor="#6DDBAD"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M647.9,685.4C569.9,647.4 412.4,573 277.3,573C142.1,573 80.5,611.7 39.1,632.4C-2.4,653.2 -9.5,656.1 -14.9,657.4L-25,978.5L650,985L647.9,685.4Z"
|
||||||
|
android:fillColor="#64C780"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M1362.1,728.7C1327,696.7 1194.3,624.7 888.5,623C725.4,622.1 492.3,679.9 354.7,757.1C228.9,827.6 68.9,867 -5.2,873.5L-152,1140H1583L1362.1,728.7Z"
|
||||||
|
android:fillColor="#46A47D"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M1107,1027.2C885.5,1027.2 739,943.6 544.9,943.6C350.9,943.6 228.8,1031.7 186.1,1040.2L-8,1120.8V1385L1376,1379L1367,909C1361.5,929 1287.6,1027.2 1107,1027.2Z"
|
||||||
|
android:fillColor="#417563"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
|
||||||
@@ -1,28 +1,29 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="1024"
|
android:viewportWidth="1366"
|
||||||
android:viewportHeight="1024">
|
android:viewportHeight="1366">
|
||||||
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
<group>
|
||||||
<clip-path
|
<clip-path
|
||||||
android:pathData="M0,0h1024v1024h-1024z"/>
|
android:pathData="M0,0h1366v1366h-1366z"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M0,0h1024v1024h-1024z"
|
android:pathData="M0,0h1366v1366h-1366z"
|
||||||
android:fillColor="#CC7C7C"/>
|
android:fillColor="#CC7C7C"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
android:pathData="M598,683m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||||
android:fillColor="#CC7C7C"/>
|
android:fillColor="#CC7C7C"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
android:pathData="M873.8,459.7C752.5,412.3 642.7,323 397.2,323C151.8,323 18.6,418.7 4.3,463.8C-10.1,508.8 4.7,562.4 -1.7,561.4L-25,878H1382L1373,631.1C1346.8,597.8 1260.8,569.9 1217.2,562.4C1173.6,554.9 996.8,507.8 873.8,459.7Z"
|
||||||
android:fillColor="#C6B471"/>
|
android:fillColor="#C6B471"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
android:pathData="M647.9,685.4C569.9,647.4 412.4,573 277.3,573C142.1,573 80.5,611.7 39.1,632.4C-2.4,653.2 -9.5,656.1 -14.9,657.4L-25,978.5L650,985L647.9,685.4Z"
|
||||||
android:fillColor="#59AF7F"/>
|
android:fillColor="#59AF7F"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
android:pathData="M1362.1,728.7C1327,696.7 1194.3,624.7 888.5,623C725.4,622.1 492.3,679.9 354.7,757.1C228.9,827.6 68.9,867 -5.2,873.5L-152,1140H1583L1362.1,728.7Z"
|
||||||
android:fillColor="#5F8CAB"/>
|
android:fillColor="#5F8CAB"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
android:pathData="M1107,1027.2C885.5,1027.2 739,943.6 544.9,943.6C350.9,943.6 228.8,1031.7 186.1,1040.2L-8,1120.8V1385L1376,1379L1367,909C1361.5,929 1287.6,1027.2 1107,1027.2Z"
|
||||||
android:fillColor="#70558D"/>
|
android:fillColor="#70558D"/>
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
||||||
@@ -1,28 +1,29 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="1024"
|
android:viewportWidth="1366"
|
||||||
android:viewportHeight="1024">
|
android:viewportHeight="1366">
|
||||||
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
<group>
|
||||||
<clip-path
|
<clip-path
|
||||||
android:pathData="M0,0h1024v1024h-1024z"/>
|
android:pathData="M0,0h1366v1366h-1366z"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M0,0h1024v1024h-1024z"
|
android:pathData="M0,0h1366v1366h-1366z"
|
||||||
android:fillColor="#65B8C2"/>
|
android:fillColor="#65B8C2"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
android:pathData="M598,683m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||||
android:fillColor="#65B8C2"/>
|
android:fillColor="#65B8C2"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
android:pathData="M873.8,459.7C752.5,412.3 642.7,323 397.2,323C151.8,323 18.6,418.7 4.3,463.8C-10.1,508.8 4.7,562.4 -1.7,561.4L-25,878H1382L1373,631.1C1346.8,597.8 1260.8,569.9 1217.2,562.4C1173.6,554.9 996.8,507.8 873.8,459.7Z"
|
||||||
android:fillColor="#DEA5CD"/>
|
android:fillColor="#DEA5CD"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
android:pathData="M647.9,685.4C569.9,647.4 412.4,573 277.3,573C142.1,573 80.5,611.7 39.1,632.4C-2.4,653.2 -9.5,656.1 -14.9,657.4L-25,978.5L650,985L647.9,685.4Z"
|
||||||
android:fillColor="#DEE8EA"/>
|
android:fillColor="#DEE8EA"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
android:pathData="M1362.1,728.7C1327,696.7 1194.3,624.7 888.5,623C725.4,622.1 492.3,679.9 354.7,757.1C228.9,827.6 68.9,867 -5.2,873.5L-152,1140H1583L1362.1,728.7Z"
|
||||||
android:fillColor="#C485BD"/>
|
android:fillColor="#C485BD"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
android:pathData="M1107,1027.2C885.5,1027.2 739,943.6 544.9,943.6C350.9,943.6 228.8,1031.7 186.1,1040.2L-8,1120.8V1385L1376,1379L1367,909C1361.5,929 1287.6,1027.2 1107,1027.2Z"
|
||||||
android:fillColor="#259CB4"/>
|
android:fillColor="#259CB4"/>
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_green_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="launcher_game_update_required_short">A Geometry Dash update is required.</string>
|
<string name="launcher_game_update_required_short">A Geometry Dash update is required.</string>
|
||||||
<string name="launcher_game_update_required">The currently installed version of Geometry Dash is no longer supported. Please update Geometry Dash to continue receiving Geode updates.</string>
|
<string name="launcher_game_update_required">The currently installed version of Geometry Dash is no longer supported. Please update Geometry Dash to version %1$s to continue receiving Geode updates.</string>
|
||||||
|
|
||||||
<string name="launcher_update_available">A new launcher update is available!</string>
|
<string name="launcher_update_available">A new launcher update is available!</string>
|
||||||
<string name="launcher_update_dismiss">Dismiss</string>
|
<string name="launcher_update_dismiss">Dismiss</string>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<string name="launcher_cancelled_crash">Launch paused due to a crash.</string>
|
<string name="launcher_cancelled_crash">Launch paused due to a crash.</string>
|
||||||
<string name="launcher_cancelled_crash_instant">Geode crashed.</string>
|
<string name="launcher_cancelled_crash_instant">Geode crashed.</string>
|
||||||
<string name="launcher_cancelled_error">Geode cannot be used.</string>
|
<string name="launcher_cancelled_error">Geode cannot be used.</string>
|
||||||
<string name="launcher_cancelled_outdated">A Geometry Dash update is required.</string>
|
<string name="launcher_cancelled_outdated">Geometry Dash version %1$s is required.</string>
|
||||||
<string name="launcher_cancelled_restart">Restart</string>
|
<string name="launcher_cancelled_restart">Restart</string>
|
||||||
<string name="launcher_cancelled_resume">Resume</string>
|
<string name="launcher_cancelled_resume">Resume</string>
|
||||||
<string name="launcher_cancelled_retry">Retry</string>
|
<string name="launcher_cancelled_retry">Retry</string>
|
||||||
@@ -105,6 +105,7 @@
|
|||||||
<string name="release_fetch_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string>
|
<string name="release_fetch_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string>
|
||||||
|
|
||||||
<string name="release_fetch_external_in_use_short">Update failed. External library in use.</string>
|
<string name="release_fetch_external_in_use_short">Update failed. External library in use.</string>
|
||||||
|
<string name="release_fetch_hash_validation_failed_short">Update failed. Integrity check failed.</string>
|
||||||
<string name="release_fetch_no_internet_short">Update failed. Internet not connected.</string>
|
<string name="release_fetch_no_internet_short">Update failed. Internet not connected.</string>
|
||||||
<string name="release_fetch_generic_short">Update failed. Please try again later.</string>
|
<string name="release_fetch_generic_short">Update failed. Please try again later.</string>
|
||||||
|
|
||||||
@@ -337,6 +338,7 @@
|
|||||||
<string name="application_icon_pride">Pride</string>
|
<string name="application_icon_pride">Pride</string>
|
||||||
<string name="application_icon_trans">Transgender</string>
|
<string name="application_icon_trans">Transgender</string>
|
||||||
<string name="application_icon_sapphire">Sapphire</string>
|
<string name="application_icon_sapphire">Sapphire</string>
|
||||||
|
<string name="application_icon_green">Android</string>
|
||||||
<string name="application_icon_confirm">Change icon to %1$s?</string>
|
<string name="application_icon_confirm">Change icon to %1$s?</string>
|
||||||
<string name="application_icon_confirm_description">Geode Launcher will close itself to change the application icon.</string>
|
<string name="application_icon_confirm_description">Geode Launcher will close itself to change the application icon.</string>
|
||||||
|
|
||||||
|
|||||||
+4
-8
@@ -1,13 +1,9 @@
|
|||||||
buildscript {
|
|
||||||
val composeBOM by extra("2026.03.01")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "9.1.0" apply false
|
id("com.android.application") version "9.4.1" apply false
|
||||||
id("com.android.library") version "9.1.0" apply false
|
id("com.android.library") version "9.4.1" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.20" apply false
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.21" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" apply false
|
id("org.jetbrains.kotlin.plugin.compose") version "2.4.21" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Delete>("clean") {
|
tasks.register<Delete>("clean") {
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+3
-2
@@ -1,8 +1,9 @@
|
|||||||
#Sun Apr 05 23:29:16 MST 2026
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
retries=0
|
||||||
|
retryBackOffMs=500
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
# gradlew start up script for POSIX generated by Gradle.
|
||||||
#
|
#
|
||||||
# Important for running:
|
# Important for running:
|
||||||
#
|
#
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
# bash, then to run this script, type that shell name before the whole
|
# bash, then to run this script, type that shell name before the whole
|
||||||
# command line, like:
|
# command line, like:
|
||||||
#
|
#
|
||||||
# ksh Gradle
|
# ksh gradlew
|
||||||
#
|
#
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
# requires all of these POSIX shell features:
|
# requires all of these POSIX shell features:
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -172,7 +171,6 @@ fi
|
|||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
@@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
Vendored
+12
-24
@@ -19,12 +19,12 @@
|
|||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem gradlew startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
setlocal EnableExtensions
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@@ -51,7 +51,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
@@ -65,30 +65,18 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute gradlew
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||||
|
@rem which allows us to clear the local environment before executing the java command
|
||||||
|
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||||
|
|
||||||
:end
|
:exitWithErrorLevel
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
Reference in new issue
Block a user