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)) {