From abeb87c536bbbda5a79abcc4e573b3441c1fb646 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Tue, 5 May 2026 05:32:31 +0000 Subject: [PATCH] =?UTF-8?q?fix(kiosk):=20fix=20APK=20install=20failure=20?= =?UTF-8?q?=E2=80=94=20session=20lifecycle,=20error=20details,=20issue=20r?= =?UTF-8?q?eporting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../dadaloop/evershelf/kiosk/ErrorReporter.kt | 25 ++- .../dadaloop/evershelf/kiosk/KioskActivity.kt | 184 ++++++++++++------ .../dadaloop/evershelf/kiosk/SetupActivity.kt | 39 +++- 3 files changed, 170 insertions(+), 78 deletions(-) diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ErrorReporter.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ErrorReporter.kt index 29ae258..cf96700 100644 --- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ErrorReporter.kt +++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ErrorReporter.kt @@ -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 = emptyMap() + extra: Map = emptyMap(), + forceReport: Boolean = false ) { val ctx = mutableMapOf("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,10 +137,14 @@ object ErrorReporter { return key.hashCode().toString(16) } - private fun reportAsync(type: String, message: String, stack: String, context: Map) { + private fun reportAsync(type: String, message: String, stack: String, context: Map, force: Boolean = false) { val fp = fingerprint(type, message) - synchronized(sentFingerprints) { - if (!sentFingerprints.add(fp)) return // already reported this session + 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) } } diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt index 337d2c1..983b212 100644 --- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt +++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt @@ -644,50 +644,110 @@ 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) } } - 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 -> { + } 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?) { + 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) + else intent?.getParcelableExtra(Intent.EXTRA_INTENT) + if (confirmIntent != null) { + pendingInstallFile = file + pendingInstallPkg = targetPkg + setInstallUI("\u23F3", getString(R.string.install_installing), getString(R.string.install_confirm_detail), 0xFF94a3b8.toInt(), btnEnabled = false) @Suppress("DEPRECATION") - val confirmIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - intent?.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) - else intent?.getParcelableExtra(Intent.EXTRA_INTENT) - if (confirmIntent != null) { - pendingInstallFile = file - pendingInstallPkg = targetPkg - setInstallUI("\u23F3", getString(R.string.install_installing), getString(R.string.install_confirm_detail), 0xFF94a3b8.toInt(), btnEnabled = false) - @Suppress("DEPRECATION") - startActivityForResult(confirmIntent, INSTALL_CONFIRM_REQUEST) - } + 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 -> { - 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 - bannerProgressBar.visibility = View.GONE - }, 3000) + } + 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 + bannerProgressBar.visibility = View.GONE + }, 3000) + } + PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, + PackageInstaller.STATUS_FAILURE_CONFLICT -> { + unregisterReceiver(this) + runOnUiThread { + pendingInstallFile = file + pendingInstallPkg = targetPkg + androidx.appcompat.app.AlertDialog.Builder(this@KioskActivity) + .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() + @Suppress("DEPRECATION") + startActivityForResult(Intent(Intent.ACTION_DELETE, android.net.Uri.parse("package:$targetPkg")), UNINSTALL_REQUEST) + } + .setNegativeButton("Annulla", null).show() } - PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, - PackageInstaller.STATUS_FAILURE_CONFLICT -> { + } + -1 /* STATUS_FAILURE_ABORTED */ -> { + unregisterReceiver(this) + runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) } + } + 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("⚠️ Conflitto firma APK") - .setMessage("L'app installata usa una firma diversa.\n\nDisinstalla la versione precedente: al termine l'installazione riparte automaticamente.") - .setPositiveButton("Disinstalla") { _, _ -> + .setTitle("⚠️ Installazione fallita (status=$status)") + .setMessage(diagInfo.trim()) + .setPositiveButton("Disinstalla e riprova") { _, _ -> disableKioskLock() @Suppress("DEPRECATION") startActivityForResult(Intent(Intent.ACTION_DELETE, android.net.Uri.parse("package:$targetPkg")), UNINSTALL_REQUEST) @@ -695,48 +755,48 @@ 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) - runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) } - ErrorReporter.reportMessage("install_failure", "PackageInstaller status=$status msg=$msg pkg=$targetPkg") - 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?") - .setPositiveButton("Disinstalla e riprova") { _, _ -> - disableKioskLock() - @Suppress("DEPRECATION") - startActivityForResult(Intent(Intent.ACTION_DELETE, android.net.Uri.parse("package:$targetPkg")), UNINSTALL_REQUEST) - } - .setNegativeButton("Annulla", null).show() - } - } - } } } } - val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) RECEIVER_NOT_EXPORTED else 0 - registerReceiver(resultReceiver, IntentFilter(action), flags) - val pi2 = PendingIntent.getBroadcast( - this, sessionId, - Intent(action).setPackage(packageName), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - session.commit(pi2.intentSender) } + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) RECEIVER_NOT_EXPORTED else 0 + registerReceiver(resultReceiver, IntentFilter(action), flags) + val pi2 = PendingIntent.getBroadcast( + this, sessionId, + Intent(action).setPackage(packageName), + 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 { diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SetupActivity.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SetupActivity.kt index 53141fb..00cab61 100644 --- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SetupActivity.kt +++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SetupActivity.kt @@ -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())