diff --git a/server/android/build.gradle.kts b/server/android/build.gradle.kts index 9e17b10d0..54afaf0a0 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") } @@ -79,17 +79,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: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") @@ -109,7 +109,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 @@ -129,7 +129,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 @@ -163,10 +163,11 @@ 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.txt"), + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) signingConfig = signingConfigs.getByName("release") @@ -177,6 +178,7 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { jvmTarget = "17" } 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..005f8a6ed 100644 --- a/server/android/src/main/java/dev/slimevr/android/Main.kt +++ b/server/android/src/main/java/dev/slimevr/android/Main.kt @@ -19,21 +19,31 @@ 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 kotlinx.coroutines.runBlocking import java.io.File import java.time.ZonedDateTime import kotlin.concurrent.thread import kotlin.system.exitProcess +lateinit var webServer: NettyApplicationEngine + private set + +val webServerInitialized: Boolean + get() = ::webServer.isInitialized + +var webServerPort = 0 + 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 = 0) { routing { install(CachingHeaders) { options { _, _ -> @@ -43,7 +53,10 @@ fun main(activity: AppCompatActivity) { staticResources("/", "web-gui", "index.html") } }.start(wait = false) + webServerPort = runBlocking { webServer.resolvedConnectors().first().port } +} +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..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,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 @@ -59,7 +71,7 @@ class MainActivity : AppCompatActivity() { guiWebView.clearCache(true) // Load GUI page - guiWebView.loadUrl("http://127.0.0.1:34536/") + guiWebView.loadUrl("http://127.0.0.1:$webServerPort/") LogManager.info("[MainActivity] GUI WebView has been initialized and loaded.") // Start a foreground service to notify the user the SlimeVR Server is running @@ -67,4 +79,8 @@ class MainActivity : AppCompatActivity() { val serviceIntent = Intent(this, ForegroundService::class.java) startForegroundService(serviceIntent) } + + companion object { + val initLock = ReentrantLock() + } } 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..7d934b858 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 @@ -99,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()) @@ -130,7 +133,11 @@ 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 != 4) { + throw IOException("Unexpected number of bytes skipped: $bytesSkipped") + } } if (canceled) return false @@ -138,13 +145,15 @@ 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") } catch (e: Exception) { LogManager.severe("Unable to upload the firmware using ota", e) return false + } finally { + connection?.close() } }