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:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user