From 32248c75cf5f7f7f472516777a73a5f2317ba889 Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Sun, 7 Dec 2025 04:21:37 -0500 Subject: [PATCH 01/11] Fix Android SDK level errors --- .../main/java/dev/slimevr/config/ConfigManager.java | 6 +++++- .../dev/slimevr/firmware/FirmwareUpdateHandler.kt | 4 +++- .../main/java/dev/slimevr/firmware/OTAUpdateTask.kt | 12 ++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/config/ConfigManager.java b/server/core/src/main/java/dev/slimevr/config/ConfigManager.java index 8687bc372..51bfcd7b0 100644 --- a/server/core/src/main/java/dev/slimevr/config/ConfigManager.java +++ b/server/core/src/main/java/dev/slimevr/config/ConfigManager.java @@ -17,6 +17,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.*; import java.util.Comparator; +import java.util.stream.Collectors; import java.util.stream.Stream; @@ -122,7 +123,10 @@ public class ConfigManager { var cfgFileMaybeFolder = cfgFile.toFile(); if (cfgFileMaybeFolder.isDirectory()) { try (Stream pathStream = Files.walk(cfgFile)) { - var list = pathStream.sorted(Comparator.reverseOrder()).toList(); + // Can't use .toList() on Android + var list = pathStream + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); for (var path : list) { Files.delete(path); } diff --git a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt index 1df7e4092..ecc3e160c 100644 --- a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt +++ b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt @@ -24,6 +24,7 @@ import java.security.MessageDigest import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList +import java.util.stream.Collectors import kotlin.concurrent.scheduleAtFixedRate data class DownloadedFirmwarePart( @@ -119,7 +120,8 @@ class FirmwareUpdateHandler(private val server: VRServer) : ssid: String, password: String, ) { - val serialPort = this.server.serialHandler.knownPorts.toList() + // Can't use .toList() on Android + val serialPort = this.server.serialHandler.knownPorts.collect(Collectors.toList()) .find { port -> deviceId.id == port.portLocation } if (serialPort == null) { diff --git a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt b/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt index f0f2966a7..9d8acd328 100644 --- a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt +++ b/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt @@ -3,6 +3,8 @@ package dev.slimevr.firmware import io.eiren.util.logging.LogManager import java.io.DataInputStream import java.io.DataOutputStream +import java.io.EOFException +import java.io.IOException import java.net.DatagramPacket import java.net.DatagramSocket import java.net.InetAddress @@ -130,7 +132,13 @@ class OTAUpdateTask( // so we simply skip it. // The reason those bytes are skipped here is to not have to skip all of them when checking // for the OK response. Saving time - dis.skipNBytes(4) + val bytesSkipped = dis.skipBytes(4) + // Replicate behaviour of .skipNBytes() + if (bytesSkipped == 0) { + throw EOFException("Unexpected EOF") + } else if (bytesSkipped != 4) { + throw IOException("Unexpected number of bytes skipped: $bytesSkipped") + } } if (canceled) return false @@ -138,7 +146,7 @@ class OTAUpdateTask( // We set the timeout of the connection bigger as it can take some time for the MCU // to confirm that everything is ok connection.setSoTimeout(10000) - val responseBytes = dis.readAllBytes() + val responseBytes = dis.readBytes() val response = String(responseBytes) return response.contains("OK") From 184133a613c982c55a0277066c2073cd409237df Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Sun, 7 Dec 2025 04:22:01 -0500 Subject: [PATCH 02/11] Fix OTA upload socket not being closed --- .../core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt b/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt index 9d8acd328..8fc88c79d 100644 --- a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt +++ b/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt @@ -101,11 +101,12 @@ class OTAUpdateTask( } private fun upload(serverSocket: ServerSocket): Boolean { + var connection: Socket? = null try { LogManager.info("[OTAUpdate] Starting on: ${serverSocket.localPort}") LogManager.info("[OTAUpdate] Waiting for device...") - val connection = serverSocket.accept() + connection = serverSocket.accept() this.uploadSocket = connection connection.setSoTimeout(1000) val dos = DataOutputStream(connection.getOutputStream()) @@ -153,6 +154,8 @@ class OTAUpdateTask( } catch (e: Exception) { LogManager.severe("Unable to upload the firmware using ota", e) return false + } finally { + connection?.close() } } From 215635634f8eade9978bd43240738c0dda74b063 Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Sun, 7 Dec 2025 04:43:15 -0500 Subject: [PATCH 03/11] Update Android versions & Proguard settings --- server/android/build.gradle.kts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/server/android/build.gradle.kts b/server/android/build.gradle.kts index b12f28f13..f78d8e670 100644 --- a/server/android/build.gradle.kts +++ b/server/android/build.gradle.kts @@ -13,7 +13,7 @@ plugins { kotlin("plugin.serialization") id("com.github.gmazzo.buildconfig") - id("com.android.application") version "8.6.1" + id("com.android.application") version "8.13.1" id("org.ajoberstar.grgit") } @@ -70,17 +70,17 @@ dependencies { implementation("org.apache.commons:commons-lang3:3.15.0") // Android stuff - implementation("androidx.appcompat:appcompat:1.7.0") - implementation("androidx.core:core-ktx:1.13.1") - implementation("com.google.android.material:material:1.12.0") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("androidx.core:core-ktx:1.17.0") + implementation("com.google.android.material:material:1.13.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.1") implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + androidTestImplementation("androidx.test.ext:junit:1.3.0") + androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") // For hosting web GUI - implementation("io.ktor:ktor-server-core:2.3.12") - implementation("io.ktor:ktor-server-netty:2.3.10") - implementation("io.ktor:ktor-server-caching-headers:2.3.12") + implementation("io.ktor:ktor-server-core:3.3.3") + implementation("io.ktor:ktor-server-netty:3.3.3") + implementation("io.ktor:ktor-server-caching-headers:3.3.3") // Serial implementation("com.github.mik3y:usb-serial-for-android:3.7.0") @@ -100,7 +100,7 @@ android { compile your app. This means your app can use the API features included in this API level and lower. */ - compileSdk = 35 + compileSdk = 36 /* The defaultConfig block encapsulates default settings and entries for all build variants and can override some attributes in main/AndroidManifest.xml @@ -120,7 +120,7 @@ android { minSdk = 26 // Specifies the API level used to test the app. - targetSdk = 35 + targetSdk = 36 // adds an offset of the version code as we might do apk releases in the middle of actual // releases if we failed on bundling or stuff @@ -148,7 +148,7 @@ android { getByName("release") { isMinifyEnabled = true // Enables code shrinking for the release build type. proguardFiles( - getDefaultProguardFile("proguard-android.txt"), + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } From b221250ba7ca7cbea128e63b7bfd9e5930cd20ec Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Sun, 7 Dec 2025 04:48:09 -0500 Subject: [PATCH 04/11] More Gradle cleanup --- server/android/build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/android/build.gradle.kts b/server/android/build.gradle.kts index f78d8e670..49cce9a48 100644 --- a/server/android/build.gradle.kts +++ b/server/android/build.gradle.kts @@ -145,8 +145,9 @@ android { /* By default, Android Studio configures the release build type to enable code shrinking, using minifyEnabled, and specifies the default ProGuard rules file. */ - getByName("release") { + release { isMinifyEnabled = true // Enables code shrinking for the release build type. + isShrinkResources = true // Enables resource shrinking. proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", @@ -158,6 +159,7 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { jvmTarget = "17" } From dab6ec28afa13126085755890bb24fe6e759602c Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Sun, 7 Dec 2025 04:53:09 -0500 Subject: [PATCH 05/11] Simpler `bytesSkipped` check --- .../core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt b/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt index 8fc88c79d..7d934b858 100644 --- a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt +++ b/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt @@ -135,9 +135,7 @@ class OTAUpdateTask( // for the OK response. Saving time val bytesSkipped = dis.skipBytes(4) // Replicate behaviour of .skipNBytes() - if (bytesSkipped == 0) { - throw EOFException("Unexpected EOF") - } else if (bytesSkipped != 4) { + if (bytesSkipped != 4) { throw IOException("Unexpected number of bytes skipped: $bytesSkipped") } } From 364ed3209c2e5e1034fb85b712e61fd508158aa8 Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Sun, 7 Dec 2025 05:12:56 -0500 Subject: [PATCH 06/11] Prevent Android GUI race condition --- .../src/main/java/dev/slimevr/android/Main.kt | 15 ++++++++-- .../java/dev/slimevr/android/MainActivity.kt | 28 +++++++++++++++---- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/server/android/src/main/java/dev/slimevr/android/Main.kt b/server/android/src/main/java/dev/slimevr/android/Main.kt index 0a355003d..3a0b8ccae 100644 --- a/server/android/src/main/java/dev/slimevr/android/Main.kt +++ b/server/android/src/main/java/dev/slimevr/android/Main.kt @@ -14,11 +14,12 @@ import dev.slimevr.tracking.trackers.Tracker import io.eiren.util.logging.LogManager import io.ktor.http.CacheControl import io.ktor.http.CacheControl.Visibility -import io.ktor.server.application.install +import io.ktor.server.engine.EmbeddedServer import io.ktor.server.engine.embeddedServer import io.ktor.server.http.content.CachingOptions import io.ktor.server.http.content.staticResources import io.ktor.server.netty.Netty +import io.ktor.server.netty.NettyApplicationEngine import io.ktor.server.plugins.cachingheaders.CachingHeaders import io.ktor.server.routing.routing import java.io.File @@ -26,14 +27,20 @@ import java.time.ZonedDateTime import kotlin.concurrent.thread import kotlin.system.exitProcess +lateinit var webServer: EmbeddedServer + private set + +val webServerInitialized: Boolean + get() = ::webServer.isInitialized + lateinit var vrServer: VRServer private set val vrServerInitialized: Boolean get() = ::vrServer.isInitialized -fun main(activity: AppCompatActivity) { +fun startWebServer() { // Host the web GUI server - embeddedServer(Netty, port = 34536) { + webServer = embeddedServer(Netty, port = 34536) { routing { install(CachingHeaders) { options { _, _ -> @@ -43,7 +50,9 @@ fun main(activity: AppCompatActivity) { staticResources("/", "web-gui", "index.html") } }.start(wait = false) +} +fun startVRServer(activity: AppCompatActivity) { thread(start = true, name = "Main VRServer Thread") { try { LogManager.initialize(activity.filesDir) diff --git a/server/android/src/main/java/dev/slimevr/android/MainActivity.kt b/server/android/src/main/java/dev/slimevr/android/MainActivity.kt index 53a98aa14..33c0782e2 100644 --- a/server/android/src/main/java/dev/slimevr/android/MainActivity.kt +++ b/server/android/src/main/java/dev/slimevr/android/MainActivity.kt @@ -7,6 +7,8 @@ import android.webkit.WebSettings import android.webkit.WebView import androidx.appcompat.app.AppCompatActivity import io.eiren.util.logging.LogManager +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock class AndroidJsObject { @JavascriptInterface @@ -25,12 +27,22 @@ class MainActivity : AppCompatActivity() { e1.printStackTrace() } - // Start the server if it isn't already running - if (!vrServerInitialized) { - LogManager.info("[MainActivity] VRServer isn't running yet, starting it...") - main(this) - } else { - LogManager.info("[MainActivity] VRServer is already running, skipping initialization.") + initLock.withLock { + // Start the GUI if it isn't already running + if (!webServerInitialized) { + LogManager.info("[MainActivity] WebServer isn't running yet, starting it...") + startWebServer() + } else { + LogManager.info("[MainActivity] WebServer is already running, skipping initialization.") + } + + // Start the server if it isn't already running + if (!vrServerInitialized) { + LogManager.info("[MainActivity] VRServer isn't running yet, starting it...") + startVRServer(this) + } else { + LogManager.info("[MainActivity] VRServer is already running, skipping initialization.") + } } // Load the web GUI web page @@ -67,4 +79,8 @@ class MainActivity : AppCompatActivity() { val serviceIntent = Intent(this, ForegroundService::class.java) startForegroundService(serviceIntent) } + + companion object { + val initLock = ReentrantLock() + } } From 066c28adb43ee824e3d87a7b83223ece92c76eda Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Sun, 7 Dec 2025 05:20:13 -0500 Subject: [PATCH 07/11] Use randomized Android GUI port --- server/android/src/main/java/dev/slimevr/android/Main.kt | 2 +- .../android/src/main/java/dev/slimevr/android/MainActivity.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/android/src/main/java/dev/slimevr/android/Main.kt b/server/android/src/main/java/dev/slimevr/android/Main.kt index 3a0b8ccae..f41d1b674 100644 --- a/server/android/src/main/java/dev/slimevr/android/Main.kt +++ b/server/android/src/main/java/dev/slimevr/android/Main.kt @@ -40,7 +40,7 @@ val vrServerInitialized: Boolean fun startWebServer() { // Host the web GUI server - webServer = embeddedServer(Netty, port = 34536) { + webServer = embeddedServer(Netty, port = 0) { routing { install(CachingHeaders) { options { _, _ -> diff --git a/server/android/src/main/java/dev/slimevr/android/MainActivity.kt b/server/android/src/main/java/dev/slimevr/android/MainActivity.kt index 33c0782e2..60812a828 100644 --- a/server/android/src/main/java/dev/slimevr/android/MainActivity.kt +++ b/server/android/src/main/java/dev/slimevr/android/MainActivity.kt @@ -7,6 +7,7 @@ import android.webkit.WebSettings import android.webkit.WebView import androidx.appcompat.app.AppCompatActivity import io.eiren.util.logging.LogManager +import kotlinx.coroutines.runBlocking import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -71,7 +72,8 @@ class MainActivity : AppCompatActivity() { guiWebView.clearCache(true) // Load GUI page - guiWebView.loadUrl("http://127.0.0.1:34536/") + val port = runBlocking { webServer.engine.resolvedConnectors().first().port } + guiWebView.loadUrl("http://127.0.0.1:$port/") LogManager.info("[MainActivity] GUI WebView has been initialized and loaded.") // Start a foreground service to notify the user the SlimeVR Server is running From 3a6c60d9127e6dba573b58e4395b2d8454ec009a Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Sun, 7 Dec 2025 05:23:03 -0500 Subject: [PATCH 08/11] Only get GUI port once --- server/android/src/main/java/dev/slimevr/android/Main.kt | 4 ++++ .../android/src/main/java/dev/slimevr/android/MainActivity.kt | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/server/android/src/main/java/dev/slimevr/android/Main.kt b/server/android/src/main/java/dev/slimevr/android/Main.kt index f41d1b674..6be536641 100644 --- a/server/android/src/main/java/dev/slimevr/android/Main.kt +++ b/server/android/src/main/java/dev/slimevr/android/Main.kt @@ -22,6 +22,7 @@ import io.ktor.server.netty.Netty import io.ktor.server.netty.NettyApplicationEngine import io.ktor.server.plugins.cachingheaders.CachingHeaders import io.ktor.server.routing.routing +import kotlinx.coroutines.runBlocking import java.io.File import java.time.ZonedDateTime import kotlin.concurrent.thread @@ -33,6 +34,8 @@ lateinit var webServer: EmbeddedServer Date: Mon, 8 Dec 2025 21:47:00 -0500 Subject: [PATCH 09/11] Revert Ktor version update --- server/android/build.gradle.kts | 6 +++--- server/android/src/main/java/dev/slimevr/android/Main.kt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/android/build.gradle.kts b/server/android/build.gradle.kts index 49cce9a48..f0a588b6b 100644 --- a/server/android/build.gradle.kts +++ b/server/android/build.gradle.kts @@ -78,9 +78,9 @@ dependencies { androidTestImplementation("androidx.test.ext:junit:1.3.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") // For hosting web GUI - implementation("io.ktor:ktor-server-core:3.3.3") - implementation("io.ktor:ktor-server-netty:3.3.3") - implementation("io.ktor:ktor-server-caching-headers:3.3.3") + implementation("io.ktor:ktor-server-core:2.3.12") + implementation("io.ktor:ktor-server-netty:2.3.10") + implementation("io.ktor:ktor-server-caching-headers:2.3.12") // Serial implementation("com.github.mik3y:usb-serial-for-android:3.7.0") diff --git a/server/android/src/main/java/dev/slimevr/android/Main.kt b/server/android/src/main/java/dev/slimevr/android/Main.kt index 6be536641..005f8a6ed 100644 --- a/server/android/src/main/java/dev/slimevr/android/Main.kt +++ b/server/android/src/main/java/dev/slimevr/android/Main.kt @@ -14,7 +14,7 @@ import dev.slimevr.tracking.trackers.Tracker import io.eiren.util.logging.LogManager import io.ktor.http.CacheControl import io.ktor.http.CacheControl.Visibility -import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.application.install import io.ktor.server.engine.embeddedServer import io.ktor.server.http.content.CachingOptions import io.ktor.server.http.content.staticResources @@ -28,7 +28,7 @@ import java.time.ZonedDateTime import kotlin.concurrent.thread import kotlin.system.exitProcess -lateinit var webServer: EmbeddedServer +lateinit var webServer: NettyApplicationEngine private set val webServerInitialized: Boolean @@ -53,7 +53,7 @@ fun startWebServer() { staticResources("/", "web-gui", "index.html") } }.start(wait = false) - webServerPort = runBlocking { webServer.engine.resolvedConnectors().first().port } + webServerPort = runBlocking { webServer.resolvedConnectors().first().port } } fun startVRServer(activity: AppCompatActivity) { From 94a70d3b2ea15c523edd0491000ec07caccc855a Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Mon, 8 Dec 2025 21:50:53 -0500 Subject: [PATCH 10/11] Update Ktor to latest 2.3.X version --- server/android/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/android/build.gradle.kts b/server/android/build.gradle.kts index f0a588b6b..cf73dd236 100644 --- a/server/android/build.gradle.kts +++ b/server/android/build.gradle.kts @@ -78,9 +78,9 @@ dependencies { androidTestImplementation("androidx.test.ext:junit:1.3.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") // For hosting web GUI - implementation("io.ktor:ktor-server-core:2.3.12") - implementation("io.ktor:ktor-server-netty:2.3.10") - implementation("io.ktor:ktor-server-caching-headers:2.3.12") + implementation("io.ktor:ktor-server-core:2.3.13") + implementation("io.ktor:ktor-server-netty:2.3.13") + implementation("io.ktor:ktor-server-caching-headers:2.3.13") // Serial implementation("com.github.mik3y:usb-serial-for-android:3.7.0") From 85ee162d5802455e08f5ed3520e7b5415ee1e423 Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Tue, 9 Dec 2025 07:18:26 -0500 Subject: [PATCH 11/11] Remove unused import --- server/android/src/main/java/dev/slimevr/android/MainActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/server/android/src/main/java/dev/slimevr/android/MainActivity.kt b/server/android/src/main/java/dev/slimevr/android/MainActivity.kt index 429914e7e..5d6e2b58a 100644 --- a/server/android/src/main/java/dev/slimevr/android/MainActivity.kt +++ b/server/android/src/main/java/dev/slimevr/android/MainActivity.kt @@ -7,7 +7,6 @@ import android.webkit.WebSettings import android.webkit.WebView import androidx.appcompat.app.AppCompatActivity import io.eiren.util.logging.LogManager -import kotlinx.coroutines.runBlocking import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock