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
}
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)
}
@@ -728,11 +730,32 @@ class KioskActivity : AppCompatActivity() {
conn.requestMethod = "GET"
val code = conn.responseCode
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 {
if (code in 200..399) {
showUrlStatus("✓ Connected successfully!", true)
if (apiOk) {
showUrlStatus("✅ Server EverShelf trovato e API attiva!", true)
} 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) }
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
// 'evershelf-update.apk'). The URL contains 'gateway' or 'scale' when installing the
// scale gateway; anything else is a kiosk self-update.
@@ -1260,6 +1306,7 @@ class KioskActivity : AppCompatActivity() {
.setTitle("⚠️ Conflitto firma APK")
.setMessage("L'app installata usa una firma diversa.\n\nDisinstalla la versione precedente: al termine l'installazione riparte automaticamente.")
.setPositiveButton("Disinstalla") { _, _ ->
disableKioskLock() // release screen pin so uninstall UI can open
startActivityForResult(
Intent(Intent.ACTION_DELETE, android.net.Uri.parse("package:$targetPkg")),
UNINSTALL_REQUEST
@@ -1275,7 +1322,7 @@ class KioskActivity : AppCompatActivity() {
) ?: "status=$status"
setInstallUI(
"\u274C",
getString(R.string.install_error_download),
getString(R.string.install_error_install),
msg,
0xFFf87171.toInt(),
btnEnabled = true,
@@ -1284,9 +1331,7 @@ class KioskActivity : AppCompatActivity() {
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.reportMessage("install_failure",
"PackageInstaller status=$status msg=$msg pkg=$targetPkg")
// Generic failure on an already-installed package often means
// a signature conflict with the old version. Offer uninstall as
// last resort (only after the system installer already failed).
// Generic failure on an already-installed package: offer uninstall as last resort.
val pkgInstalled = try {
packageManager.getPackageInfo(targetPkg, 0); true
} catch (_: Exception) { false }
@@ -1301,6 +1346,7 @@ class KioskActivity : AppCompatActivity() {
"bisogna prima disinstallarla.\n\n" +
"Disinstalla ora e riprova automaticamente?")
.setPositiveButton("Disinstalla e riprova") { _, _ ->
disableKioskLock() // release screen pin so uninstall UI can open
startActivityForResult(
Intent(Intent.ACTION_DELETE,
android.net.Uri.parse("package:$targetPkg")),
@@ -1464,6 +1510,7 @@ class KioskActivity : AppCompatActivity() {
.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.")
.setPositiveButton("Disinstalla") { _, _ ->
disableKioskLock() // release screen pin so uninstall UI can open
startActivityForResult(
Intent(Intent.ACTION_DELETE, android.net.Uri.parse("package:$pkg")),
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) {
enableKioskLock()
val f = pendingInstallFile
val pkg = pendingInstallPkg
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_error_download">Download fehlgeschlagen</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_btn_retry">↩ Nochmal versuchen</string>
@@ -28,6 +28,7 @@
<string name="install_success_detail">L\'app è stata aggiornata.</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_install">Installazione fallita</string>
<string name="install_perm_detail">Abilita \'Installa app sconosciute\' nelle impostazioni, poi torna qui.</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_error_download">Download fallito</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_btn_retry">↩ Riprova</string>