fix(kiosk): 4 bug fix — uninstall loop, PHP check, APK validation, ErrorReporter init

Bug 1 — Uninstall loop (kiosk lock task blocks system uninstall UI):
  startActivityForResult(ACTION_DELETE) was called while lock task was
  active. The system uninstall activity is not in the lock task whitelist
  so it either silently fails or creates an unresolvable loop.
  Fix: call disableKioskLock() immediately before every ACTION_DELETE
  intent (3 call sites). Call enableKioskLock() at the start of
  onActivityResult(UNINSTALL_REQUEST) before retrying install.
  Added 600 ms delay after uninstall so PackageManager finishes cleanup.

Bug 2 — Step 2 only checks HTTP connectivity, not PHP API:
  testConnection() was checking the root URL only. A generic web server
  could pass while the EverShelf PHP API was absent.
  Fix: after HTTP 200-399 on the root URL, do a second GET to
  /api/?action=check_update and check the response body contains
  'latest_tag'|'webapp_version'|'ok'. Shows:
     Server EverShelf trovato e API attiva!
    ⚠  Server raggiungibile ma API PHP non trovata (codice N)

Bug 3 — STATUS_FAILURE=1 even after uninstall (invalid APK file):
  GitHub DownloadManager follows redirects; if the release asset does
  not exist yet, GitHub returns a 404 HTML page but DownloadManager
  still reports STATUS_SUCCESSFUL. PackageInstaller then tries to parse
  HTML as an APK and returns STATUS_FAILURE=1.
  Fix: validate APK magic bytes (0x504B = 'PK') before calling
  installWithPackageInstaller. If invalid: show error, delete corrupt
  file, send ErrorReporter event, re-enable retry button.
  Also renamed install error string to install_error_install (separate
  from install_error_download) for clarity.

Bug 4 — ErrorReporter.serverBaseUrl empty during wizard install:
  ErrorReporter.init() is called in onCreate() with the saved URL.
  On first setup the URL is typed in step 2 and saved to prefs, but
  ErrorReporter still has serverBaseUrl='' for the rest of that session.
  Any install error in step 3 silently failed to POST.
  Fix: call ErrorReporter.init(this, url) in btnStep2Next immediately
  after prefs.edit().putString(KEY_URL, url) so step 3 has a live URL.
This commit is contained in:
dadaloop82
2026-05-03 20:10:40 +00:00
parent 38eb66cfbf
commit 22e506bd66
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>