chore: auto-merge develop → main

Triggered by: 09fd122 fix(kiosk): rewrite autoDiscover — real-time IP feedback + CompletionService + TCP pre-check
This commit is contained in:
github-actions[bot]
2026-05-04 17:02:52 +00:00
4 changed files with 116 additions and 79 deletions
@@ -12,7 +12,6 @@ import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@@ -30,11 +29,17 @@ import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.switchmaterial.SwitchMaterial
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.Socket
import java.net.URL import java.net.URL
import java.util.Locale import java.util.Locale
import java.util.concurrent.ExecutorCompletionService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager import javax.net.ssl.TrustManager
@@ -502,7 +507,6 @@ class SetupActivity : AppCompatActivity() {
// ── Auto-Discover ───────────────────────────────────────────────────── // ── Auto-Discover ─────────────────────────────────────────────────────
@Suppress("DEPRECATION")
private fun autoDiscover() { private fun autoDiscover() {
discoverCancelled.set(false) discoverCancelled.set(false)
btnDiscover.isEnabled = false btnDiscover.isEnabled = false
@@ -511,92 +515,125 @@ class SetupActivity : AppCompatActivity() {
discoverStatus.text = getString(R.string.setup_discovering_detail) discoverStatus.text = getString(R.string.setup_discovering_detail)
discoverStatus.setTextColor(0xFF94a3b8.toInt()) discoverStatus.setTextColor(0xFF94a3b8.toInt())
// Determine local subnet
val wifiMgr = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val ipInt = wifiMgr.connectionInfo.ipAddress
val subnets = mutableListOf<String>()
if (ipInt != 0) {
val a = (ipInt shr 0) and 0xFF
val b = (ipInt shr 8) and 0xFF
val c = (ipInt shr 16) and 0xFF
subnets += "$a.$b.$c"
}
// Always include common subnets as fallback
for (s in listOf("192.168.1", "192.168.0", "192.168.2", "10.0.0")) {
if (!subnets.contains(s)) subnets += s
}
val ports = listOf(80, 8080)
val paths = listOf(
"/api/index.php?action=get_settings",
"/dispensa/api/index.php?action=get_settings",
"/evershelf/api/index.php?action=get_settings"
)
val executor = Executors.newFixedThreadPool(40)
val found = AtomicBoolean(false)
Thread { Thread {
val futures = mutableListOf<java.util.concurrent.Future<String?>>() // ── 1. Detect subnets via NetworkInterface (not deprecated WifiManager) ──
outer@ for (subnet in subnets) { val subnets = mutableListOf<String>()
for (i in 1..254) { try {
if (discoverCancelled.get() || found.get()) break@outer val interfaces = NetworkInterface.getNetworkInterfaces()
val ip = "$subnet.$i" while (interfaces != null && interfaces.hasMoreElements()) {
for (port in ports) { val intf = interfaces.nextElement()
if (discoverCancelled.get() || found.get()) break@outer if (!intf.isUp || intf.isLoopback) continue
futures += executor.submit<String?> submit@{ for (addr in intf.interfaceAddresses) {
if (discoverCancelled.get() || found.get()) return@submit null val ip = addr.address
val scheme = if (port == 443 || port == 8443) "https" else "http" if (ip is java.net.Inet4Address && !ip.isLoopbackAddress) {
for (path in paths) { val parts = ip.hostAddress?.split(".") ?: continue
val urlStr = "$scheme://$ip:$port$path" if (parts.size == 4) subnets += "${parts[0]}.${parts[1]}.${parts[2]}"
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\"")) {
val base = urlStr.substringBefore("/api/")
return@submit "$base/"
}
} else conn.disconnect()
} catch (_: Exception) {}
}
null
} }
} }
} }
} catch (_: Exception) {}
// Append common fallback subnets (deduped)
for (s in listOf("192.168.1", "192.168.0", "192.168.2", "10.0.0", "10.0.1")) {
if (!subnets.contains(s)) subnets += s
} }
// Collect results val ports = listOf(80, 8080)
for (f in futures) { val paths = listOf(
if (discoverCancelled.get()) break "/api/index.php?action=get_settings",
val result = try { f.get(4, TimeUnit.SECONDS) } catch (_: Exception) { null } "/dispensa/api/index.php?action=get_settings",
if (result != null && found.compareAndSet(false, true)) { "/evershelf/api/index.php?action=get_settings",
runOnUiThread { )
urlEdit.setText(result)
discoverStatus.text = "${getString(R.string.setup_server_found)}: $result" // Build full task list: subnet-first ordering ensures local subnet is scanned first
discoverStatus.setTextColor(0xFF34d399.toInt()) val allTargets = mutableListOf<Pair<String, Int>>()
showUrlStatus("${getString(R.string.setup_server_found)}", true) for (subnet in subnets.distinct()) {
btnDiscover.isEnabled = true for (i in 1..254) {
btnDiscover.text = getString(R.string.setup_discover_btn) for (port in ports) {
allTargets += "$subnet.$i" to port
} }
}
}
val executor = Executors.newFixedThreadPool(60)
val cs = ExecutorCompletionService<String?>(executor)
val found = AtomicBoolean(false)
val scanned = AtomicInteger(0)
val total = allTargets.size
val lastUiMs = AtomicLong(0L)
// ── 2. Submit all tasks ─────────────────────────────────────────────
for ((ip, port) in allTargets) {
cs.submit {
if (discoverCancelled.get() || found.get()) return@submit null
val n = scanned.incrementAndGet()
// Update status ~8 fps (every 120 ms) without hammering the UI thread
val now = System.currentTimeMillis()
if (now - lastUiMs.get() > 120) {
lastUiMs.set(now)
runOnUiThread {
discoverStatus.text = "🔍 $ip:$port ($n / $total)"
}
}
// TCP pre-check (600 ms) — skips unreachable hosts instantly
val reachable = try {
Socket().use { s -> s.connect(InetSocketAddress(ip, port), 600); true }
} catch (_: Exception) { false }
if (!reachable || discoverCancelled.get() || found.get()) return@submit null
// Full HTTP probe on reachable host
val scheme = if (port == 443 || port == 8443) "https" else "http"
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) {}
}
null
}
}
// ── 3. Collect results as they complete (not in submission order) ────
var result: String? = null
var collected = 0
while (collected < total && !discoverCancelled.get()) {
val future = cs.poll(3, TimeUnit.SECONDS) ?: break
collected++
val r = try { future.get() } catch (_: Exception) { null }
if (r != null && found.compareAndSet(false, true)) {
result = r
break break
} }
} }
executor.shutdown() executor.shutdownNow()
if (!found.get() && !discoverCancelled.get()) { val finalResult = result
runOnUiThread { runOnUiThread {
discoverStatus.text = getString(R.string.setup_discover_not_found) when {
discoverStatus.setTextColor(0xFFf87171.toInt()) finalResult != null -> {
btnDiscover.isEnabled = true urlEdit.setText(finalResult)
btnDiscover.text = getString(R.string.setup_discover_btn) discoverStatus.text = "${getString(R.string.setup_server_found)}: $finalResult"
} discoverStatus.setTextColor(0xFF34d399.toInt())
} else if (!found.get()) { showUrlStatus("${getString(R.string.setup_server_found)}", true)
runOnUiThread { }
btnDiscover.isEnabled = true !discoverCancelled.get() -> {
btnDiscover.text = getString(R.string.setup_discover_btn) discoverStatus.text = getString(R.string.setup_discover_not_found)
discoverStatus.setTextColor(0xFFf87171.toInt())
}
} }
btnDiscover.isEnabled = true
btnDiscover.text = getString(R.string.setup_discover_btn)
} }
}.start() }.start()
} }
@@ -10,7 +10,7 @@
<string name="setup_unreachable">Server nicht erreichbar</string> <string name="setup_unreachable">Server nicht erreichbar</string>
<string name="setup_discover_btn">🔍 Lokales Netzwerk durchsuchen</string> <string name="setup_discover_btn">🔍 Lokales Netzwerk durchsuchen</string>
<string name="setup_discovering">Suche läuft…</string> <string name="setup_discovering">Suche läuft…</string>
<string name="setup_discovering_detail">Suche nach EverShelf-Servern im lokalen Netzwerk (kann bis zu 30 s dauern)</string> <string name="setup_discovering_detail">Suche nach EverShelf-Servern im lokalen Netzwerk…</string>
<string name="setup_discover_not_found">Kein EverShelf-Server automatisch gefunden. URL manuell eingeben.</string> <string name="setup_discover_not_found">Kein EverShelf-Server automatisch gefunden. URL manuell eingeben.</string>
<string name="setup_exit_title">Setup beenden?</string> <string name="setup_exit_title">Setup beenden?</string>
<string name="setup_exit_message">Die Einrichtung kann später beim erneuten Öffnen der App abgeschlossen werden.</string> <string name="setup_exit_message">Die Einrichtung kann später beim erneuten Öffnen der App abgeschlossen werden.</string>
@@ -10,7 +10,7 @@
<string name="setup_unreachable">Impossibile raggiungere il server</string> <string name="setup_unreachable">Impossibile raggiungere il server</string>
<string name="setup_discover_btn">🔍 Cerca nella rete locale</string> <string name="setup_discover_btn">🔍 Cerca nella rete locale</string>
<string name="setup_discovering">Scansione in corso…</string> <string name="setup_discovering">Scansione in corso…</string>
<string name="setup_discovering_detail">Ricerca server EverShelf nella rete locale (potrebbe richiedere fino a 30 s)</string> <string name="setup_discovering_detail">Ricerca server EverShelf nella rete locale…</string>
<string name="setup_discover_not_found">Nessun server EverShelf trovato automaticamente. Inserisci l\'URL manualmente.</string> <string name="setup_discover_not_found">Nessun server EverShelf trovato automaticamente. Inserisci l\'URL manualmente.</string>
<string name="setup_exit_title">Uscire dalla configurazione?</string> <string name="setup_exit_title">Uscire dalla configurazione?</string>
<string name="setup_exit_message">Puoi completare la configurazione più tardi riaprendo l\'app.</string> <string name="setup_exit_message">Puoi completare la configurazione più tardi riaprendo l\'app.</string>
@@ -9,7 +9,7 @@
<string name="setup_unreachable">Cannot reach server</string> <string name="setup_unreachable">Cannot reach server</string>
<string name="setup_discover_btn">🔍 Search local network</string> <string name="setup_discover_btn">🔍 Search local network</string>
<string name="setup_discovering">Scanning…</string> <string name="setup_discovering">Scanning…</string>
<string name="setup_discovering_detail">Scanning local network for EverShelf servers (this may take up to 30 s)</string> <string name="setup_discovering_detail">Searching for EverShelf servers on the local network</string>
<string name="setup_discover_not_found">No EverShelf server found automatically. Enter the URL manually.</string> <string name="setup_discover_not_found">No EverShelf server found automatically. Enter the URL manually.</string>
<string name="setup_exit_title">Exit setup?</string> <string name="setup_exit_title">Exit setup?</string>
<string name="setup_exit_message">You can complete setup later when you reopen the app.</string> <string name="setup_exit_message">You can complete setup later when you reopen the app.</string>