fix: APK install conflict (PackageInstaller) + dashboard stat skeleton
APK install conflict:
- Replace ACTION_VIEW-based install with PackageInstaller.Session API (API 21+)
- PackageInstaller gives us the actual install status via BroadcastReceiver:
STATUS_PENDING_USER_ACTION → launch system confirmation dialog automatically
STATUS_SUCCESS → success toast
STATUS_FAILURE_CONFLICT/INCOMPATIBLE → show AlertDialog offering to
uninstall the old version (ACTION_DELETE) so user can re-download and install
- FileProvider no longer needed for install (still kept for other uses)
- Kiosk: derive target package from filename (gateway vs kiosk self-update)
Dashboard 0-flash:
- Replace hardcoded 0 in HTML stat-value spans with ... placeholder
- Add .stat-loading CSS class: shimmer skeleton animation (gradient sweep)
- showPage(dashboard): set ... + stat-loading before API call
- loadDashboard: remove stat-loading class and set real count after data arrives
This commit is contained in:
@@ -372,6 +372,21 @@ body {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Skeleton pulse while stat is loading */
|
||||
.stat-value.stat-loading {
|
||||
color: transparent;
|
||||
background: linear-gradient(90deg, var(--border) 25%, var(--bg-dark, #e2e8f0) 50%, var(--border) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: stat-shimmer 1.2s infinite;
|
||||
border-radius: 6px;
|
||||
min-width: 2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
@keyframes stat-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-light);
|
||||
|
||||
+11
-2
@@ -2227,7 +2227,14 @@ function showPage(pageId, param = null) {
|
||||
|
||||
// Page-specific init
|
||||
switch(pageId) {
|
||||
case 'dashboard': loadDashboard(); break;
|
||||
case 'dashboard':
|
||||
// Show skeleton on stat-cards while data loads
|
||||
['dispensa', 'frigo', 'freezer'].forEach(loc => {
|
||||
const el = document.getElementById(`stat-${loc}`);
|
||||
if (el) { el.textContent = '…'; el.classList.add('stat-loading'); }
|
||||
});
|
||||
loadDashboard();
|
||||
break;
|
||||
case 'inventory':
|
||||
if (param !== null) {
|
||||
currentLocation = param;
|
||||
@@ -2643,7 +2650,9 @@ async function loadDashboard() {
|
||||
['dispensa', 'frigo', 'freezer'].forEach(loc => {
|
||||
const s = summary.find(x => x.location === loc);
|
||||
const count = s ? s.product_count : 0;
|
||||
document.getElementById(`stat-${loc}`).textContent = count;
|
||||
const el = document.getElementById(`stat-${loc}`);
|
||||
el.textContent = count;
|
||||
el.classList.remove('stat-loading');
|
||||
total += count;
|
||||
});
|
||||
// Add non-standard locations
|
||||
|
||||
@@ -9,6 +9,8 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.app.PendingIntent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.net.Uri
|
||||
@@ -828,24 +830,91 @@ class KioskActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun installApk(file: java.io.File) {
|
||||
if (!file.exists() || file.length() == 0L) {
|
||||
runOnUiThread { Toast.makeText(this, "File APK non trovato", Toast.LENGTH_LONG).show() }
|
||||
return
|
||||
}
|
||||
// Derive the package name we are installing from the filename
|
||||
val targetPkg = when {
|
||||
file.name.contains("gateway") || file.name.contains("scale") -> GATEWAY_PACKAGE
|
||||
else -> packageName // kiosk self-update
|
||||
}
|
||||
installWithPackageInstaller(file, targetPkg)
|
||||
}
|
||||
|
||||
/** Use PackageInstaller (API 21+) for reliable install-over-existing support. */
|
||||
private fun installWithPackageInstaller(file: java.io.File, targetPkg: String) {
|
||||
try {
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
androidx.core.content.FileProvider.getUriForFile(
|
||||
this, "$packageName.provider", file
|
||||
val pi = packageManager.packageInstaller
|
||||
val params = android.content.pm.PackageInstaller.SessionParams(
|
||||
android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
)
|
||||
params.setAppPackageName(targetPkg)
|
||||
val sessionId = pi.createSession(params)
|
||||
pi.openSession(sessionId).use { session ->
|
||||
file.inputStream().use { input ->
|
||||
session.openWrite("package", 0, file.length()).use { out ->
|
||||
input.copyTo(out)
|
||||
session.fsync(out)
|
||||
}
|
||||
}
|
||||
// Register a BroadcastReceiver for the install result
|
||||
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(
|
||||
android.content.pm.PackageInstaller.EXTRA_STATUS,
|
||||
android.content.pm.PackageInstaller.STATUS_FAILURE
|
||||
) ?: android.content.pm.PackageInstaller.STATUS_FAILURE
|
||||
when (status) {
|
||||
android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
// Android needs user confirmation — launch the system dialog
|
||||
@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) startActivity(confirmIntent)
|
||||
}
|
||||
android.content.pm.PackageInstaller.STATUS_SUCCESS ->
|
||||
runOnUiThread { Toast.makeText(this@KioskActivity, "✅ Aggiornamento installato", Toast.LENGTH_SHORT).show() }
|
||||
android.content.pm.PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
|
||||
android.content.pm.PackageInstaller.STATUS_FAILURE_CONFLICT -> {
|
||||
// Signature mismatch: offer to uninstall the old version first
|
||||
runOnUiThread {
|
||||
androidx.appcompat.app.AlertDialog.Builder(this@KioskActivity)
|
||||
.setTitle("⚠️ Conflitto firma APK")
|
||||
.setMessage("L'app installata usa una firma diversa.\n\nDevi disinstallare la versione precedente e poi ripremere Scarica.")
|
||||
.setPositiveButton("Disinstalla") { _, _ ->
|
||||
startActivity(Intent(Intent.ACTION_DELETE,
|
||||
android.net.Uri.parse("package:$targetPkg")))
|
||||
}
|
||||
.setNegativeButton("Annulla", null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val msg = intent?.getStringExtra(
|
||||
android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE
|
||||
) ?: "status=$status"
|
||||
runOnUiThread { Toast.makeText(this@KioskActivity, "Installazione: $msg", Toast.LENGTH_LONG).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
|
||||
)
|
||||
} else {
|
||||
Uri.fromFile(file)
|
||||
session.commit(pi2.intentSender)
|
||||
}
|
||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(install)
|
||||
Toast.makeText(this, "Installazione in corso…", Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
runOnUiThread { Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+67
-11
@@ -8,6 +8,8 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.app.PendingIntent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -517,21 +519,75 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener
|
||||
}
|
||||
|
||||
private fun installApk(file: java.io.File) {
|
||||
if (!file.exists() || file.length() == 0L) {
|
||||
runOnUiThread { Toast.makeText(this, "File APK non trovato", Toast.LENGTH_LONG).show() }
|
||||
return
|
||||
}
|
||||
try {
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
androidx.core.content.FileProvider.getUriForFile(
|
||||
this, "$packageName.provider", file
|
||||
val pi = packageManager.packageInstaller
|
||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
params.setAppPackageName(packageName)
|
||||
val sessionId = pi.createSession(params)
|
||||
pi.openSession(sessionId).use { session ->
|
||||
file.inputStream().use { input ->
|
||||
session.openWrite("package", 0, file.length()).use { out ->
|
||||
input.copyTo(out)
|
||||
session.fsync(out)
|
||||
}
|
||||
}
|
||||
val action = "it.dadaloop.evershelf.scalegate.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 -> {
|
||||
@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) startActivity(confirmIntent)
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS ->
|
||||
runOnUiThread { Toast.makeText(this@MainActivity, "✅ Aggiornamento installato", Toast.LENGTH_SHORT).show() }
|
||||
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
|
||||
PackageInstaller.STATUS_FAILURE_CONFLICT -> {
|
||||
runOnUiThread {
|
||||
AlertDialog.Builder(this@MainActivity)
|
||||
.setTitle("⚠️ Conflitto firma APK")
|
||||
.setMessage("L'app installata usa una firma diversa.\n\nDevi disinstallare la versione precedente e poi ripremere Scarica.")
|
||||
.setPositiveButton("Disinstalla") { _, _ ->
|
||||
startActivity(Intent(Intent.ACTION_DELETE,
|
||||
android.net.Uri.parse("package:$packageName")))
|
||||
}
|
||||
.setNegativeButton("Annulla", null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val msg = intent?.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
?: "status=$status"
|
||||
runOnUiThread { Toast.makeText(this@MainActivity, "Installazione: $msg", Toast.LENGTH_LONG).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
|
||||
)
|
||||
} else { Uri.fromFile(file) }
|
||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
session.commit(pi2.intentSender)
|
||||
}
|
||||
startActivity(install)
|
||||
Toast.makeText(this, "Installazione in corso…", Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
runOnUiThread { Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -82,17 +82,17 @@
|
||||
<div class="dashboard-stats" id="dashboard-stats">
|
||||
<div class="stat-card" onclick="showPage('inventory', 'dispensa')">
|
||||
<span class="stat-icon">🗄️</span>
|
||||
<span class="stat-value" id="stat-dispensa">0</span>
|
||||
<span class="stat-value stat-loading" id="stat-dispensa">…</span>
|
||||
<span class="stat-label" data-i18n="locations.dispensa">Dispensa</span>
|
||||
</div>
|
||||
<div class="stat-card" onclick="showPage('inventory', 'frigo')">
|
||||
<span class="stat-icon">🧊</span>
|
||||
<span class="stat-value" id="stat-frigo">0</span>
|
||||
<span class="stat-value stat-loading" id="stat-frigo">…</span>
|
||||
<span class="stat-label" data-i18n="locations.frigo">Frigo</span>
|
||||
</div>
|
||||
<div class="stat-card" onclick="showPage('inventory', 'freezer')">
|
||||
<span class="stat-icon">❄️</span>
|
||||
<span class="stat-value" id="stat-freezer">0</span>
|
||||
<span class="stat-value stat-loading" id="stat-freezer">…</span>
|
||||
<span class="stat-label" data-i18n="locations.freezer">Freezer</span>
|
||||
</div>
|
||||
<div class="stat-card" onclick="showPage('shopping')">
|
||||
|
||||
Reference in New Issue
Block a user