fix(kiosk): fix APK install failure — session lifecycle, error details, issue reporting

- KioskActivity: remove .use{} on PackageInstaller session to prevent premature
  session close causing STATUS_FAILURE=1; align with SetupActivity pattern
- SetupActivity: show full diagnostic info (status code + human-readable hint,
  device, Android version) in the UI card instead of just 'status=1'
- SetupActivity: use Build.PRODUCT/BOARD fallback when MANUFACTURER='unknown'
- ErrorReporter: add forceReport param to bypass in-session dedup for retries
- ErrorReporter: include Android SDK version in deviceInfo; fallback for
  'unknown' MANUFACTURER/MODEL using PRODUCT/HARDWARE/BOARD
This commit is contained in:
dadaloop82
2026-05-05 05:32:31 +00:00
parent fc47cd8c27
commit abeb87c536
3 changed files with 170 additions and 78 deletions
@@ -64,7 +64,14 @@ object ErrorReporter {
val pi = context.packageManager.getPackageInfo(context.packageName, 0)
appVersion = pi.versionName ?: "unknown"
} catch (_: Exception) {}
deviceInfo = "${Build.MANUFACTURER} ${Build.MODEL} (Android ${Build.VERSION.RELEASE})"
deviceInfo = buildString {
val mfr = Build.MANUFACTURER.takeIf { it.isNotBlank() && it != "unknown" }
?: Build.PRODUCT.takeIf { it.isNotBlank() && it != "unknown" }
?: Build.BOARD
val model = Build.MODEL.takeIf { it.isNotBlank() && it != "unknown" }
?: Build.HARDWARE
append("$mfr $model (Android ${Build.VERSION.RELEASE}/${Build.VERSION.SDK_INT})")
}
// Send any crash that was saved to prefs during a previous session
sendPendingCrash()
@@ -110,15 +117,17 @@ object ErrorReporter {
/**
* Report a non-exception message (e.g. WebView page error, network failure).
* @param forceReport if true, bypasses the in-session dedup so retries are always sent.
*/
fun reportMessage(
type: String,
message: String,
extra: Map<String, Any?> = emptyMap()
extra: Map<String, Any?> = emptyMap(),
forceReport: Boolean = false
) {
val ctx = mutableMapOf<String, Any?>("device" to deviceInfo)
ctx.putAll(extra)
reportAsync(type = type, message = message, stack = "", context = ctx)
reportAsync(type = type, message = message, stack = "", context = ctx, force = forceReport)
}
// ── Internal ─────────────────────────────────────────────────────────────
@@ -128,11 +137,15 @@ object ErrorReporter {
return key.hashCode().toString(16)
}
private fun reportAsync(type: String, message: String, stack: String, context: Map<String, Any?>) {
private fun reportAsync(type: String, message: String, stack: String, context: Map<String, Any?>, force: Boolean = false) {
val fp = fingerprint(type, message)
if (!force) {
synchronized(sentFingerprints) {
if (!sentFingerprints.add(fp)) return // already reported this session
}
} else {
synchronized(sentFingerprints) { sentFingerprints.add(fp) }
}
executor.execute { doPost(type, message, stack, context) }
}
@@ -644,22 +644,29 @@ class KioskActivity : AppCompatActivity() {
try {
val pi = packageManager.packageInstaller
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
params.setAppPackageName(targetPkg)
// Note: setAppPackageName() is intentionally omitted — it causes STATUS_FAILURE (1)
// on some OEM/Android versions even when the package name is correct.
val sessionId = pi.createSession(params)
pi.openSession(sessionId).use { session ->
val session = pi.openSession(sessionId)
try {
file.inputStream().use { input ->
session.openWrite("package", 0, file.length()).use { out ->
input.copyTo(out)
session.fsync(out)
}
}
} catch (e: Exception) {
try { session.abandon() } catch (_: Exception) {}
throw e
}
// Do NOT close() the session after commit — it is now owned by the system.
val action = "it.dadaloop.evershelf.kiosk.INSTALL_RESULT_$sessionId"
val resultReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) {
unregisterReceiver(this)
val status = intent?.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) ?: PackageInstaller.STATUS_FAILURE
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
// Do NOT unregister here — the final result arrives as a second broadcast
@Suppress("DEPRECATION")
val confirmIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
intent?.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
@@ -670,9 +677,13 @@ class KioskActivity : AppCompatActivity() {
setInstallUI("\u23F3", getString(R.string.install_installing), getString(R.string.install_confirm_detail), 0xFF94a3b8.toInt(), btnEnabled = false)
@Suppress("DEPRECATION")
startActivityForResult(confirmIntent, INSTALL_CONFIRM_REQUEST)
} else {
unregisterReceiver(this)
setInstallUI("\u274C", getString(R.string.install_error_install), "No confirmation intent", 0xFFf87171.toInt(), btnEnabled = true, progress = -2)
}
}
PackageInstaller.STATUS_SUCCESS -> {
unregisterReceiver(this)
setInstallUI("\u2705", getString(R.string.install_success), getString(R.string.install_success_detail), 0xFF34d399.toInt(), btnEnabled = false, progress = -2)
Handler(Looper.getMainLooper()).postDelayed({
updateBanner.visibility = View.GONE
@@ -681,6 +692,7 @@ class KioskActivity : AppCompatActivity() {
}
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
PackageInstaller.STATUS_FAILURE_CONFLICT -> {
unregisterReceiver(this)
runOnUiThread {
pendingInstallFile = file
pendingInstallPkg = targetPkg
@@ -695,19 +707,46 @@ class KioskActivity : AppCompatActivity() {
.setNegativeButton("Annulla", null).show()
}
}
else -> {
val msg = intent?.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "status=$status"
setInstallUI("\u274C", getString(R.string.install_error_install), msg, 0xFFf87171.toInt(), btnEnabled = true, progress = -2)
-1 /* STATUS_FAILURE_ABORTED */ -> {
unregisterReceiver(this)
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.reportMessage("install_failure", "PackageInstaller status=$status msg=$msg pkg=$targetPkg")
}
else -> {
unregisterReceiver(this)
val msg = intent?.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: ""
val deviceLabel = buildDeviceLabel()
val diagInfo = buildString {
appendLine("Status: $status${installStatusHint(status)}")
if (msg.isNotEmpty()) appendLine("Msg: $msg")
appendLine("PKG: $targetPkg")
appendLine("APK: ${file.length() / 1024} KB")
appendLine("Android: ${Build.VERSION.SDK_INT} (${Build.VERSION.RELEASE})")
appendLine("Device: $deviceLabel")
}
setInstallUI("\u274C", getString(R.string.install_error_install),
diagInfo.trim(), 0xFFf87171.toInt(), btnEnabled = true, progress = -2)
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.reportMessage(
"install_failure",
"PackageInstaller status=$status pkg=$targetPkg android=${Build.VERSION.SDK_INT}",
mapOf(
"pkg" to targetPkg,
"status" to status,
"msg" to msg,
"apk_kb" to (file.length() / 1024),
"android" to Build.VERSION.SDK_INT,
"device" to deviceLabel
),
forceReport = true
)
val pkgInstalled = try { packageManager.getPackageInfo(targetPkg, 0); true } catch (_: Exception) { false }
if (pkgInstalled) {
runOnUiThread {
pendingInstallFile = file
pendingInstallPkg = targetPkg
androidx.appcompat.app.AlertDialog.Builder(this@KioskActivity)
.setTitle("⚠️ Installazione fallita")
.setMessage("Installazione fallita (status=$status).\n\nDisinstalla la versione precedente e riprova?")
.setTitle("⚠️ Installazione fallita (status=$status)")
.setMessage(diagInfo.trim())
.setPositiveButton("Disinstalla e riprova") { _, _ ->
disableKioskLock()
@Suppress("DEPRECATION")
@@ -728,15 +767,36 @@ class KioskActivity : AppCompatActivity() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
session.commit(pi2.intentSender)
}
setInstallUI("\u23F3", getString(R.string.install_installing), getString(R.string.install_installing), 0xFF94a3b8.toInt(), btnEnabled = false, progress = -1)
} catch (e: Exception) {
setInstallUI("\u274C", getString(R.string.install_error_download), e.message ?: "", 0xFFf87171.toInt(), btnEnabled = true, progress = -2)
setInstallUI("\u274C", getString(R.string.install_error_install), e.message ?: "", 0xFFf87171.toInt(), btnEnabled = true, progress = -2)
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.reportMessage("install_packager_exception", "installWithPackageInstaller exception for $targetPkg: ${e.message}")
ErrorReporter.reportMessage("install_packager_exception",
"installWithPackageInstaller exception for $targetPkg: ${e.message}",
forceReport = true)
}
}
private fun buildDeviceLabel(): String {
val mfr = Build.MANUFACTURER.takeIf { it.isNotBlank() && it != "unknown" }
?: Build.PRODUCT.takeIf { it.isNotBlank() && it != "unknown" }
?: Build.BOARD
val model = Build.MODEL.takeIf { it.isNotBlank() && it != "unknown" }
?: Build.HARDWARE
return "$mfr $model"
}
private fun installStatusHint(status: Int): String = when (status) {
1 -> "Errore generico (APK incompatibile con questo dispositivo o versione Android)"
2 -> "Bloccato da policy o da un'altra app in corso"
3 -> "Annullato dall'utente"
4 -> "APK non valido o corrotto"
5 -> "Conflitto: versione precedente con firma diversa"
6 -> "Spazio insufficiente"
7 -> "Incompatibile con questa versione di Android"
else -> "Errore sconosciuto"
}
// ── Error Page ────────────────────────────────────────────────────────
private fun errorPageHtml(): String {
@@ -913,28 +913,48 @@ class SetupActivity : AppCompatActivity() {
unregisterReceiver(this)
val msg = intent?.getStringExtra(
android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE
) ?: "status=$status"
) ?: ""
val deviceLabel = buildString {
val mfr = Build.MANUFACTURER.takeIf { it.isNotBlank() && it != "unknown" }
?: Build.PRODUCT.takeIf { it.isNotBlank() && it != "unknown" }
?: Build.BOARD
val model = Build.MODEL.takeIf { it.isNotBlank() && it != "unknown" }
?: Build.HARDWARE
append("$mfr $model")
}
val hint = when (status) {
1 -> "APK incompatibile con questo dispositivo o versione Android"
2 -> "Bloccato da policy o da un'altra installazione in corso"
3 -> "Annullato"
4 -> "APK non valido o corrotto"
5 -> "Conflitto: versione precedente con firma diversa"
6 -> "Spazio insufficiente"
7 -> "Incompatibile con questa versione di Android"
else -> "Errore sconosciuto"
}
val diagInfo = buildString {
appendLine("Status: $status")
appendLine("Msg: $msg")
appendLine("PKG: $targetPkg")
appendLine("Status $status: $hint")
if (msg.isNotEmpty()) appendLine("Dettaglio: $msg")
appendLine("Pacchetto: $targetPkg")
appendLine("APK: ${file.length() / 1024} KB")
appendLine("Android: ${Build.VERSION.SDK_INT} (${Build.VERSION.RELEASE})")
appendLine("Device: ${Build.MANUFACTURER} ${Build.MODEL}")
appendLine("Dispositivo: $deviceLabel")
}
setGatewayUI("", getString(R.string.install_error_install),
msg, 0xFFf87171.toInt())
diagInfo.trim(), 0xFFf87171.toInt())
ErrorReporter.reportMessage(
"install_failure",
"PackageInstaller failed: status=$status msg=$msg",
"PackageInstaller status=$status pkg=$targetPkg android=${Build.VERSION.SDK_INT}",
mapOf(
"pkg" to targetPkg,
"status" to status,
"hint" to hint,
"msg" to msg,
"apk_kb" to (file.length() / 1024),
"android" to Build.VERSION.SDK_INT,
"device" to "${Build.MANUFACTURER} ${Build.MODEL}"
)
"device" to deviceLabel
),
forceReport = true
)
val pkgInstalled = try {
packageManager.getPackageInfo(targetPkg, 0); true
@@ -943,7 +963,6 @@ class SetupActivity : AppCompatActivity() {
if (pkgInstalled) {
offerUninstallAndRetry(file, targetPkg)
} else {
// Show diagnostic dialog with Retry button
AlertDialog.Builder(this@SetupActivity)
.setTitle("❌ Installazione fallita (status=$status)")
.setMessage(diagInfo.trim())