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 <cursoragent@cursor.com>
This commit is contained in:
dadaloop82
2026-06-11 05:40:41 +00:00
parent c5b0dbcf42
commit 8a69e6d941
4 changed files with 126 additions and 26 deletions
+4 -2
View File
@@ -37,8 +37,10 @@ jobs:
id: version id: version
run: | run: |
VERSION=$(grep 'versionName' evershelf-kiosk/app/build.gradle.kts | grep -oP '"\K[^"]+') 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 "name=$VERSION" >> "$GITHUB_OUTPUT"
echo "Kiosk version: $VERSION" echo "code=$VCODE" >> "$GITHUB_OUTPUT"
echo "Kiosk version: $VERSION (versionCode $VCODE)"
- name: Build debug APK - name: Build debug APK
run: gradle assembleDebug --no-daemon run: gradle assembleDebug --no-daemon
@@ -75,7 +77,7 @@ jobs:
sleep 3 sleep 3
gh release create kiosk-latest \ gh release create kiosk-latest \
--title "EverShelf 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 \ --prerelease \
artifacts/evershelf-kiosk.apk artifacts/evershelf-kiosk.apk
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "it.dadaloop.evershelf.kiosk" applicationId = "it.dadaloop.evershelf.kiosk"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 18 versionCode = 19
versionName = "1.7.17" versionName = "1.7.18"
} }
signingConfigs { signingConfigs {
@@ -667,10 +667,15 @@ class KioskActivity : AppCompatActivity() {
packageManager.getPackageInfo(packageName, 0).versionName ?: "" packageManager.getPackageInfo(packageName, 0).versionName ?: ""
} catch (_: Exception) { "" } } 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"). // The kiosk-latest release uses a non-semver tag ("kiosk-latest").
// Extract the actual kiosk version from the release body text. // Extract the actual kiosk version from the release body text.
// Body format: "Alias automatico → kiosk-X.Y.Z" or just "kiosk-X.Y.Z". // Body format: "Alias automatico → kiosk-X.Y.Z (versionCode N)".
// Fall back to stripping the tag prefix if body parsing fails.
val bodyText = json.optString("body", "") val bodyText = json.optString("body", "")
val norm = { v: String -> v.replace(Regex("^[^0-9]*"), "") } val norm = { v: String -> v.replace(Regex("^[^0-9]*"), "") }
val remoteKioskVersion = Regex("""kiosk-v?(\d+\.\d+(?:\.\d+)?)""") val remoteKioskVersion = Regex("""kiosk-v?(\d+\.\d+(?:\.\d+)?)""")
@@ -678,6 +683,9 @@ class KioskActivity : AppCompatActivity() {
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
?: norm(latestTag) ?: 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` // Compare semver: returns true if `remote` is strictly greater than `local`
fun semverNewer(remote: String, local: String): Boolean { fun semverNewer(remote: String, local: String): Boolean {
val r = remote.split(".").map { it.filter(Char::isDigit).toIntOrNull() ?: 0 } 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 if (kioskApkUrl.isEmpty()) kioskApkUrl = KIOSK_DOWNLOAD_URL
// Only flag an update when the remote version is parseable as semver AND val kioskNeedsUpdate = when {
// strictly greater than the installed version. remoteVc > 0 && installedVc >= 0 -> remoteVc > installedVc
val kioskNeedsUpdate = currentKiosk.isNotEmpty() && isSemver && currentKiosk.isNotEmpty() && isSemver -> semverNewer(remoteKioskVersion, currentKiosk)
semverNewer(remoteKioskVersion, currentKiosk) else -> false
}
val result = JSONObject() val result = JSONObject()
.put("has_update", kioskNeedsUpdate) .put("has_update", kioskNeedsUpdate)
@@ -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? { private fun openConn(urlStr: String): HttpURLConnection? {
return try { return try {
val conn = URL(urlStr).openConnection() val conn = URL(urlStr).openConnection()
@@ -772,9 +824,52 @@ class SetupActivity : AppCompatActivity() {
runOnUiThread { discoverStatus.text = "📡 $detectedLabel" } runOnUiThread { discoverStatus.text = "📡 $detectedLabel" }
val ports = listOf(443, 80, 8080, 8443) val ports = listOf(443, 80, 8080, 8443)
// ── 1b. Fast path: likely hosts on Wi-Fi subnet (incl. .128) before full sweep ─
val priorityIps = linkedSetOf<String>()
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( 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", "/dispensa/api/index.php?action=get_settings",
"/api/index.php?action=get_settings",
"/evershelf/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 // Full HTTP probe on reachable host
val scheme = if (port == 443 || port == 8443) "https" else "http" 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) { for (path in paths) {
if (discoverCancelled.get() || found.get()) break if (discoverCancelled.get() || found.get()) break
val urlStr = "$scheme://$ip:$port$path" probeEverShelfEndpoint("$scheme://$ip$portInUrl$path")?.let { return@submit it }
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) {}
} }
null 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 result: String? = null
var collected = 0 var collected = 0
while (collected < total && !discoverCancelled.get()) { while (collected < total && !discoverCancelled.get() && result == null) {
val future = cs.poll(3, TimeUnit.SECONDS) ?: break val future = cs.poll(500, TimeUnit.MILLISECONDS) ?: continue
collected++ collected++
val r = try { future.get() } catch (_: Exception) { null } val r = try { future.get() } catch (_: Exception) { null }
if (r != null && found.compareAndSet(false, true)) { if (r != null && found.compareAndSet(false, true)) {