diff --git a/assets/css/style.css b/assets/css/style.css index b8959e3..faafa3c 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -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); diff --git a/assets/js/app.js b/assets/js/app.js index 7ee0e07..12f7259 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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 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 f9ebd98..356f24e 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 @@ -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() } } } diff --git a/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt b/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt index fe242db..d24d0f9 100644 --- a/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt +++ b/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt @@ -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() } } } diff --git a/index.html b/index.html index f0d0209..4b0e6a5 100644 --- a/index.html +++ b/index.html @@ -82,17 +82,17 @@
🗄️ - 0 + Dispensa
🧊 - 0 + Frigo
❄️ - 0 + Freezer