Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49e5319f4c | |||
| 3ebe551b9e | |||
| 0e1eccfe33 | |||
| 4624811707 | |||
| 3607ebf1d7 | |||
| 8bb6c01b7d | |||
| b1a882f92d | |||
| 1b7b271b43 |
+28
-19
@@ -303,14 +303,15 @@ function _scaleOnMessage(msg) {
|
|||||||
const rawValue = parseFloat(msg.value);
|
const rawValue = parseFloat(msg.value);
|
||||||
if (rawValue < 0) return;
|
if (rawValue < 0) return;
|
||||||
|
|
||||||
// Ignore sub-gram jitter for stability decisions: only integer-gram changes matter.
|
// Ignore sub-2g jitter for stability decisions: changes below 2g are considered noise.
|
||||||
|
const SCALE_NOISE_G = 2;
|
||||||
let effectiveStable = !!msg.stable;
|
let effectiveStable = !!msg.stable;
|
||||||
const grams = _scaleToGrams(rawValue, msg.unit);
|
const grams = _scaleToGrams(rawValue, msg.unit);
|
||||||
if (grams !== null) {
|
if (grams !== null) {
|
||||||
if (effectiveStable) {
|
if (effectiveStable) {
|
||||||
_scaleLastStableGrams = grams;
|
_scaleLastStableGrams = grams;
|
||||||
} else if (_scaleLastStableGrams !== null) {
|
} else if (_scaleLastStableGrams !== null) {
|
||||||
if (Math.round(grams) === Math.round(_scaleLastStableGrams)) {
|
if (Math.abs(grams - _scaleLastStableGrams) < SCALE_NOISE_G) {
|
||||||
effectiveStable = true;
|
effectiveStable = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,7 +403,7 @@ function _scaleUpdateLiveBox(msg) {
|
|||||||
|
|
||||||
const raw = parseFloat(msg.value);
|
const raw = parseFloat(msg.value);
|
||||||
const rawUnit = (msg.unit || 'kg').toLowerCase();
|
const rawUnit = (msg.unit || 'kg').toLowerCase();
|
||||||
// Convert to grams for the < 10 g threshold check
|
// Convert to grams for the < 2 g threshold check
|
||||||
let gForCheck = isFinite(raw) ? raw : 0;
|
let gForCheck = isFinite(raw) ? raw : 0;
|
||||||
if (rawUnit === 'kg') gForCheck = raw * 1000;
|
if (rawUnit === 'kg') gForCheck = raw * 1000;
|
||||||
if (rawUnit === 'lbs' || rawUnit === 'lb') gForCheck = raw * 453.592;
|
if (rawUnit === 'lbs' || rawUnit === 'lb') gForCheck = raw * 453.592;
|
||||||
@@ -410,7 +411,7 @@ function _scaleUpdateLiveBox(msg) {
|
|||||||
const valEl = document.getElementById('scale-live-val');
|
const valEl = document.getElementById('scale-live-val');
|
||||||
const lblEl = document.getElementById('scale-live-label');
|
const lblEl = document.getElementById('scale-live-label');
|
||||||
|
|
||||||
if (isFinite(raw) && gForCheck < 10 && gForCheck > 0) {
|
if (isFinite(raw) && gForCheck < 2 && gForCheck > 0) {
|
||||||
// Weight too low — show red flashing warning
|
// Weight too low — show red flashing warning
|
||||||
box.classList.add('scale-low-weight');
|
box.classList.add('scale-low-weight');
|
||||||
if (valEl) valEl.textContent = `${raw} ${msg.unit || 'kg'}`;
|
if (valEl) valEl.textContent = `${raw} ${msg.unit || 'kg'}`;
|
||||||
@@ -475,14 +476,14 @@ function _scaleAutoFillUse(msg) {
|
|||||||
else if (srcUnit === 'ml') { grams = rawVal; scaleAlreadyMl = true; }
|
else if (srcUnit === 'ml') { grams = rawVal; scaleAlreadyMl = true; }
|
||||||
else grams = rawVal;
|
else grams = rawVal;
|
||||||
|
|
||||||
// Reject if raw grams < 10 (piatto vuoto / tara / rumore)
|
// Reject if raw grams < 2 (tara / rumore)
|
||||||
if (grams < 10) {
|
if (grams < 2) {
|
||||||
_cancelScaleStabilityWait(); // stop bar only; keep sentinel & userDismissed
|
_cancelScaleStabilityWait(); // stop bar only; keep sentinel & userDismissed
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject if weight hasn't changed enough from last confirmed reading (same product still on scale)
|
// Reject if weight hasn't changed enough from last confirmed reading (same product still on scale)
|
||||||
if (_scaleLastConfirmedGrams !== null && Math.abs(grams - _scaleLastConfirmedGrams) < 10) {
|
if (_scaleLastConfirmedGrams !== null && Math.abs(grams - _scaleLastConfirmedGrams) < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,15 +508,8 @@ function _scaleAutoFillUse(msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject if converted value < 10 (density edge case)
|
// Reject if converted value < 2 (density edge case)
|
||||||
if (val < 10) {
|
if (val < 2) {
|
||||||
_cancelScaleStabilityWait();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (val !== _scaleStabilityVal) {
|
|
||||||
// New (different) weight → clear dismissal, restart stability wait
|
|
||||||
_scaleStabilityVal = val;
|
|
||||||
_scaleUserDismissed = false;
|
_scaleUserDismissed = false;
|
||||||
_cancelScaleTimersOnly();
|
_cancelScaleTimersOnly();
|
||||||
_startScaleStabilityWait(() => {
|
_startScaleStabilityWait(() => {
|
||||||
@@ -618,15 +612,15 @@ function _scaleAutoFillRecipeUse(msg) {
|
|||||||
hint.style.display = '';
|
hint.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (val < 10) {
|
if (val < 2) {
|
||||||
_cancelScaleStabilityWait(); // stop bar only; keep sentinel
|
_cancelScaleStabilityWait(); // stop bar only; keep sentinel
|
||||||
if (livLabel) livLabel.textContent = t('scale.weight_too_low');
|
if (livLabel) livLabel.textContent = t('scale.weight_too_low');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject if weight hasn't changed enough from last confirmed reading.
|
// Reject if weight hasn't changed enough from last confirmed reading.
|
||||||
// Threshold: 5g — gives enough time to tare after opening the modal.
|
// Threshold: 2g — noise filter.
|
||||||
if (_scaleLastConfirmedGrams !== null && Math.abs(grams - _scaleLastConfirmedGrams) < 5) {
|
if (_scaleLastConfirmedGrams !== null && Math.abs(grams - _scaleLastConfirmedGrams) < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4820,6 +4814,9 @@ function showItemDetail(inventoryId, productId) {
|
|||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
document.getElementById('modal-overlay').style.display = 'none';
|
document.getElementById('modal-overlay').style.display = 'none';
|
||||||
|
clearMoveModalTimer();
|
||||||
|
// Restore native kiosk settings button visibility
|
||||||
|
try { if (typeof _kioskBridge !== 'undefined') _kioskBridge.setNativeSettingsVisible(true); } catch (_) {}
|
||||||
_cancelScaleAutoConfirm(false);
|
_cancelScaleAutoConfirm(false);
|
||||||
_scaleRecipeAutoFillPaused = false;
|
_scaleRecipeAutoFillPaused = false;
|
||||||
_scaleUserDismissed = false;
|
_scaleUserDismissed = false;
|
||||||
@@ -8048,14 +8045,22 @@ function closeLowStockPrompt() {
|
|||||||
|
|
||||||
let _moveModalTimer = null;
|
let _moveModalTimer = null;
|
||||||
let _moveModalRAF = null;
|
let _moveModalRAF = null;
|
||||||
|
let _moveModalTouchHandler = null;
|
||||||
|
|
||||||
function clearMoveModalTimer() {
|
function clearMoveModalTimer() {
|
||||||
if (_moveModalTimer) { clearTimeout(_moveModalTimer); _moveModalTimer = null; }
|
if (_moveModalTimer) { clearTimeout(_moveModalTimer); _moveModalTimer = null; }
|
||||||
if (_moveModalRAF) { cancelAnimationFrame(_moveModalRAF); _moveModalRAF = null; }
|
if (_moveModalRAF) { cancelAnimationFrame(_moveModalRAF); _moveModalRAF = null; }
|
||||||
|
if (_moveModalTouchHandler) {
|
||||||
|
document.getElementById('modal-content')?.removeEventListener('pointerdown', _moveModalTouchHandler, true);
|
||||||
|
_moveModalTouchHandler = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startMoveModalCountdown(btnId, onExpire) {
|
function startMoveModalCountdown(btnId, onExpire) {
|
||||||
clearMoveModalTimer();
|
clearMoveModalTimer();
|
||||||
|
// Any touch inside the modal cancels the auto-close countdown
|
||||||
|
_moveModalTouchHandler = () => clearMoveModalTimer();
|
||||||
|
document.getElementById('modal-content')?.addEventListener('pointerdown', _moveModalTouchHandler, { capture: true, once: true });
|
||||||
const duration = 15000;
|
const duration = 15000;
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
const btn = document.getElementById(btnId);
|
const btn = document.getElementById(btnId);
|
||||||
@@ -8102,6 +8107,8 @@ function showMoveAfterUseModal(product, fromLoc, remaining, openedId, openedVacu
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.getElementById('modal-overlay').style.display = 'flex';
|
document.getElementById('modal-overlay').style.display = 'flex';
|
||||||
|
// Hide the native kiosk settings button while the modal is open (prevents touch bleed-through)
|
||||||
|
try { if (typeof _kioskBridge !== 'undefined') _kioskBridge.setNativeSettingsVisible(false); } catch (_) {}
|
||||||
startMoveModalCountdown('btn-move-stay', () => { _saveVacuumAndStay(openedId || 0); });
|
startMoveModalCountdown('btn-move-stay', () => { _saveVacuumAndStay(openedId || 0); });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11729,6 +11736,8 @@ function showRecipeMoveModal(productId, fromLoc, remaining, openedId, wasVacuum)
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.getElementById('modal-overlay').style.display = 'flex';
|
document.getElementById('modal-overlay').style.display = 'flex';
|
||||||
|
// Hide the native kiosk settings button while the modal is open (prevents touch bleed-through)
|
||||||
|
try { if (typeof _kioskBridge !== 'undefined') _kioskBridge.setNativeSettingsVisible(false); } catch (_) {}
|
||||||
startMoveModalCountdown('btn-move-stay', () => { closeModal(); });
|
startMoveModalCountdown('btn-move-stay', () => { closeModal(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "it.dadaloop.evershelf.kiosk"
|
applicationId = "it.dadaloop.evershelf.kiosk"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 11
|
versionCode = 13
|
||||||
versionName = "1.7.0"
|
versionName = "1.7.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package it.dadaloop.evershelf.kiosk
|
package it.dadaloop.evershelf.kiosk
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.ApplicationExitInfo
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
@@ -40,7 +43,9 @@ object ErrorReporter {
|
|||||||
|
|
||||||
// SharedPreferences for crash persistence
|
// SharedPreferences for crash persistence
|
||||||
private const val PREFS_NAME = "evershelf_kiosk_errors"
|
private const val PREFS_NAME = "evershelf_kiosk_errors"
|
||||||
private const val KEY_PENDING = "pending_crash_json"
|
private const val KEY_PENDING = "pending_crash_json"
|
||||||
|
private const val KEY_WAS_RUNNING = "was_running_dirty"
|
||||||
|
private const val KEY_LAST_EXIT_TS = "last_reported_exit_ts"
|
||||||
|
|
||||||
private val executor = Executors.newSingleThreadExecutor()
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
@@ -76,6 +81,9 @@ object ErrorReporter {
|
|||||||
// Send any crash that was saved to prefs during a previous session
|
// Send any crash that was saved to prefs during a previous session
|
||||||
sendPendingCrash()
|
sendPendingCrash()
|
||||||
|
|
||||||
|
// Detect ANR / OOM / native crashes from the previous run
|
||||||
|
detectPreviousCrash()
|
||||||
|
|
||||||
// Install a global UncaughtExceptionHandler so ANY unhandled crash is reported
|
// Install a global UncaughtExceptionHandler so ANY unhandled crash is reported
|
||||||
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
|
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
@@ -96,6 +104,17 @@ object ErrorReporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call from Activity.onDestroy() on a *clean* exit (back-pressed, settings, shutdown).
|
||||||
|
* Clears the dirty-launch sentinel so the next start does not report a false positive.
|
||||||
|
*/
|
||||||
|
fun markCleanStop() {
|
||||||
|
if (::appContext.isInitialized) {
|
||||||
|
appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit().putBoolean(KEY_WAS_RUNNING, false).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report a caught [Throwable] asynchronously (does not block UI thread).
|
* Report a caught [Throwable] asynchronously (does not block UI thread).
|
||||||
*/
|
*/
|
||||||
@@ -132,6 +151,96 @@ object ErrorReporter {
|
|||||||
|
|
||||||
// ── Internal ─────────────────────────────────────────────────────────────
|
// ── Internal ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects whether the *previous* run of the app ended with a crash, ANR or OOM kill.
|
||||||
|
*
|
||||||
|
* On Android 11+ (API 30) we use [ActivityManager.getHistoricalProcessExitReasons] which
|
||||||
|
* gives the exact reason and (for Java crashes) a stack trace.
|
||||||
|
*
|
||||||
|
* On Android 7–10 we use a "dirty-launch sentinel": a boolean in SharedPreferences that is
|
||||||
|
* set to `true` on every start and `false` only when the activity is destroyed cleanly via
|
||||||
|
* [markCleanStop]. If it is still `true` on the next start, the previous run was not clean.
|
||||||
|
*/
|
||||||
|
private fun detectPreviousCrash() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
detectExitReasonApi30()
|
||||||
|
} else {
|
||||||
|
// API 24–29: dirty-launch sentinel
|
||||||
|
val prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
if (prefs.getBoolean(KEY_WAS_RUNNING, false)) {
|
||||||
|
reportAsync(
|
||||||
|
type = "crash-sentinel",
|
||||||
|
message = "App was not cleanly shut down on previous run (ANR / OOM / native crash suspected).",
|
||||||
|
stack = "",
|
||||||
|
context = mapOf(
|
||||||
|
"device" to deviceInfo,
|
||||||
|
"note" to "Detected via dirty-launch sentinel (API ${Build.VERSION.SDK_INT})"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mark this launch as running — will be cleared by markCleanStop() on clean exit
|
||||||
|
appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit().putBoolean(KEY_WAS_RUNNING, true).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun detectExitReasonApi30() {
|
||||||
|
try {
|
||||||
|
val am = appContext.getSystemService(ActivityManager::class.java) ?: return
|
||||||
|
// Check the last 5 exits; stop at the first we already reported
|
||||||
|
val exits = am.getHistoricalProcessExitReasons(null, 0, 5)
|
||||||
|
if (exits.isEmpty()) return
|
||||||
|
|
||||||
|
val prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
val lastReportedTs = prefs.getLong(KEY_LAST_EXIT_TS, 0L)
|
||||||
|
|
||||||
|
val crashReasons = setOf(
|
||||||
|
ApplicationExitInfo.REASON_CRASH,
|
||||||
|
ApplicationExitInfo.REASON_CRASH_NATIVE,
|
||||||
|
ApplicationExitInfo.REASON_ANR,
|
||||||
|
ApplicationExitInfo.REASON_LOW_MEMORY
|
||||||
|
)
|
||||||
|
|
||||||
|
var newestTs = lastReportedTs
|
||||||
|
for (exit in exits) {
|
||||||
|
if (exit.timestamp <= lastReportedTs) continue // already reported
|
||||||
|
if (exit.reason !in crashReasons) continue
|
||||||
|
|
||||||
|
val reasonName = when (exit.reason) {
|
||||||
|
ApplicationExitInfo.REASON_CRASH -> "crash-java"
|
||||||
|
ApplicationExitInfo.REASON_CRASH_NATIVE -> "crash-native"
|
||||||
|
ApplicationExitInfo.REASON_ANR -> "anr"
|
||||||
|
ApplicationExitInfo.REASON_LOW_MEMORY -> "oom-kill"
|
||||||
|
else -> "exit-${exit.reason}"
|
||||||
|
}
|
||||||
|
val msg = exit.description?.takeIf { it.isNotEmpty() }
|
||||||
|
?: "${exit.processName ?: "app"} terminated (reason ${exit.reason})"
|
||||||
|
|
||||||
|
// Java crashes include a tombstone trace — read up to 4KB
|
||||||
|
var stack = ""
|
||||||
|
try {
|
||||||
|
exit.traceInputStream?.bufferedReader()?.use { stack = it.readText().take(4000) }
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
|
val ctx = mutableMapOf<String, Any?>(
|
||||||
|
"device" to deviceInfo,
|
||||||
|
"reason" to exit.reason,
|
||||||
|
"process" to (exit.processName ?: ""),
|
||||||
|
"crash_ts" to SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date(exit.timestamp)),
|
||||||
|
"note" to "Detected via ApplicationExitInfo on restart (API ${Build.VERSION.SDK_INT})"
|
||||||
|
)
|
||||||
|
reportAsync(type = reasonName, message = msg, stack = stack, context = ctx)
|
||||||
|
|
||||||
|
if (exit.timestamp > newestTs) newestTs = exit.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newestTs > lastReportedTs) {
|
||||||
|
prefs.edit().putLong(KEY_LAST_EXIT_TS, newestTs).apply()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
private fun fingerprint(type: String, message: String): String {
|
private fun fingerprint(type: String, message: String): String {
|
||||||
val key = "$type:${message.take(120)}"
|
val key = "$type:${message.take(120)}"
|
||||||
return key.hashCode().toString(16)
|
return key.hashCode().toString(16)
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
private val pollHandler = Handler(Looper.getMainLooper())
|
private val pollHandler = Handler(Looper.getMainLooper())
|
||||||
private var activeDownloadId: Long = -1
|
private var activeDownloadId: Long = -1
|
||||||
|
|
||||||
|
// Periodic update-check handler (fires every 30 min; internal throttle in checkForUpdates limits real API calls to every 6h)
|
||||||
|
private val updateCheckHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val updateCheckRunnable = Runnable { schedulePeriodicUpdateCheck() }
|
||||||
|
private val UPDATE_CHECK_INTERVAL_MS = 30L * 60 * 1000 // 30 minutes
|
||||||
|
|
||||||
|
private fun schedulePeriodicUpdateCheck() {
|
||||||
|
checkForUpdates(forceCheck = false)
|
||||||
|
updateCheckHandler.postDelayed(updateCheckRunnable, UPDATE_CHECK_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
// File chooser
|
// File chooser
|
||||||
private var fileChooserCallback: ValueCallback<Array<Uri>>? = null
|
private var fileChooserCallback: ValueCallback<Array<Uri>>? = null
|
||||||
|
|
||||||
@@ -104,6 +114,9 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
private const val KIOSK_DOWNLOAD_URL = "https://github.com/dadaloop82/EverShelf/releases/download/kiosk-latest/evershelf-kiosk.apk"
|
private const val KIOSK_DOWNLOAD_URL = "https://github.com/dadaloop82/EverShelf/releases/download/kiosk-latest/evershelf-kiosk.apk"
|
||||||
private const val SPLASH_DURATION = 1500L
|
private const val SPLASH_DURATION = 1500L
|
||||||
private const val GITHUB_RELEASES_API = "https://api.github.com/repos/dadaloop82/EverShelf/releases/latest"
|
private const val GITHUB_RELEASES_API = "https://api.github.com/repos/dadaloop82/EverShelf/releases/latest"
|
||||||
|
// Keys for persisting a pending update across restarts
|
||||||
|
private const val KEY_PENDING_UPDATE_VERSION = "pending_update_version"
|
||||||
|
private const val KEY_PENDING_UPDATE_URL = "pending_update_url"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context) {
|
override fun attachBaseContext(newBase: Context) {
|
||||||
@@ -492,6 +505,16 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
if (apkUrl.isBlank()) return
|
if (apkUrl.isBlank()) return
|
||||||
runOnUiThread { triggerApkDownload(apkUrl) }
|
runOnUiThread { triggerApkDownload(apkUrl) }
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Called by the webapp when a modal is shown / hidden so the native settings
|
||||||
|
* button does not intercept touches that belong to the HTML modal content.
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
fun setNativeSettingsVisible(visible: Boolean) {
|
||||||
|
runOnUiThread {
|
||||||
|
btnSettings.visibility = if (visible) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
}, "_kioskBridge")
|
}, "_kioskBridge")
|
||||||
|
|
||||||
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
|
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
|
||||||
@@ -638,7 +661,17 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
notifyJs(result)
|
notifyJs(result)
|
||||||
|
|
||||||
if (!kioskNeedsUpdate) return@Thread
|
if (!kioskNeedsUpdate) {
|
||||||
|
// Clear any stale pending update if the current version is now up to date
|
||||||
|
prefs.edit().remove(KEY_PENDING_UPDATE_VERSION).remove(KEY_PENDING_UPDATE_URL).apply()
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the pending update so the banner reappears after a crash/restart
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_PENDING_UPDATE_VERSION, latestTag)
|
||||||
|
.putString(KEY_PENDING_UPDATE_URL, kioskApkUrl)
|
||||||
|
.apply()
|
||||||
|
|
||||||
val label = if (isSemver) "$currentKiosk → $latestTag" else latestTag
|
val label = if (isSemver) "$currentKiosk → $latestTag" else latestTag
|
||||||
runOnUiThread { showNativeUpdateBanner("🔄 Kiosk $label", kioskApkUrl) }
|
runOnUiThread { showNativeUpdateBanner("🔄 Kiosk $label", kioskApkUrl) }
|
||||||
@@ -648,6 +681,33 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On resume: if a previous session detected an available update and saved it to prefs,
|
||||||
|
* restore the update banner immediately without a network round-trip.
|
||||||
|
*/
|
||||||
|
private fun restorePendingUpdateBanner() {
|
||||||
|
val savedVersion = prefs.getString(KEY_PENDING_UPDATE_VERSION, null) ?: return
|
||||||
|
val savedUrl = prefs.getString(KEY_PENDING_UPDATE_URL, null) ?: return
|
||||||
|
val currentKiosk = try { packageManager.getPackageInfo(packageName, 0).versionName ?: "" } catch (_: Exception) { "" }
|
||||||
|
// Normalise: strip non-numeric prefix for comparison
|
||||||
|
val norm = { v: String -> v.replace(Regex("^[^0-9]*"), "") }
|
||||||
|
fun semverNewer(remote: String, local: String): Boolean {
|
||||||
|
val r = remote.split(".").map { it.filter(Char::isDigit).toIntOrNull() ?: 0 }
|
||||||
|
val l = local.split(".").map { it.filter(Char::isDigit).toIntOrNull() ?: 0 }
|
||||||
|
for (i in 0 until maxOf(r.size, l.size)) {
|
||||||
|
val rv = r.getOrElse(i) { 0 }; val lv = l.getOrElse(i) { 0 }
|
||||||
|
if (rv != lv) return rv > lv
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (currentKiosk.isNotEmpty() && semverNewer(norm(savedVersion), norm(currentKiosk))) {
|
||||||
|
showNativeUpdateBanner("🔄 Kiosk $currentKiosk → $savedVersion", savedUrl)
|
||||||
|
} else {
|
||||||
|
// Update was installed or is no longer applicable — clear the saved entry
|
||||||
|
prefs.edit().remove(KEY_PENDING_UPDATE_VERSION).remove(KEY_PENDING_UPDATE_URL).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showNativeUpdateBanner(message: String, apkDownloadUrl: String) {
|
private fun showNativeUpdateBanner(message: String, apkDownloadUrl: String) {
|
||||||
pendingApkDownloadUrl = apkDownloadUrl
|
pendingApkDownloadUrl = apkDownloadUrl
|
||||||
tvUpdateMessage.text = "⬆️ Aggiornamento disponibile: $message"
|
tvUpdateMessage.text = "⬆️ Aggiornamento disponibile: $message"
|
||||||
@@ -952,6 +1012,16 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
// Re-apply screensaver flag in case the user changed it in Settings
|
// Re-apply screensaver flag in case the user changed it in Settings
|
||||||
applyScreensaverFlag()
|
applyScreensaverFlag()
|
||||||
}
|
}
|
||||||
|
// Show banner immediately if there is a pending update detected in a previous session
|
||||||
|
restorePendingUpdateBanner()
|
||||||
|
// Start (or restart) the periodic update check
|
||||||
|
updateCheckHandler.removeCallbacks(updateCheckRunnable)
|
||||||
|
updateCheckHandler.postDelayed(updateCheckRunnable, UPDATE_CHECK_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
updateCheckHandler.removeCallbacks(updateCheckRunnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@@ -1040,6 +1110,8 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
ErrorReporter.markCleanStop()
|
||||||
|
updateCheckHandler.removeCallbacks(updateCheckRunnable)
|
||||||
tts?.stop()
|
tts?.stop()
|
||||||
tts?.shutdown()
|
tts?.shutdown()
|
||||||
tts = null
|
tts = null
|
||||||
|
|||||||
Reference in New Issue
Block a user