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
+2 -2
View File
@@ -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 {
@@ -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)
@@ -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<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(
"/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)) {