From 8a69e6d941ab23f5a796d99c16768423225d1f1a Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Thu, 11 Jun 2026 05:40:41 +0000 Subject: [PATCH] Fix kiosk LAN discovery and improve OTA update detection. Discovery no longer aborts after 3s idle, probes priority hosts (.128, gateway) first, accepts ping API and normalizes HTTPS URLs. OTA compares versionCode from release notes; bump kiosk to 1.7.18. Co-authored-by: Cursor --- .github/workflows/build-kiosk.yml | 6 +- evershelf-kiosk/app/build.gradle.kts | 4 +- .../dadaloop/evershelf/kiosk/KioskActivity.kt | 21 ++- .../dadaloop/evershelf/kiosk/SetupActivity.kt | 121 +++++++++++++++--- 4 files changed, 126 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build-kiosk.yml b/.github/workflows/build-kiosk.yml index e012b16..28adb70 100644 --- a/.github/workflows/build-kiosk.yml +++ b/.github/workflows/build-kiosk.yml @@ -37,8 +37,10 @@ jobs: id: version run: | VERSION=$(grep 'versionName' evershelf-kiosk/app/build.gradle.kts | grep -oP '"\K[^"]+') + VCODE=$(grep 'versionCode' evershelf-kiosk/app/build.gradle.kts | grep -oP '\d+') echo "name=$VERSION" >> "$GITHUB_OUTPUT" - echo "Kiosk version: $VERSION" + echo "code=$VCODE" >> "$GITHUB_OUTPUT" + echo "Kiosk version: $VERSION (versionCode $VCODE)" - name: Build debug APK run: gradle assembleDebug --no-daemon @@ -75,7 +77,7 @@ jobs: sleep 3 gh release create kiosk-latest \ --title "EverShelf Kiosk Latest" \ - --notes "Alias automatico → kiosk-${{ steps.version.outputs.name }}" \ + --notes "Alias automatico → kiosk-${{ steps.version.outputs.name }} (versionCode ${{ steps.version.outputs.code }})" \ --prerelease \ artifacts/evershelf-kiosk.apk diff --git a/evershelf-kiosk/app/build.gradle.kts b/evershelf-kiosk/app/build.gradle.kts index a152b2a..9234355 100644 --- a/evershelf-kiosk/app/build.gradle.kts +++ b/evershelf-kiosk/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "it.dadaloop.evershelf.kiosk" minSdk = 24 targetSdk = 35 - versionCode = 18 - versionName = "1.7.17" + versionCode = 19 + versionName = "1.7.18" } signingConfigs { diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt index 2e4e85b..b0e90ad 100644 --- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt +++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt @@ -667,10 +667,15 @@ class KioskActivity : AppCompatActivity() { packageManager.getPackageInfo(packageName, 0).versionName ?: "" } catch (_: Exception) { "" } + val installedVc: Long = try { + val pi = packageManager.getPackageInfo(packageName, 0) + if (Build.VERSION.SDK_INT >= 28) pi.longVersionCode + else @Suppress("DEPRECATION") pi.versionCode.toLong() + } catch (_: Exception) { -1L } + // The kiosk-latest release uses a non-semver tag ("kiosk-latest"). // Extract the actual kiosk version from the release body text. - // Body format: "Alias automatico → kiosk-X.Y.Z" or just "kiosk-X.Y.Z". - // Fall back to stripping the tag prefix if body parsing fails. + // Body format: "Alias automatico → kiosk-X.Y.Z (versionCode N)". val bodyText = json.optString("body", "") val norm = { v: String -> v.replace(Regex("^[^0-9]*"), "") } val remoteKioskVersion = Regex("""kiosk-v?(\d+\.\d+(?:\.\d+)?)""") @@ -678,6 +683,9 @@ class KioskActivity : AppCompatActivity() { ?.takeIf { it.isNotEmpty() } ?: norm(latestTag) + val remoteVc = Regex("""versionCode[=:\s(]+(\d+)""", RegexOption.IGNORE_CASE) + .find(bodyText)?.groupValues?.get(1)?.toLongOrNull() ?: -1L + // Compare semver: returns true if `remote` is strictly greater than `local` fun semverNewer(remote: String, local: String): Boolean { val r = remote.split(".").map { it.filter(Char::isDigit).toIntOrNull() ?: 0 } @@ -707,10 +715,11 @@ class KioskActivity : AppCompatActivity() { } if (kioskApkUrl.isEmpty()) kioskApkUrl = KIOSK_DOWNLOAD_URL - // Only flag an update when the remote version is parseable as semver AND - // strictly greater than the installed version. - val kioskNeedsUpdate = currentKiosk.isNotEmpty() && isSemver && - semverNewer(remoteKioskVersion, currentKiosk) + val kioskNeedsUpdate = when { + remoteVc > 0 && installedVc >= 0 -> remoteVc > installedVc + currentKiosk.isNotEmpty() && isSemver -> semverNewer(remoteKioskVersion, currentKiosk) + else -> false + } val result = JSONObject() .put("has_update", kioskNeedsUpdate) diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SetupActivity.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SetupActivity.kt index 716c639..6ce3444 100644 --- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SetupActivity.kt +++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SetupActivity.kt @@ -697,6 +697,58 @@ class SetupActivity : AppCompatActivity() { }) } + private fun normalizeDiscoveredBase(urlStr: String): String { + var base = urlStr.substringBefore("/api/") + if (base.endsWith(":443")) base = base.removeSuffix(":443") + if (base.endsWith(":80")) base = base.removeSuffix(":80") + return if (base.endsWith("/")) base else "$base/" + } + + private fun probeEverShelfEndpoint(urlStr: String): String? { + return try { + val conn = openConn(urlStr) ?: return null + val code = conn.responseCode + if (code !in 200..399) { + conn.disconnect() + return null + } + val body = conn.inputStream.bufferedReader().readText() + conn.disconnect() + if (body.contains("gemini_key_set") || body.contains("\"success\"") || body.contains("\"ok\"")) { + normalizeDiscoveredBase(urlStr) + } else null + } catch (_: Exception) { + null + } + } + + private fun probeEverShelfHost(ip: String, port: Int): String? { + val reachable = try { + Socket().use { s -> s.connect(InetSocketAddress(ip, port), 800); true } + } catch (_: Exception) { + false + } + if (!reachable) return null + + val scheme = if (port == 443 || port == 8443) "https" else "http" + val portInUrl = when { + scheme == "https" && port == 443 -> "" + scheme == "http" && port == 80 -> "" + else -> ":$port" + } + val paths = listOf( + "/dispensa/api/index.php?action=ping", + "/api/index.php?action=ping", + "/dispensa/api/index.php?action=get_settings", + "/api/index.php?action=get_settings", + "/evershelf/api/index.php?action=get_settings", + ) + for (path in paths) { + probeEverShelfEndpoint("$scheme://$ip$portInUrl$path")?.let { return it } + } + return null + } + private fun openConn(urlStr: String): HttpURLConnection? { return try { val conn = URL(urlStr).openConnection() @@ -772,9 +824,52 @@ class SetupActivity : AppCompatActivity() { runOnUiThread { discoverStatus.text = "📡 $detectedLabel" } val ports = listOf(443, 80, 8080, 8443) + + // ── 1b. Fast path: likely hosts on Wi-Fi subnet (incl. .128) before full sweep ─ + val priorityIps = linkedSetOf() + try { + val ifaces = NetworkInterface.getNetworkInterfaces() + while (ifaces != null && ifaces.hasMoreElements()) { + val intf = ifaces.nextElement() + if (!intf.isUp || intf.isLoopback) continue + for (addr in intf.interfaceAddresses) { + val ip = addr.address + if (ip is java.net.Inet4Address && !ip.isLoopbackAddress) { + priorityIps.add(ip.hostAddress ?: continue) + } + } + } + } catch (_: Exception) {} + for (subnet in wifiSubnets.ifEmpty { subnets.take(1) }) { + for (last in listOf(1, 128, 100, 10, 50, 254)) { + priorityIps.add("$subnet.$last") + } + } + + runOnUiThread { discoverStatus.text = "🔍 ${getString(R.string.setup_discovering_detail)}" } + for (ip in priorityIps) { + if (discoverCancelled.get()) break + for (port in ports) { + val hit = probeEverShelfHost(ip, port) + if (hit != null) { + runOnUiThread { + urlEdit.setText(hit) + discoverStatus.text = "✅ ${getString(R.string.setup_server_found)}: $hit" + discoverStatus.setTextColor(0xFF34d399.toInt()) + showUrlStatus("✅ ${getString(R.string.setup_server_found)}", true) + btnDiscover.isEnabled = true + btnDiscover.text = getString(R.string.setup_discover_btn) + } + return@Thread + } + } + } + val paths = listOf( - "/api/index.php?action=get_settings", + "/dispensa/api/index.php?action=ping", + "/api/index.php?action=ping", "/dispensa/api/index.php?action=get_settings", + "/api/index.php?action=get_settings", "/evershelf/api/index.php?action=get_settings", ) @@ -819,30 +914,24 @@ class SetupActivity : AppCompatActivity() { // Full HTTP probe on reachable host val scheme = if (port == 443 || port == 8443) "https" else "http" + val portInUrl = when { + scheme == "https" && port == 443 -> "" + scheme == "http" && port == 80 -> "" + else -> ":$port" + } for (path in paths) { if (discoverCancelled.get() || found.get()) break - val urlStr = "$scheme://$ip:$port$path" - try { - val conn = openConn(urlStr) ?: continue - val code = conn.responseCode - if (code in 200..399) { - val body = conn.inputStream.bufferedReader().readText() - conn.disconnect() - if (body.contains("gemini_key_set") || body.contains("\"success\"")) { - return@submit urlStr.substringBefore("/api/") + "/" - } - } else conn.disconnect() - } catch (_: Exception) {} + probeEverShelfEndpoint("$scheme://$ip$portInUrl$path")?.let { return@submit it } } null } } - // ── 3. Collect results as they complete (not in submission order) ──── + // ── 3. Collect results until all tasks finish or a server is found ──── var result: String? = null var collected = 0 - while (collected < total && !discoverCancelled.get()) { - val future = cs.poll(3, TimeUnit.SECONDS) ?: break + while (collected < total && !discoverCancelled.get() && result == null) { + val future = cs.poll(500, TimeUnit.MILLISECONDS) ?: continue collected++ val r = try { future.get() } catch (_: Exception) { null } if (r != null && found.compareAndSet(false, true)) {