fix(kiosk): real-time download progress bar + ErrorReporter on failures

Problem: tapping 'Aggiorna Scale Gateway' gave no visible feedback after
the button was pressed — user could not tell if the download was
happening, stuck, or had silently failed.

Changes:
- layout: add horizontal ProgressBar (determinate) + percentage TextView
  inside the wizard step-3 status card
- layout: add thin ProgressBar (4 dp) at the bottom of the update banner
  (banner changed to vertical orientation to accommodate it)
- startDownloadProgressPoll(downloadId): polls DownloadManager every
  500 ms, reads COLUMN_BYTES_DOWNLOADED_SO_FAR and COLUMN_TOTAL_SIZE_BYTES,
  updates status card + banner with 'Download: 45% (18.2 MB / 40.5 MB)'
- setInstallUI(): new 'progress' parameter (-2 = hide, -1 = indeterminate,
  0-100 = determinate) and 'progressText' for the label under the bar
  — both bars updated in sync
- Status transitions now visible:
     Download: 45% [====----] 18.2 MB / 40.5 MB
     Installazione in corso… [~~~~] (indeterminate)
     + 'Conferma nel dialog…'
     Installato! → bar hidden, gateway status re-checked after 3 s
     + error detail → bar hidden, button re-enabled as '↩ Riprova'
- All error paths (download fail, PackageInstaller exception, installer
  failure status) now call ErrorReporter.report() → GitHub Issue created
  automatically so failures are tracked without user intervention
- Dismiss button also cancels the progress poll + hides the bar
This commit is contained in:
dadaloop82
2026-05-03 19:07:19 +00:00
parent 7d8132a743
commit fe633c97cb
2 changed files with 202 additions and 48 deletions
@@ -41,6 +41,7 @@ import android.widget.LinearLayout
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -79,16 +80,22 @@ class KioskActivity : AppCompatActivity() {
private lateinit var scaleStatusDetail: TextView private lateinit var scaleStatusDetail: TextView
private lateinit var scaleQuestionLayout: LinearLayout private lateinit var scaleQuestionLayout: LinearLayout
private lateinit var step3BottomButtons: LinearLayout private lateinit var step3BottomButtons: LinearLayout
// Update banner (native, shown at the top over the WebView) // Update banner
private lateinit var updateBanner: LinearLayout private lateinit var updateBanner: LinearLayout
private lateinit var tvUpdateMessage: TextView private lateinit var tvUpdateMessage: TextView
private lateinit var btnInstallUpdate: MaterialButton private lateinit var btnInstallUpdate: MaterialButton
private lateinit var btnDismissUpdate: MaterialButton private lateinit var btnDismissUpdate: MaterialButton
private lateinit var downloadProgressBar: ProgressBar
private lateinit var downloadProgressText: TextView
private lateinit var bannerProgressBar: ProgressBar
private var pendingApkDownloadUrl: String = "" private var pendingApkDownloadUrl: String = ""
private var pendingInstallFile: java.io.File? = null private var pendingInstallFile: java.io.File? = null
private var pendingInstallPkg: String = "" private var pendingInstallPkg: String = ""
/** The button that triggered the current download/install — updated throughout the flow. */ /** The button that triggered the current download/install — updated throughout the flow. */
private var activeInstallBtn: MaterialButton? = null private var activeInstallBtn: MaterialButton? = null
/** Handler for the 500 ms download-progress polling loop. */
private val pollHandler = Handler(Looper.getMainLooper())
private var activeDownloadId: Long = -1
// Triple-tap to exit // Triple-tap to exit
private var tapCount = 0 private var tapCount = 0
@@ -175,11 +182,18 @@ class KioskActivity : AppCompatActivity() {
step3BottomButtons = findViewById(R.id.step3BottomButtons) step3BottomButtons = findViewById(R.id.step3BottomButtons)
// Update banner // Update banner
updateBanner = findViewById(R.id.updateBanner) updateBanner = findViewById(R.id.updateBanner)
tvUpdateMessage = findViewById(R.id.tvUpdateMessage) tvUpdateMessage = findViewById(R.id.tvUpdateMessage)
btnInstallUpdate = findViewById(R.id.btnInstallUpdate) btnInstallUpdate = findViewById(R.id.btnInstallUpdate)
btnDismissUpdate = findViewById(R.id.btnDismissUpdate) btnDismissUpdate = findViewById(R.id.btnDismissUpdate)
btnDismissUpdate.setOnClickListener { updateBanner.visibility = View.GONE } downloadProgressBar = findViewById(R.id.downloadProgressBar)
downloadProgressText = findViewById(R.id.downloadProgressText)
bannerProgressBar = findViewById(R.id.bannerProgressBar)
btnDismissUpdate.setOnClickListener {
updateBanner.visibility = View.GONE
bannerProgressBar.visibility = View.GONE
pollHandler.removeCallbacksAndMessages(null)
}
btnInstallUpdate.setOnClickListener { btnInstallUpdate.setOnClickListener {
activeInstallBtn = btnInstallUpdate activeInstallBtn = btnInstallUpdate
triggerApkDownload(pendingApkDownloadUrl) triggerApkDownload(pendingApkDownloadUrl)
@@ -520,10 +534,14 @@ class KioskActivity : AppCompatActivity() {
* @param detail Secondary detail line (status card only) * @param detail Secondary detail line (status card only)
* @param color ARGB color for the detail text * @param color ARGB color for the detail text
* @param btnEnabled Whether to re-enable the active button after this state * @param btnEnabled Whether to re-enable the active button after this state
* @param progress 0-100 to show determinate bar; -1 = indeterminate; -2 = hide bar
* @param progressText optional text shown under the bar (e.g. "18.2 MB / 40.5 MB")
*/ */
private fun setInstallUI( private fun setInstallUI(
icon: String, title: String, detail: String, color: Int, icon: String, title: String, detail: String, color: Int,
btnEnabled: Boolean = false btnEnabled: Boolean = false,
progress: Int = -2,
progressText: String = ""
) = runOnUiThread { ) = runOnUiThread {
// Wizard status card (step 3) // Wizard status card (step 3)
val statusCard = try { findViewById<LinearLayout>(R.id.scaleStatusCard) } catch (_: Exception) { null } val statusCard = try { findViewById<LinearLayout>(R.id.scaleStatusCard) } catch (_: Exception) { null }
@@ -532,11 +550,42 @@ class KioskActivity : AppCompatActivity() {
scaleStatusText.text = title scaleStatusText.text = title
scaleStatusDetail.text = detail scaleStatusDetail.text = detail
scaleStatusDetail.setTextColor(color) scaleStatusDetail.setTextColor(color)
when {
progress == -2 -> {
downloadProgressBar.visibility = View.GONE
downloadProgressText.visibility = View.GONE
}
progress == -1 -> {
downloadProgressBar.isIndeterminate = true
downloadProgressBar.visibility = View.VISIBLE
downloadProgressText.text = progressText
downloadProgressText.visibility = if (progressText.isEmpty()) View.GONE else View.VISIBLE
}
else -> {
downloadProgressBar.isIndeterminate = false
downloadProgressBar.progress = progress
downloadProgressBar.visibility = View.VISIBLE
downloadProgressText.text = progressText
downloadProgressText.visibility = if (progressText.isEmpty()) View.GONE else View.VISIBLE
}
}
} }
// Update banner (kiosk / gateway auto-update outside wizard) // Update banner (kiosk / gateway auto-update outside wizard)
if (updateBanner.visibility == View.VISIBLE) { if (updateBanner.visibility == View.VISIBLE) {
tvUpdateMessage.text = "$icon $title" tvUpdateMessage.text = "$icon $title"
if (detail.isNotEmpty()) tvUpdateMessage.text = "${tvUpdateMessage.text}\n$detail" if (detail.isNotEmpty()) tvUpdateMessage.text = "${tvUpdateMessage.text}\n$detail"
when {
progress == -2 -> bannerProgressBar.visibility = View.GONE
progress == -1 -> {
bannerProgressBar.isIndeterminate = true
bannerProgressBar.visibility = View.VISIBLE
}
else -> {
bannerProgressBar.isIndeterminate = false
bannerProgressBar.progress = progress
bannerProgressBar.visibility = View.VISIBLE
}
}
} }
// Button state // Button state
val btn = activeInstallBtn val btn = activeInstallBtn
@@ -546,6 +595,46 @@ class KioskActivity : AppCompatActivity() {
} }
} }
/**
* Polls DownloadManager every 500 ms to report actual byte-level progress
* in the status card and banner. Stops automatically when download is no
* longer RUNNING or PENDING.
*/
private fun startDownloadProgressPoll(downloadId: Long) {
activeDownloadId = downloadId
pollHandler.removeCallbacksAndMessages(null)
fun tick() {
if (activeDownloadId != downloadId) return // superseded download
val dm = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
val c = dm.query(DownloadManager.Query().setFilterById(downloadId))
if (!c.moveToFirst()) { c.close(); return }
val status = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
if (status == DownloadManager.STATUS_RUNNING ||
status == DownloadManager.STATUS_PENDING) {
val dl = c.getLong(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
val tot = c.getLong(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
c.close()
val pct = if (tot > 0) (dl * 100 / tot).toInt() else 0
val dlMb = dl / 1_048_576f
val totMb = tot / 1_048_576f
val txt = if (tot > 0) "%.1f MB / %.1f MB".format(dlMb, totMb) else ""
setInstallUI(
"\u23F3",
getString(R.string.install_downloading) + if (tot > 0) " ($pct%)" else "",
txt,
0xFF94a3b8.toInt(),
btnEnabled = false,
progress = pct,
progressText = txt
)
pollHandler.postDelayed({ tick() }, 500)
} else {
c.close() // terminal state — BroadcastReceiver will handle success/failure
}
}
pollHandler.post { tick() }
}
// ── Connection Test ─────────────────────────────────────────────────── // ── Connection Test ───────────────────────────────────────────────────
private fun testConnection() { private fun testConnection() {
@@ -947,6 +1036,7 @@ class KioskActivity : AppCompatActivity() {
setMimeType("application/vnd.android.package-archive") setMimeType("application/vnd.android.package-archive")
} }
val downloadId = dm.enqueue(req) val downloadId = dm.enqueue(req)
startDownloadProgressPoll(downloadId)
val receiver = object : BroadcastReceiver() { val receiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) { override fun onReceive(ctx: Context?, intent: Intent?) {
@@ -963,23 +1053,31 @@ class KioskActivity : AppCompatActivity() {
} }
c.close() c.close()
if (ok) { if (ok) {
pollHandler.removeCallbacksAndMessages(null)
activeDownloadId = -1
setInstallUI( setInstallUI(
"\u23F3", "\u23F3",
getString(R.string.install_installing), getString(R.string.install_installing),
getString(R.string.install_installing), getString(R.string.install_installing),
0xFF94a3b8.toInt(), 0xFF94a3b8.toInt(),
btnEnabled = false btnEnabled = false,
progress = -1
) )
installApk(destFile) installApk(destFile)
} else { } else {
pollHandler.removeCallbacksAndMessages(null)
activeDownloadId = -1
setInstallUI( setInstallUI(
"\u274C", "\u274C",
getString(R.string.install_error_download), getString(R.string.install_error_download),
getString(R.string.install_error_download_detail), getString(R.string.install_error_download_detail),
0xFFf87171.toInt(), 0xFFf87171.toInt(),
btnEnabled = true btnEnabled = true,
progress = -2
) )
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) } runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.report(this@KioskActivity, "install_download_failed",
"DownloadManager returned failure for URL: $apkUrl")
} }
} }
} }
@@ -1073,13 +1171,15 @@ class KioskActivity : AppCompatActivity() {
getString(R.string.install_success), getString(R.string.install_success),
getString(R.string.install_success_detail), getString(R.string.install_success_detail),
0xFF34d399.toInt(), 0xFF34d399.toInt(),
btnEnabled = false btnEnabled = false,
progress = -2
) )
// Re-check gateway status after 3 s so the wizard reflects reality // Re-check gateway status after 3 s so the wizard reflects reality
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
val card = try { findViewById<LinearLayout>(R.id.scaleStatusCard) } catch (_: Exception) { null } val card = try { findViewById<LinearLayout>(R.id.scaleStatusCard) } catch (_: Exception) { null }
if (card?.visibility == View.VISIBLE) checkGatewayStatus() if (card?.visibility == View.VISIBLE) checkGatewayStatus()
updateBanner.visibility = View.GONE updateBanner.visibility = View.GONE
bannerProgressBar.visibility = View.GONE
}, 3000) }, 3000)
} }
android.content.pm.PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, android.content.pm.PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
@@ -1110,9 +1210,12 @@ class KioskActivity : AppCompatActivity() {
getString(R.string.install_error_download), getString(R.string.install_error_download),
msg, msg,
0xFFf87171.toInt(), 0xFFf87171.toInt(),
btnEnabled = true btnEnabled = true,
progress = -2
) )
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) } runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.report(this@KioskActivity, "install_failure",
"PackageInstaller status=$status msg=$msg pkg=$targetPkg")
} }
} }
} }
@@ -1134,7 +1237,8 @@ class KioskActivity : AppCompatActivity() {
getString(R.string.install_installing), getString(R.string.install_installing),
getString(R.string.install_installing), getString(R.string.install_installing),
0xFF94a3b8.toInt(), 0xFF94a3b8.toInt(),
btnEnabled = false btnEnabled = false,
progress = -1
) )
} catch (e: Exception) { } catch (e: Exception) {
setInstallUI( setInstallUI(
@@ -1142,9 +1246,12 @@ class KioskActivity : AppCompatActivity() {
getString(R.string.install_error_download), getString(R.string.install_error_download),
e.message ?: "", e.message ?: "",
0xFFf87171.toInt(), 0xFFf87171.toInt(),
btnEnabled = true btnEnabled = true,
progress = -2
) )
runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) } runOnUiThread { activeInstallBtn?.text = getString(R.string.install_btn_retry) }
ErrorReporter.report(this, "install_packager_exception",
"installWithPackageInstaller exception for $targetPkg: ${e.message}")
} }
} }
@@ -1241,12 +1348,14 @@ class KioskActivity : AppCompatActivity() {
getString(R.string.install_success), getString(R.string.install_success),
getString(R.string.install_success_detail), getString(R.string.install_success_detail),
0xFF34d399.toInt(), 0xFF34d399.toInt(),
btnEnabled = false btnEnabled = false,
progress = -2
) )
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
val card = try { findViewById<LinearLayout>(R.id.scaleStatusCard) } catch (_: Exception) { null } val card = try { findViewById<LinearLayout>(R.id.scaleStatusCard) } catch (_: Exception) { null }
if (card?.visibility == View.VISIBLE) checkGatewayStatus() if (card?.visibility == View.VISIBLE) checkGatewayStatus()
updateBanner.visibility = View.GONE updateBanner.visibility = View.GONE
bannerProgressBar.visibility = View.GONE
}, 3000) }, 3000)
} }
// Not OK = install failed (possibly signature conflict). // Not OK = install failed (possibly signature conflict).
@@ -398,6 +398,31 @@
android:textColor="#64748b" android:textColor="#64748b"
android:textSize="13sp" android:textSize="13sp"
android:gravity="center" /> android:gravity="center" />
<!-- Download / install progress bar — shown only during active download -->
<ProgressBar
android:id="@+id/downloadProgressBar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_marginTop="14dp"
android:layout_marginBottom="2dp"
android:progressTint="#7c3aed"
android:progressBackgroundTint="#334155"
android:max="100"
android:progress="0"
android:indeterminate="false"
android:visibility="gone" />
<TextView
android:id="@+id/downloadProgressText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textColor="#94a3b8"
android:textSize="12sp"
android:gravity="center"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
<!-- Bottom nav (Back / Launch) — hidden until user answers the question --> <!-- Bottom nav (Back / Launch) — hidden until user answers the question -->
@@ -479,46 +504,66 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="top" android:layout_gravity="top"
android:orientation="horizontal" android:orientation="vertical"
android:background="#1e293b" android:background="#1e293b"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center_vertical"
android:visibility="gone"> android:visibility="gone">
<TextView <!-- Banner row: message + buttons -->
android:id="@+id/tvUpdateMessage" <LinearLayout
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="horizontal"
android:textColor="#fbbf24" android:paddingStart="16dp"
android:textSize="13sp" android:paddingEnd="8dp"
android:text="" android:paddingTop="10dp"
android:drawablePadding="6dp" /> android:paddingBottom="10dp"
android:gravity="center_vertical">
<com.google.android.material.button.MaterialButton <TextView
android:id="@+id/btnInstallUpdate" android:id="@+id/tvUpdateMessage"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_weight="1"
android:text="⬇ Scarica" android:textColor="#fbbf24"
android:textSize="12sp" android:textSize="13sp"
android:textColor="#1e293b" android:text=""
android:backgroundTint="#fbbf24" android:drawablePadding="6dp" />
style="@style/Widget.MaterialComponents.Button" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnDismissUpdate" android:id="@+id/btnInstallUpdate"
android:layout_width="36dp" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="8dp"
android:text="" android:text="⬇ Scarica"
android:textSize="14sp" android:textSize="12sp"
android:textColor="#94a3b8" android:textColor="#1e293b"
android:backgroundTint="@android:color/transparent" android:backgroundTint="#fbbf24"
style="@style/Widget.MaterialComponents.Button.TextButton" /> style="@style/Widget.MaterialComponents.Button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDismissUpdate"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="4dp"
android:text="✕"
android:textSize="14sp"
android:textColor="#94a3b8"
android:backgroundTint="@android:color/transparent"
style="@style/Widget.MaterialComponents.Button.TextButton" />
</LinearLayout>
<!-- Thin progress bar at the bottom of the banner — visible during download/install -->
<ProgressBar
android:id="@+id/bannerProgressBar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="4dp"
android:progressTint="#7c3aed"
android:progressBackgroundTint="#334155"
android:max="100"
android:progress="0"
android:indeterminate="false"
android:visibility="gone" />
</LinearLayout> </LinearLayout>