chore: auto-merge develop → main

Triggered by: 22e506b fix(kiosk): 4 bug fix — uninstall loop, PHP check, APK validation, ErrorReporter init
This commit is contained in:
github-actions[bot]
2026-05-03 20:12:17 +00:00
4 changed files with 63 additions and 9 deletions
@@ -229,6 +229,8 @@ class KioskActivity : AppCompatActivity() {
return@setOnClickListener return@setOnClickListener
} }
prefs.edit().putString(KEY_URL, url).apply() prefs.edit().putString(KEY_URL, url).apply()
// Re-init ErrorReporter immediately so install errors in step 3 reach GitHub Issues.
ErrorReporter.init(this, url)
goToStep(3) goToStep(3)
} }
@@ -728,11 +730,32 @@ class KioskActivity : AppCompatActivity() {
conn.requestMethod = "GET" conn.requestMethod = "GET"
val code = conn.responseCode val code = conn.responseCode
conn.disconnect() conn.disconnect()
if (code !in 200..399) {
runOnUiThread { showUrlStatus("⚠ Server responded with code $code", false) }
return@Thread
}
// Second check: verify the EverShelf PHP API is actually present.
var apiOk = false
var apiCode = -1
try {
val base = url.trimEnd('/')
val apiConn = java.net.URL("$base/api/?action=check_update")
.openConnection() as java.net.HttpURLConnection
apiConn.requestMethod = "GET"
apiConn.connectTimeout = 5000
apiConn.readTimeout = 5000
apiCode = apiConn.responseCode
val body = apiConn.inputStream.bufferedReader().readText()
apiConn.disconnect()
apiOk = apiCode in 200..399 &&
(body.contains("latest_tag") || body.contains("webapp_version") || body.contains("ok"))
} catch (_: Exception) {}
runOnUiThread { runOnUiThread {
if (code in 200..399) { if (apiOk) {
showUrlStatus("✓ Connected successfully!", true) showUrlStatus("✅ Server EverShelf trovato e API attiva!", true)
} else { } else {
showUrlStatus("⚠ Server responded with code $code", false) showUrlStatus("⚠ Server raggiungibile (HTTP $code) ma API PHP non trovata (codice $apiCode). " +
"Verifica che il server EverShelf sia installato correttamente.", false)
} }
} }
} }
@@ -1176,6 +1199,29 @@ class KioskActivity : AppCompatActivity() {
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) } runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
return return
} }
// Validate APK magic bytes (ZIP local file header = 0x504B0304).
// If GitHub returned a 404 HTML page, DownloadManager still reports SUCCESS
// but the file starts with '<' not 'PK' — this catches that case early.
val magic = try { file.inputStream().use { it.read(4) ; true }; // read to check open
file.inputStream().use { s -> val b = ByteArray(4); s.read(b); b }
} catch (_: Exception) { null }
val isApk = magic != null && magic[0] == 0x50.toByte() && magic[1] == 0x4B.toByte()
if (!isApk) {
setInstallUI(
"\u274C",
getString(R.string.install_error_download),
"Il file scaricato non è un APK valido (possibile 404 sulla release). " +
"Verifica che la release GitHub sia pubblicata.",
0xFFf87171.toInt(),
btnEnabled = true,
progress = -2
)
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.reportMessage("install_invalid_apk",
"Downloaded file is not a valid APK (bad magic bytes). URL=$pendingApkDownloadUrl size=${file.length()}")
file.delete() // remove corrupt file so next attempt re-downloads
return
}
// Derive the target package from the download URL (not the filename, which is always // Derive the target package from the download URL (not the filename, which is always
// 'evershelf-update.apk'). The URL contains 'gateway' or 'scale' when installing the // 'evershelf-update.apk'). The URL contains 'gateway' or 'scale' when installing the
// scale gateway; anything else is a kiosk self-update. // scale gateway; anything else is a kiosk self-update.
@@ -1260,6 +1306,7 @@ class KioskActivity : AppCompatActivity() {
.setTitle("⚠️ Conflitto firma APK") .setTitle("⚠️ Conflitto firma APK")
.setMessage("L'app installata usa una firma diversa.\n\nDisinstalla la versione precedente: al termine l'installazione riparte automaticamente.") .setMessage("L'app installata usa una firma diversa.\n\nDisinstalla la versione precedente: al termine l'installazione riparte automaticamente.")
.setPositiveButton("Disinstalla") { _, _ -> .setPositiveButton("Disinstalla") { _, _ ->
disableKioskLock() // release screen pin so uninstall UI can open
startActivityForResult( startActivityForResult(
Intent(Intent.ACTION_DELETE, android.net.Uri.parse("package:$targetPkg")), Intent(Intent.ACTION_DELETE, android.net.Uri.parse("package:$targetPkg")),
UNINSTALL_REQUEST UNINSTALL_REQUEST
@@ -1275,7 +1322,7 @@ class KioskActivity : AppCompatActivity() {
) ?: "status=$status" ) ?: "status=$status"
setInstallUI( setInstallUI(
"\u274C", "\u274C",
getString(R.string.install_error_download), getString(R.string.install_error_install),
msg, msg,
0xFFf87171.toInt(), 0xFFf87171.toInt(),
btnEnabled = true, btnEnabled = true,
@@ -1284,9 +1331,7 @@ class KioskActivity : AppCompatActivity() {
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) } runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.reportMessage("install_failure", ErrorReporter.reportMessage("install_failure",
"PackageInstaller status=$status msg=$msg pkg=$targetPkg") "PackageInstaller status=$status msg=$msg pkg=$targetPkg")
// Generic failure on an already-installed package often means // Generic failure on an already-installed package: offer uninstall as last resort.
// a signature conflict with the old version. Offer uninstall as
// last resort (only after the system installer already failed).
val pkgInstalled = try { val pkgInstalled = try {
packageManager.getPackageInfo(targetPkg, 0); true packageManager.getPackageInfo(targetPkg, 0); true
} catch (_: Exception) { false } } catch (_: Exception) { false }
@@ -1301,6 +1346,7 @@ class KioskActivity : AppCompatActivity() {
"bisogna prima disinstallarla.\n\n" + "bisogna prima disinstallarla.\n\n" +
"Disinstalla ora e riprova automaticamente?") "Disinstalla ora e riprova automaticamente?")
.setPositiveButton("Disinstalla e riprova") { _, _ -> .setPositiveButton("Disinstalla e riprova") { _, _ ->
disableKioskLock() // release screen pin so uninstall UI can open
startActivityForResult( startActivityForResult(
Intent(Intent.ACTION_DELETE, Intent(Intent.ACTION_DELETE,
android.net.Uri.parse("package:$targetPkg")), android.net.Uri.parse("package:$targetPkg")),
@@ -1464,6 +1510,7 @@ class KioskActivity : AppCompatActivity() {
.setTitle("⚠️ Installazione non riuscita") .setTitle("⚠️ Installazione non riuscita")
.setMessage("Se hai visto un errore di conflitto firma, devi disinstallare la versione precedente.\n\nDisinstalla ora? L'installazione ripartirà automaticamente.") .setMessage("Se hai visto un errore di conflitto firma, devi disinstallare la versione precedente.\n\nDisinstalla ora? L'installazione ripartirà automaticamente.")
.setPositiveButton("Disinstalla") { _, _ -> .setPositiveButton("Disinstalla") { _, _ ->
disableKioskLock() // release screen pin so uninstall UI can open
startActivityForResult( startActivityForResult(
Intent(Intent.ACTION_DELETE, android.net.Uri.parse("package:$pkg")), Intent(Intent.ACTION_DELETE, android.net.Uri.parse("package:$pkg")),
UNINSTALL_REQUEST UNINSTALL_REQUEST
@@ -1474,12 +1521,16 @@ class KioskActivity : AppCompatActivity() {
} }
} }
} }
// Returned from uninstall screen — auto-retry the install with the saved APK file. // Returned from uninstall screen — re-enable kiosk lock, then auto-retry install.
if (requestCode == UNINSTALL_REQUEST) { if (requestCode == UNINSTALL_REQUEST) {
enableKioskLock()
val f = pendingInstallFile val f = pendingInstallFile
val pkg = pendingInstallPkg val pkg = pendingInstallPkg
if (f != null && f.exists() && pkg.isNotEmpty()) { if (f != null && f.exists() && pkg.isNotEmpty()) {
installWithPackageInstaller(f, pkg) // Small delay: give PackageManager time to finish processing the removal.
Handler(Looper.getMainLooper()).postDelayed({
installWithPackageInstaller(f, pkg)
}, 600)
} }
} }
} }
@@ -28,6 +28,7 @@
<string name="install_success_detail">Die App wurde aktualisiert.</string> <string name="install_success_detail">Die App wurde aktualisiert.</string>
<string name="install_error_download">Download fehlgeschlagen</string> <string name="install_error_download">Download fehlgeschlagen</string>
<string name="install_error_download_detail">Verbindung prüfen und erneut versuchen.</string> <string name="install_error_download_detail">Verbindung prüfen und erneut versuchen.</string>
<string name="install_error_install">Installation fehlgeschlagen</string>
<string name="install_perm_detail">Aktiviere \'Unbekannte Apps installieren\' in den Einstellungen, dann komm zurück.</string> <string name="install_perm_detail">Aktiviere \'Unbekannte Apps installieren\' in den Einstellungen, dann komm zurück.</string>
<string name="install_btn_retry">↩ Nochmal versuchen</string> <string name="install_btn_retry">↩ Nochmal versuchen</string>
@@ -28,6 +28,7 @@
<string name="install_success_detail">L\'app è stata aggiornata.</string> <string name="install_success_detail">L\'app è stata aggiornata.</string>
<string name="install_error_download">Download fallito</string> <string name="install_error_download">Download fallito</string>
<string name="install_error_download_detail">Controlla la connessione e riprova.</string> <string name="install_error_download_detail">Controlla la connessione e riprova.</string>
<string name="install_error_install">Installazione fallita</string>
<string name="install_perm_detail">Abilita \'Installa app sconosciute\' nelle impostazioni, poi torna qui.</string> <string name="install_perm_detail">Abilita \'Installa app sconosciute\' nelle impostazioni, poi torna qui.</string>
<string name="install_btn_retry">↩ Riprova</string> <string name="install_btn_retry">↩ Riprova</string>
@@ -27,6 +27,7 @@
<string name="install_success_detail">L\'app è stata aggiornata.</string> <string name="install_success_detail">L\'app è stata aggiornata.</string>
<string name="install_error_download">Download fallito</string> <string name="install_error_download">Download fallito</string>
<string name="install_error_download_detail">Controlla la connessione e riprova.</string> <string name="install_error_download_detail">Controlla la connessione e riprova.</string>
<string name="install_error_install">Installazione fallita</string>
<string name="install_perm_detail">Abilita \'Installa app sconosciute\' nelle impostazioni, poi torna qui.</string> <string name="install_perm_detail">Abilita \'Installa app sconosciute\' nelle impostazioni, poi torna qui.</string>
<string name="install_btn_retry">↩ Riprova</string> <string name="install_btn_retry">↩ Riprova</string>