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:
@@ -41,6 +41,7 @@ import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -79,16 +80,22 @@ class KioskActivity : AppCompatActivity() {
|
||||
private lateinit var scaleStatusDetail: TextView
|
||||
private lateinit var scaleQuestionLayout: 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 tvUpdateMessage: TextView
|
||||
private lateinit var btnInstallUpdate: 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 pendingInstallFile: java.io.File? = null
|
||||
private var pendingInstallPkg: String = ""
|
||||
/** The button that triggered the current download/install — updated throughout the flow. */
|
||||
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
|
||||
private var tapCount = 0
|
||||
@@ -179,7 +186,14 @@ class KioskActivity : AppCompatActivity() {
|
||||
tvUpdateMessage = findViewById(R.id.tvUpdateMessage)
|
||||
btnInstallUpdate = findViewById(R.id.btnInstallUpdate)
|
||||
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 {
|
||||
activeInstallBtn = btnInstallUpdate
|
||||
triggerApkDownload(pendingApkDownloadUrl)
|
||||
@@ -520,10 +534,14 @@ class KioskActivity : AppCompatActivity() {
|
||||
* @param detail Secondary detail line (status card only)
|
||||
* @param color ARGB color for the detail text
|
||||
* @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(
|
||||
icon: String, title: String, detail: String, color: Int,
|
||||
btnEnabled: Boolean = false
|
||||
btnEnabled: Boolean = false,
|
||||
progress: Int = -2,
|
||||
progressText: String = ""
|
||||
) = runOnUiThread {
|
||||
// Wizard status card (step 3)
|
||||
val statusCard = try { findViewById<LinearLayout>(R.id.scaleStatusCard) } catch (_: Exception) { null }
|
||||
@@ -532,11 +550,42 @@ class KioskActivity : AppCompatActivity() {
|
||||
scaleStatusText.text = title
|
||||
scaleStatusDetail.text = detail
|
||||
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)
|
||||
if (updateBanner.visibility == View.VISIBLE) {
|
||||
tvUpdateMessage.text = "$icon $title"
|
||||
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
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
private fun testConnection() {
|
||||
@@ -947,6 +1036,7 @@ class KioskActivity : AppCompatActivity() {
|
||||
setMimeType("application/vnd.android.package-archive")
|
||||
}
|
||||
val downloadId = dm.enqueue(req)
|
||||
startDownloadProgressPoll(downloadId)
|
||||
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||
@@ -963,23 +1053,31 @@ class KioskActivity : AppCompatActivity() {
|
||||
}
|
||||
c.close()
|
||||
if (ok) {
|
||||
pollHandler.removeCallbacksAndMessages(null)
|
||||
activeDownloadId = -1
|
||||
setInstallUI(
|
||||
"\u23F3",
|
||||
getString(R.string.install_installing),
|
||||
getString(R.string.install_installing),
|
||||
0xFF94a3b8.toInt(),
|
||||
btnEnabled = false
|
||||
btnEnabled = false,
|
||||
progress = -1
|
||||
)
|
||||
installApk(destFile)
|
||||
} else {
|
||||
pollHandler.removeCallbacksAndMessages(null)
|
||||
activeDownloadId = -1
|
||||
setInstallUI(
|
||||
"\u274C",
|
||||
getString(R.string.install_error_download),
|
||||
getString(R.string.install_error_download_detail),
|
||||
0xFFf87171.toInt(),
|
||||
btnEnabled = true
|
||||
btnEnabled = true,
|
||||
progress = -2
|
||||
)
|
||||
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_detail),
|
||||
0xFF34d399.toInt(),
|
||||
btnEnabled = false
|
||||
btnEnabled = false,
|
||||
progress = -2
|
||||
)
|
||||
// Re-check gateway status after 3 s so the wizard reflects reality
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
val card = try { findViewById<LinearLayout>(R.id.scaleStatusCard) } catch (_: Exception) { null }
|
||||
if (card?.visibility == View.VISIBLE) checkGatewayStatus()
|
||||
updateBanner.visibility = View.GONE
|
||||
bannerProgressBar.visibility = View.GONE
|
||||
}, 3000)
|
||||
}
|
||||
android.content.pm.PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
|
||||
@@ -1110,9 +1210,12 @@ class KioskActivity : AppCompatActivity() {
|
||||
getString(R.string.install_error_download),
|
||||
msg,
|
||||
0xFFf87171.toInt(),
|
||||
btnEnabled = true
|
||||
btnEnabled = true,
|
||||
progress = -2
|
||||
)
|
||||
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),
|
||||
0xFF94a3b8.toInt(),
|
||||
btnEnabled = false
|
||||
btnEnabled = false,
|
||||
progress = -1
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
setInstallUI(
|
||||
@@ -1142,9 +1246,12 @@ class KioskActivity : AppCompatActivity() {
|
||||
getString(R.string.install_error_download),
|
||||
e.message ?: "",
|
||||
0xFFf87171.toInt(),
|
||||
btnEnabled = true
|
||||
btnEnabled = true,
|
||||
progress = -2
|
||||
)
|
||||
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_detail),
|
||||
0xFF34d399.toInt(),
|
||||
btnEnabled = false
|
||||
btnEnabled = false,
|
||||
progress = -2
|
||||
)
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
val card = try { findViewById<LinearLayout>(R.id.scaleStatusCard) } catch (_: Exception) { null }
|
||||
if (card?.visibility == View.VISIBLE) checkGatewayStatus()
|
||||
updateBanner.visibility = View.GONE
|
||||
bannerProgressBar.visibility = View.GONE
|
||||
}, 3000)
|
||||
}
|
||||
// Not OK = install failed (possibly signature conflict).
|
||||
|
||||
@@ -398,6 +398,31 @@
|
||||
android:textColor="#64748b"
|
||||
android:textSize="13sp"
|
||||
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>
|
||||
|
||||
<!-- Bottom nav (Back / Launch) — hidden until user answers the question -->
|
||||
@@ -479,14 +504,20 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:orientation="horizontal"
|
||||
android:orientation="vertical"
|
||||
android:background="#1e293b"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- Banner row: message + buttons -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:gravity="center_vertical"
|
||||
android:visibility="gone">
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUpdateMessage"
|
||||
@@ -519,6 +550,20 @@
|
||||
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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user