chore: auto-merge develop → main

Triggered by: fa9c52e feat(kiosk): complete setup wizard overhaul
This commit is contained in:
github-actions[bot]
2026-05-04 15:42:31 +00:00
8 changed files with 2040 additions and 1367 deletions
@@ -52,6 +52,13 @@
</intent-filter>
</activity>
<activity
android:name=".SetupActivity"
android:exported="false"
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".SettingsActivity"
android:exported="false"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,813 @@
package it.dadaloop.evershelf.kiosk
import android.Manifest
import android.app.AlertDialog
import android.app.DownloadManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
/**
* Full setup wizard — runs BEFORE KioskActivity locks the screen.
* The user can always exit (finishAffinity) via the ✕ button.
*
* Steps:
* 0 — Welcome / intro / privacy
* 1 — Permissions rationale + grant
* 2 — Server URL + auto-discovery + connection test
* 3 — Smart scale question → gateway info + install
* 4 — Done
*/
class SetupActivity : AppCompatActivity() {
private lateinit var prefs: SharedPreferences
private var currentStep = 0
// Step containers
private lateinit var stepWelcome: LinearLayout
private lateinit var stepPermissions: LinearLayout
private lateinit var stepServer: LinearLayout
private lateinit var stepScale: LinearLayout
private lateinit var stepDone: LinearLayout
// Progress dots
private lateinit var progressDots: LinearLayout
// Server step
private lateinit var urlEdit: EditText
private lateinit var urlStatus: TextView
private lateinit var btnTestUrl: MaterialButton
private lateinit var btnDiscover: MaterialButton
private lateinit var discoverStatus: TextView
// Scale step
private lateinit var scaleQuestionCard: LinearLayout
private lateinit var gatewayInfoCard: LinearLayout
private lateinit var gatewayInstallCard: LinearLayout
private lateinit var gatewayStatusIcon: TextView
private lateinit var gatewayStatusText: TextView
private lateinit var gatewayStatusDetail: TextView
private lateinit var btnInstallGateway: MaterialButton
private lateinit var gatewayProgressBar: ProgressBar
private lateinit var gatewayProgressText: TextView
private lateinit var step3NextButtons: LinearLayout
// Done step
private lateinit var summaryText: TextView
// Permissions step
private lateinit var permsGrantedCard: LinearLayout
// APK install state (for gateway)
private var pendingApkDownloadUrl = ""
private var pendingInstallFile: java.io.File? = null
private var pendingInstallPkg = ""
private val pollHandler = Handler(Looper.getMainLooper())
private var activeDownloadId: Long = -1
// Auto-discover cancellation flag
private val discoverCancelled = AtomicBoolean(false)
companion object {
private const val PREFS_NAME = "evershelf_kiosk"
private const val KEY_URL = "evershelf_url"
private const val KEY_SETUP_COMPLETE = "setup_complete"
private const val KEY_HAS_SCALE = "has_scale"
private const val GATEWAY_PACKAGE = "it.dadaloop.evershelf.scalegate"
private const val GATEWAY_DOWNLOAD_URL =
"https://github.com/dadaloop82/EverShelf/releases/latest/download/evershelf-scale-gateway.apk"
private const val INSTALL_PERM_REQUEST = 2001
private const val INSTALL_CONFIRM_REQUEST = 2002
private const val UNINSTALL_REQUEST = 2003
private const val PERMISSION_REQUEST_CODE = 2004
}
// ── Lifecycle ─────────────────────────────────────────────────────────
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_setup)
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
bindViews()
showStep(0)
}
override fun onBackPressed() {
when (currentStep) {
0 -> confirmExit()
else -> showStep(currentStep - 1)
}
}
override fun onDestroy() {
pollHandler.removeCallbacksAndMessages(null)
discoverCancelled.set(true)
super.onDestroy()
}
// ── Binding ────────────────────────────────────────────────────────────
private fun bindViews() {
progressDots = findViewById(R.id.setupProgressDots)
stepWelcome = findViewById(R.id.stepWelcome)
stepPermissions = findViewById(R.id.stepPermissions)
stepServer = findViewById(R.id.stepServer)
stepScale = findViewById(R.id.stepScale)
stepDone = findViewById(R.id.stepDone)
// Server step
urlEdit = findViewById(R.id.setupUrlEdit)
urlStatus = findViewById(R.id.setupUrlStatus)
btnTestUrl = findViewById(R.id.btnSetupTestUrl)
btnDiscover = findViewById(R.id.btnDiscover)
discoverStatus = findViewById(R.id.discoverStatus)
// Scale step
scaleQuestionCard = findViewById(R.id.scaleQuestionCard)
gatewayInfoCard = findViewById(R.id.gatewayInfoCard)
gatewayInstallCard = findViewById(R.id.gatewayInstallCard)
gatewayStatusIcon = findViewById(R.id.gatewayStatusIcon)
gatewayStatusText = findViewById(R.id.gatewayStatusText)
gatewayStatusDetail = findViewById(R.id.gatewayStatusDetail)
btnInstallGateway = findViewById(R.id.btnInstallGateway)
gatewayProgressBar = findViewById(R.id.gatewayProgressBar)
gatewayProgressText = findViewById(R.id.gatewayProgressText)
step3NextButtons = findViewById(R.id.step3NextButtons)
// Done step
summaryText = findViewById(R.id.setupSummaryText)
// Permissions step
permsGrantedCard = findViewById(R.id.permsGrantedCard)
// Pre-fill saved URL
val savedUrl = prefs.getString(KEY_URL, "") ?: ""
if (savedUrl.isNotEmpty()) urlEdit.setText(savedUrl)
// ── Welcome ──────────────────────────────────────────────────────
findViewById<MaterialButton>(R.id.btnSetupExit).setOnClickListener { confirmExit() }
findViewById<MaterialButton>(R.id.btnWelcomeStart).setOnClickListener { showStep(1) }
// ── Permissions ──────────────────────────────────────────────────
findViewById<MaterialButton>(R.id.btnGrantPerms).setOnClickListener { requestPermissions() }
findViewById<MaterialButton>(R.id.btnPermsBack).setOnClickListener { showStep(0) }
findViewById<MaterialButton>(R.id.btnPermsNext).setOnClickListener { showStep(2) }
// ── Server ───────────────────────────────────────────────────────
btnDiscover.setOnClickListener { autoDiscover() }
btnTestUrl.setOnClickListener { testConnection() }
findViewById<MaterialButton>(R.id.btnServerBack).setOnClickListener { showStep(1) }
findViewById<MaterialButton>(R.id.btnServerNext).setOnClickListener {
val url = urlEdit.text.toString().trim()
if (url.isEmpty()) {
showUrlStatus(getString(R.string.setup_enter_url), false)
return@setOnClickListener
}
prefs.edit().putString(KEY_URL, url).apply()
ErrorReporter.init(this, url)
showStep(3)
}
// ── Scale ─────────────────────────────────────────────────────────
findViewById<MaterialButton>(R.id.btnScaleYes).setOnClickListener {
scaleQuestionCard.visibility = View.GONE
gatewayInfoCard.visibility = View.VISIBLE
gatewayInstallCard.visibility = View.VISIBLE
step3NextButtons.visibility = View.VISIBLE
checkGatewayStatus()
}
findViewById<MaterialButton>(R.id.btnScaleNo).setOnClickListener {
prefs.edit().putBoolean(KEY_HAS_SCALE, false).apply()
showStep(4)
}
btnInstallGateway.setOnClickListener {
pendingApkDownloadUrl = GATEWAY_DOWNLOAD_URL
triggerApkDownload(GATEWAY_DOWNLOAD_URL)
}
findViewById<MaterialButton>(R.id.btnScaleBack).setOnClickListener { showStep(2) }
findViewById<MaterialButton>(R.id.btnScaleNext).setOnClickListener {
prefs.edit().putBoolean(KEY_HAS_SCALE, true).apply()
showStep(4)
}
// ── Done ──────────────────────────────────────────────────────────
findViewById<MaterialButton>(R.id.btnLaunch).setOnClickListener { finishSetup() }
}
// ── Step navigation ───────────────────────────────────────────────────
private fun showStep(step: Int) {
currentStep = step
stepWelcome.visibility = if (step == 0) View.VISIBLE else View.GONE
stepPermissions.visibility = if (step == 1) View.VISIBLE else View.GONE
stepServer.visibility = if (step == 2) View.VISIBLE else View.GONE
stepScale.visibility = if (step == 3) View.VISIBLE else View.GONE
stepDone.visibility = if (step == 4) View.VISIBLE else View.GONE
updateProgressDots()
// Reset scale step when entering it
if (step == 3) {
scaleQuestionCard.visibility = View.VISIBLE
gatewayInfoCard.visibility = View.GONE
gatewayInstallCard.visibility = View.GONE
step3NextButtons.visibility = View.GONE
}
// Build summary when entering done step
if (step == 4) buildSummary()
// Cancel auto-discover when leaving server step
if (step != 2) discoverCancelled.set(true)
// Scroll to top
try { findViewById<ScrollView>(R.id.setupScrollView).scrollTo(0, 0) } catch (_: Exception) {}
}
private fun updateProgressDots() {
progressDots.removeAllViews()
// 4 dots (steps 14); step 0 welcome uses no dots
val active = maxOf(currentStep, 1)
val density = resources.displayMetrics.density
for (i in 1..4) {
val dot = View(this)
val sizeDp = if (i == active) 10 else 7
val px = (sizeDp * density).toInt()
val lp = LinearLayout.LayoutParams(px, px)
lp.marginStart = (5 * density).toInt()
lp.marginEnd = (5 * density).toInt()
dot.layoutParams = lp
val bg = android.graphics.drawable.GradientDrawable()
bg.shape = android.graphics.drawable.GradientDrawable.OVAL
bg.setColor(when {
i < active -> 0xFF34d399.toInt() // completed
i == active -> 0xFF7c3aed.toInt() // current
else -> 0xFF334155.toInt() // future
})
dot.background = bg
progressDots.addView(dot)
}
}
// ── Exit ──────────────────────────────────────────────────────────────
private fun confirmExit() {
AlertDialog.Builder(this)
.setTitle(getString(R.string.setup_exit_title))
.setMessage(getString(R.string.setup_exit_message))
.setPositiveButton(getString(R.string.setup_exit_confirm)) { _, _ ->
pollHandler.removeCallbacksAndMessages(null)
discoverCancelled.set(true)
finishAffinity()
}
.setNegativeButton(getString(R.string.setup_exit_cancel), null)
.show()
}
// ── Permissions ───────────────────────────────────────────────────────
private fun allPermissionsGranted(): Boolean {
val cam = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
val mic = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
return cam && mic
}
private fun requestPermissions() {
val needed = mutableListOf<String>()
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED)
needed.add(Manifest.permission.CAMERA)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED)
needed.add(Manifest.permission.RECORD_AUDIO)
if (Build.VERSION.SDK_INT >= 33) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED)
needed.add(Manifest.permission.READ_MEDIA_IMAGES)
} else {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
needed.add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
if (needed.isEmpty()) {
// Already granted — show confirmation and allow next
permsGrantedCard.visibility = View.VISIBLE
} else {
ActivityCompat.requestPermissions(this, needed.toTypedArray(), PERMISSION_REQUEST_CODE)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE) {
permsGrantedCard.visibility = View.VISIBLE
// Proceed to next step regardless — user can always grant later
Handler(Looper.getMainLooper()).postDelayed({ showStep(2) }, 600)
}
}
// ── Connection Test ───────────────────────────────────────────────────
private fun testConnection() {
val url = urlEdit.text.toString().trim()
if (url.isEmpty()) { showUrlStatus(getString(R.string.setup_enter_url), false); return }
showUrlStatus(getString(R.string.setup_testing), null)
Thread {
val base = url.trimEnd('/')
// Try both API path variants
val candidates = listOf(
"$base/api/index.php?action=get_settings",
"$base/api/?action=get_settings"
)
var found = false
for (apiUrl in candidates) {
val conn = openConn(apiUrl) ?: continue
try {
val code = conn.responseCode
if (code !in 200..399) { conn.disconnect(); continue }
val body = conn.inputStream.bufferedReader().readText()
conn.disconnect()
if (body.contains("gemini_key_set") || body.contains("\"success\"")) {
found = true; break
}
} catch (_: Exception) { try { conn.disconnect() } catch (_: Exception) {} }
}
// If API not found, try plain base URL to distinguish unreachable vs wrong path
if (!found) {
var baseReachable = false
try {
val conn = openConn(base) ?: openConn("$base/")
val code = conn?.responseCode ?: -1
conn?.disconnect()
baseReachable = code in 200..499
} catch (_: Exception) {}
runOnUiThread {
if (baseReachable) {
showUrlStatus("${getString(R.string.setup_api_not_found)}", false)
} else {
showUrlStatus("${getString(R.string.setup_unreachable)}", false)
}
}
} else {
runOnUiThread { showUrlStatus("${getString(R.string.setup_server_found)}", true) }
}
}.start()
}
private fun showUrlStatus(text: String, success: Boolean?) {
urlStatus.visibility = View.VISIBLE
urlStatus.text = text
urlStatus.setTextColor(when (success) {
true -> 0xFF34d399.toInt()
false -> 0xFFf87171.toInt()
null -> 0xFF94a3b8.toInt()
})
}
private fun openConn(urlStr: String): HttpURLConnection? {
return try {
val conn = URL(urlStr).openConnection()
if (conn is HttpsURLConnection) {
val trustAll = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(c: Array<java.security.cert.X509Certificate>?, t: String?) {}
override fun checkServerTrusted(c: Array<java.security.cert.X509Certificate>?, t: String?) {}
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> = arrayOf()
})
val sc = SSLContext.getInstance("TLS")
sc.init(null, trustAll, java.security.SecureRandom())
conn.sslSocketFactory = sc.socketFactory
conn.hostnameVerifier = javax.net.ssl.HostnameVerifier { _, _ -> true }
}
(conn as HttpURLConnection).apply {
requestMethod = "GET"
connectTimeout = 3000
readTimeout = 3000
}
} catch (_: Exception) { null }
}
// ── Auto-Discover ─────────────────────────────────────────────────────
@Suppress("DEPRECATION")
private fun autoDiscover() {
discoverCancelled.set(false)
btnDiscover.isEnabled = false
btnDiscover.text = getString(R.string.setup_discovering)
discoverStatus.visibility = View.VISIBLE
discoverStatus.text = getString(R.string.setup_discovering_detail)
discoverStatus.setTextColor(0xFF94a3b8.toInt())
// Determine local subnet
val wifiMgr = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val ipInt = wifiMgr.connectionInfo.ipAddress
val subnets = mutableListOf<String>()
if (ipInt != 0) {
val a = (ipInt shr 0) and 0xFF
val b = (ipInt shr 8) and 0xFF
val c = (ipInt shr 16) and 0xFF
subnets += "$a.$b.$c"
}
// Always include common subnets as fallback
for (s in listOf("192.168.1", "192.168.0", "192.168.2", "10.0.0")) {
if (!subnets.contains(s)) subnets += s
}
val ports = listOf(80, 8080)
val paths = listOf(
"/api/index.php?action=get_settings",
"/dispensa/api/index.php?action=get_settings",
"/evershelf/api/index.php?action=get_settings"
)
val executor = Executors.newFixedThreadPool(40)
val found = AtomicBoolean(false)
Thread {
val futures = mutableListOf<java.util.concurrent.Future<String?>>()
outer@ for (subnet in subnets) {
for (i in 1..254) {
if (discoverCancelled.get() || found.get()) break@outer
val ip = "$subnet.$i"
for (port in ports) {
if (discoverCancelled.get() || found.get()) break@outer
futures += executor.submit<String?> submit@{
if (discoverCancelled.get() || found.get()) return@submit null
val scheme = if (port == 443 || port == 8443) "https" else "http"
for (path in paths) {
val urlStr = "$scheme://$ip:$port$path"
try {
val conn = openConn(urlStr) ?: continue
val code = conn.responseCode
if (code in 200..399) {
val body = conn.inputStream.bufferedReader().readText()
conn.disconnect()
if (body.contains("gemini_key_set") || body.contains("\"success\"")) {
val base = urlStr.substringBefore("/api/")
return@submit "$base/"
}
} else conn.disconnect()
} catch (_: Exception) {}
}
null
}
}
}
}
// Collect results
for (f in futures) {
if (discoverCancelled.get()) break
val result = try { f.get(4, TimeUnit.SECONDS) } catch (_: Exception) { null }
if (result != null && found.compareAndSet(false, true)) {
runOnUiThread {
urlEdit.setText(result)
discoverStatus.text = "${getString(R.string.setup_server_found)}: $result"
discoverStatus.setTextColor(0xFF34d399.toInt())
showUrlStatus("${getString(R.string.setup_server_found)}", true)
btnDiscover.isEnabled = true
btnDiscover.text = getString(R.string.setup_discover_btn)
}
break
}
}
executor.shutdown()
if (!found.get() && !discoverCancelled.get()) {
runOnUiThread {
discoverStatus.text = getString(R.string.setup_discover_not_found)
discoverStatus.setTextColor(0xFFf87171.toInt())
btnDiscover.isEnabled = true
btnDiscover.text = getString(R.string.setup_discover_btn)
}
} else if (!found.get()) {
runOnUiThread {
btnDiscover.isEnabled = true
btnDiscover.text = getString(R.string.setup_discover_btn)
}
}
}.start()
}
// ── Gateway ────────────────────────────────────────────────────────────
private fun isGatewayInstalled() = try {
packageManager.getPackageInfo(GATEWAY_PACKAGE, 0); true
} catch (_: PackageManager.NameNotFoundException) { false }
private fun checkGatewayStatus() {
if (isGatewayInstalled()) {
gatewayStatusIcon.text = ""
gatewayStatusText.text = getString(R.string.wizard_gateway_installed)
gatewayStatusDetail.text = getString(R.string.wizard_gateway_installed_detail)
gatewayStatusDetail.setTextColor(0xFF34d399.toInt())
btnInstallGateway.visibility = View.GONE
gatewayProgressBar.visibility = View.GONE
gatewayProgressText.visibility = View.GONE
} else {
gatewayStatusIcon.text = "📲"
gatewayStatusText.text = getString(R.string.wizard_gateway_not_installed)
gatewayStatusDetail.text = getString(R.string.wizard_gateway_not_installed_detail)
gatewayStatusDetail.setTextColor(0xFFfbbf24.toInt())
btnInstallGateway.visibility = View.VISIBLE
}
}
private fun setGatewayUI(icon: String, text: String, detail: String, color: Int,
btnEnabled: Boolean = true, progress: Int = -2) {
runOnUiThread {
gatewayStatusIcon.text = icon
gatewayStatusText.text = text
gatewayStatusDetail.text = detail
gatewayStatusDetail.setTextColor(color)
btnInstallGateway.isEnabled = btnEnabled
when {
progress == -2 -> {
gatewayProgressBar.visibility = View.GONE
gatewayProgressText.visibility = View.GONE
}
progress == -1 -> {
gatewayProgressBar.isIndeterminate = true
gatewayProgressBar.visibility = View.VISIBLE
gatewayProgressText.visibility = View.GONE
}
else -> {
gatewayProgressBar.isIndeterminate = false
gatewayProgressBar.progress = progress
gatewayProgressBar.visibility = View.VISIBLE
gatewayProgressText.visibility = View.VISIBLE
}
}
}
}
private fun startProgressPoll(downloadId: Long) {
activeDownloadId = downloadId
pollHandler.removeCallbacksAndMessages(null)
fun tick() {
if (activeDownloadId != downloadId) return
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 txt = if (tot > 0) "%.1f / %.1f MB".format(dl / 1_048_576f, tot / 1_048_576f) else ""
setGatewayUI(
"",
getString(R.string.install_downloading) + if (tot > 0) " ($pct%)" else "",
txt, 0xFF94a3b8.toInt(), btnEnabled = false, progress = pct
)
runOnUiThread { gatewayProgressText.text = txt }
pollHandler.postDelayed({ tick() }, 500)
} else {
c.close()
}
}
pollHandler.post { tick() }
}
private fun triggerApkDownload(apkUrl: String) {
pendingApkDownloadUrl = apkUrl
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !packageManager.canRequestPackageInstalls()) {
@Suppress("DEPRECATION")
startActivityForResult(
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:$packageName")),
INSTALL_PERM_REQUEST
)
return
}
setGatewayUI("", getString(R.string.install_downloading), "", 0xFF94a3b8.toInt(), btnEnabled = false, progress = -1)
val destDir = getExternalFilesDir(null) ?: filesDir
val destFile = java.io.File(destDir, "evershelf-gateway-setup.apk")
val dm = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
val req = DownloadManager.Request(Uri.parse(apkUrl)).apply {
setTitle("EverShelf Scale Gateway")
setDescription(getString(R.string.install_downloading))
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setDestinationUri(Uri.fromFile(destFile))
setMimeType("application/vnd.android.package-archive")
}
val downloadId = dm.enqueue(req)
startProgressPoll(downloadId)
val receiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id != downloadId) return
unregisterReceiver(this)
val q = DownloadManager.Query().setFilterById(downloadId)
val c = (getSystemService(DOWNLOAD_SERVICE) as DownloadManager).query(q)
var ok = false
if (c.moveToFirst()) {
ok = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) ==
DownloadManager.STATUS_SUCCESSFUL
}
c.close()
pollHandler.removeCallbacksAndMessages(null)
activeDownloadId = -1
if (ok) {
setGatewayUI("", getString(R.string.install_installing), "", 0xFF94a3b8.toInt(), btnEnabled = false, progress = -1)
installApk(destFile)
} else {
setGatewayUI("", getString(R.string.install_error_download), getString(R.string.install_error_download_detail), 0xFFf87171.toInt())
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), RECEIVER_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
}
private fun installApk(file: java.io.File) {
if (!file.exists() || file.length() == 0L) {
setGatewayUI("", getString(R.string.install_error_download), "File APK non trovato sul dispositivo.", 0xFFf87171.toInt())
return
}
// Validate APK magic bytes (ZIP header)
val magic = try { file.inputStream().use { s -> val b = ByteArray(4); s.read(b); b } } catch (_: Exception) { null }
if (magic == null || magic[0] != 0x50.toByte() || magic[1] != 0x4B.toByte()) {
setGatewayUI("", getString(R.string.install_error_download), "Il file scaricato non è un APK valido.", 0xFFf87171.toInt())
file.delete()
return
}
installWithPackageInstaller(file, GATEWAY_PACKAGE)
}
private fun installWithPackageInstaller(file: java.io.File, targetPkg: String) {
try {
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)
}
}
val action = "it.dadaloop.evershelf.kiosk.SETUP_INSTALL_$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 -> {
@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) {
pendingInstallFile = file
pendingInstallPkg = targetPkg
@Suppress("DEPRECATION")
startActivityForResult(confirmIntent, INSTALL_CONFIRM_REQUEST)
}
}
android.content.pm.PackageInstaller.STATUS_SUCCESS -> {
setGatewayUI("", getString(R.string.install_success), getString(R.string.install_success_detail), 0xFF34d399.toInt(), btnEnabled = false)
Handler(Looper.getMainLooper()).postDelayed({ checkGatewayStatus() }, 1500)
}
android.content.pm.PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
android.content.pm.PackageInstaller.STATUS_FAILURE_CONFLICT -> {
runOnUiThread { offerUninstallAndRetry(file, targetPkg) }
}
else -> {
val msg = intent?.getStringExtra(android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "status=$status"
setGatewayUI("", getString(R.string.install_error_install), msg, 0xFFf87171.toInt())
val pkgInstalled = try { packageManager.getPackageInfo(targetPkg, 0); true } catch (_: Exception) { false }
if (pkgInstalled) runOnUiThread { offerUninstallAndRetry(file, targetPkg) }
}
}
}
}
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
)
session.commit(pi2.intentSender)
}
setGatewayUI("", getString(R.string.install_installing), "", 0xFF94a3b8.toInt(), btnEnabled = false, progress = -1)
} catch (e: Exception) {
setGatewayUI("", getString(R.string.install_error_install), e.message ?: "", 0xFFf87171.toInt())
}
}
private fun offerUninstallAndRetry(file: java.io.File, pkg: String) {
pendingInstallFile = file
pendingInstallPkg = pkg
AlertDialog.Builder(this)
.setTitle("⚠️ Conflitto firma APK")
.setMessage("L'app installata usa una firma diversa. Devi prima disinstallare la versione precedente.\n\nDisinstalla ora? L'installazione riprenderà automaticamente.")
.setPositiveButton("Disinstalla") { _, _ ->
@Suppress("DEPRECATION")
startActivityForResult(
Intent(Intent.ACTION_DELETE, Uri.parse("package:$pkg")),
UNINSTALL_REQUEST
)
}
.setNegativeButton("Annulla", null)
.show()
}
// ── Summary / Finish ─────────────────────────────────────────────────
private fun buildSummary() {
val url = prefs.getString(KEY_URL, "") ?: ""
val hasScale = prefs.getBoolean(KEY_HAS_SCALE, false)
val gwOk = hasScale && isGatewayInstalled()
val sb = StringBuilder()
if (url.isNotEmpty()) sb.appendLine("🌐 Server: $url")
sb.appendLine(when {
gwOk -> "✅ Scale Gateway: installato"
hasScale -> "⚠️ Scale Gateway: non ancora installato"
else -> "⏭ Bilancia: non configurata"
})
summaryText.text = sb.toString().trimEnd()
}
private fun finishSetup() {
prefs.edit().putBoolean(KEY_SETUP_COMPLETE, true).apply()
setResult(RESULT_OK)
finish()
}
// ── Activity Results ─────────────────────────────────────────────────
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
INSTALL_PERM_REQUEST -> {
if (pendingApkDownloadUrl.isNotEmpty()) triggerApkDownload(pendingApkDownloadUrl)
}
INSTALL_CONFIRM_REQUEST -> {
if (resultCode == RESULT_OK) {
setGatewayUI("", getString(R.string.install_success), getString(R.string.install_success_detail), 0xFF34d399.toInt(), btnEnabled = false)
Handler(Looper.getMainLooper()).postDelayed({ checkGatewayStatus() }, 1500)
} else {
val f = pendingInstallFile
val pkg = pendingInstallPkg
if (f != null && f.exists() && pkg.isNotEmpty()) {
runOnUiThread {
AlertDialog.Builder(this)
.setTitle("⚠️ Installazione non riuscita")
.setMessage("Se c'è un conflitto di firma, devi disinstallare la versione precedente.\n\nDisinstalla ora?")
.setPositiveButton("Disinstalla") { _, _ ->
startActivityForResult(Intent(Intent.ACTION_DELETE, Uri.parse("package:$pkg")), UNINSTALL_REQUEST)
}
.setNegativeButton("Annulla", null).show()
}
}
}
}
UNINSTALL_REQUEST -> {
val f = pendingInstallFile
val pkg = pendingInstallPkg
if (f != null && f.exists() && pkg.isNotEmpty()) {
Handler(Looper.getMainLooper()).postDelayed({ installWithPackageInstaller(f, pkg) }, 600)
}
}
}
}
}
@@ -4,7 +4,7 @@
android:layout_height="match_parent"
android:background="#0f172a">
<!-- Splash screen (shown briefly on launch) -->
<!-- Splash screen (shown briefly on launch if setup is already done) -->
<LinearLayout
android:id="@+id/splashContainer"
android:layout_width="match_parent"
@@ -32,7 +32,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Smart Pantry Manager"
android:text="La tua dispensa smart"
android:textColor="#64748b"
android:textSize="16sp"
android:layout_marginBottom="48dp" />
@@ -43,486 +43,6 @@
android:indeterminateTint="#7c3aed" />
</LinearLayout>
<!-- Setup wizard container -->
<ScrollView
android:id="@+id/wizardContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:paddingTop="48dp"
android:paddingBottom="48dp">
<!-- Step indicator -->
<LinearLayout
android:id="@+id/stepIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="40dp"
android:gravity="center" />
<!-- Step 1: Welcome -->
<LinearLayout
android:id="@+id/step1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:visibility="visible">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:src="@drawable/ic_launcher_foreground"
android:layout_marginBottom="16dp"
android:contentDescription="EverShelf" />
<TextView
android:id="@+id/wizardTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Welcome to\nEverShelf Kiosk"
android:textColor="#f1f5f9"
android:textSize="28sp"
android:textStyle="bold"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Turn this tablet into a dedicated kitchen panel for your smart pantry. Always-on display with built-in smart scale support."
android:textColor="#94a3b8"
android:textSize="16sp"
android:gravity="center"
android:lineSpacingExtra="6dp"
android:layout_marginBottom="48dp" />
<!-- Feature highlights -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="48dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📺 Full-screen kiosk mode"
android:textColor="#cbd5e1"
android:textSize="15sp"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚖️ Bluetooth scale via Gateway app"
android:textColor="#cbd5e1"
android:textSize="15sp"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🔋 Always-on, screen never sleeps"
android:textColor="#cbd5e1"
android:textSize="15sp"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📷 Barcode scanning from WebView"
android:textColor="#cbd5e1"
android:textSize="15sp"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnGetStarted"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Get Started"
android:textSize="17sp"
android:textAllCaps="false"
android:backgroundTint="#7c3aed" />
</LinearLayout>
<!-- Step 2: Server URL -->
<LinearLayout
android:id="@+id/step2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🌐"
android:textSize="48sp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connect to EverShelf"
android:textColor="#f1f5f9"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enter the URL of your EverShelf server. This is the same address you use in your browser."
android:textColor="#94a3b8"
android:textSize="15sp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="32dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Server URL"
android:textColor="#cbd5e1"
android:textSize="13sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<EditText
android:id="@+id/wizardUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="https://192.168.1.100/dispensa/"
android:inputType="textUri"
android:textColor="#f1f5f9"
android:textColorHint="#475569"
android:background="@drawable/input_background"
android:padding="16dp"
android:textSize="16sp"
android:singleLine="true"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/urlStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textSize="13sp"
android:visibility="gone"
android:layout_marginBottom="12dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnTestUrl"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="🔗 Test Connection"
android:textSize="15sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#334155"
android:textColor="#94a3b8"
android:layout_marginBottom="24dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="💡 Tip: Copy the exact URL from your browser address bar — including the path (e.g. /dispensa/)."
android:textColor="#64748b"
android:textSize="13sp"
android:lineSpacingExtra="2dp"
android:background="@drawable/tip_background"
android:padding="14dp"
android:layout_marginBottom="32dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStep2Back"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="Back"
android:textSize="15sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#334155"
android:textColor="#94a3b8"
android:layout_marginEnd="12dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStep2Next"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="2"
android:text="Next →"
android:textSize="15sp"
android:textAllCaps="false"
android:backgroundTint="#7c3aed" />
</LinearLayout>
</LinearLayout>
<!-- Step 3: Scale Setup -->
<LinearLayout
android:id="@+id/step3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="⚖️"
android:textSize="48sp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/wizard_step3_title"
android:textColor="#f1f5f9"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/wizard_step3_description"
android:textColor="#94a3b8"
android:textSize="15sp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="16dp" />
<!-- Server reachability check — shown as soon as step 3 is entered -->
<LinearLayout
android:id="@+id/serverStatusCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/card_background"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/serverCheckIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="⏳"
android:textSize="20sp"
android:layout_marginEnd="12dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/serverCheckText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/wizard_server_checking"
android:textColor="#94a3b8"
android:textSize="13sp" />
<TextView
android:id="@+id/serverCheckDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textColor="#64748b"
android:textSize="11sp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
<!-- Scale question card — shown first, hidden after answer -->
<LinearLayout
android:id="@+id/scaleQuestionLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_background"
android:padding="20dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/wizard_step3_question"
android:textColor="#f1f5f9"
android:textSize="17sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="20dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnScaleYes"
android:layout_width="match_parent"
android:layout_height="52dp"
android:text="@string/wizard_step3_yes"
android:textSize="15sp"
android:textAllCaps="false"
android:backgroundTint="#059669"
android:layout_marginBottom="10dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnScaleNo"
android:layout_width="match_parent"
android:layout_height="52dp"
android:text="@string/wizard_step3_no"
android:textSize="15sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#334155"
android:textColor="#94a3b8" />
</LinearLayout>
<!-- Scale status card — shown after user answers "Yes" -->
<LinearLayout
android:id="@+id/scaleStatusCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_background"
android:padding="20dp"
android:layout_marginBottom="16dp"
android:visibility="gone">
<TextView
android:id="@+id/scaleStatusIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🔍"
android:textSize="32sp"
android:layout_gravity="center"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/scaleStatusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Checking..."
android:textColor="#cbd5e1"
android:textSize="16sp"
android:gravity="center"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/scaleStatusDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
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 -->
<LinearLayout
android:id="@+id/step3BottomButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="16dp"
android:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStep3Back"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="@string/btn_back"
android:textSize="15sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#334155"
android:textColor="#94a3b8"
android:layout_marginEnd="12dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnFinish"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="2"
android:text="@string/btn_launch"
android:textSize="16sp"
android:textAllCaps="false"
android:backgroundTint="#059669" />
</LinearLayout>
<!-- Install/Update gateway button — shown by checkGatewayStatus() as needed -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSkipScale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#7c3aed"
android:textColor="#a78bfa"
android:layout_marginTop="12dp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- WebView (shown after setup) -->
<WebView
android:id="@+id/webView"
@@ -545,7 +65,7 @@
android:scaleType="centerInside"
android:visibility="gone" />
<!-- ── Update banner (shown at the TOP when a new version is available) ── -->
<!-- Update banner (shown at the TOP when a new version is available) -->
<LinearLayout
android:id="@+id/updateBanner"
android:layout_width="match_parent"
@@ -573,8 +93,7 @@
android:layout_weight="1"
android:textColor="#fbbf24"
android:textSize="13sp"
android:text=""
android:drawablePadding="6dp" />
android:text="" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnInstallUpdate"
@@ -599,7 +118,7 @@
style="@style/Widget.MaterialComponents.Button.TextButton" />
</LinearLayout>
<!-- Thin progress bar at the bottom of the banner — visible during download/install -->
<!-- Thin progress bar at the bottom of the banner -->
<ProgressBar
android:id="@+id/bannerProgressBar"
style="@android:style/Widget.ProgressBar.Horizontal"
@@ -614,4 +133,30 @@
</LinearLayout>
<!-- Download progress (used by download progress poll) -->
<ProgressBar
android:id="@+id/downloadProgressBar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_gravity="top"
android:layout_marginTop="56dp"
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="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"
android:layout_marginTop="64dp"
android:text=""
android:textColor="#94a3b8"
android:textSize="12sp"
android:visibility="gone" />
</FrameLayout>
@@ -0,0 +1,961 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#0f172a">
<!-- ── Top bar: progress dots + exit button ── -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="20dp"
android:paddingEnd="8dp"
android:background="#0f172a">
<LinearLayout
android:id="@+id/setupProgressDots"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="start|center_vertical" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSetupExit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="✕ Esci"
android:textSize="13sp"
android:textColor="#64748b"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.TextButton" />
</LinearLayout>
<!-- Thin divider -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#1e293b" />
<!-- ── Scrollable step content ── -->
<ScrollView
android:id="@+id/setupScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:paddingStart="28dp"
android:paddingEnd="28dp"
android:paddingTop="32dp"
android:paddingBottom="40dp">
<!-- ════════════════════════════════════════════
STEP 0 — Welcome / Intro / Privacy
════════════════════════════════════════════ -->
<LinearLayout
android:id="@+id/stepWelcome"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:visibility="visible">
<!-- App logo -->
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:src="@drawable/ic_launcher_foreground"
android:layout_marginBottom="16dp"
android:contentDescription="EverShelf" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="EverShelf Kiosk"
android:textColor="#f1f5f9"
android:textSize="30sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="La tua dispensa smart in cucina"
android:textColor="#7c3aed"
android:textSize="16sp"
android:gravity="center"
android:layout_marginBottom="28dp" />
<!-- What is EverShelf -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Trasforma il tuo tablet in un pannello da cucina sempre attivo. Gestisci la dispensa, scansiona i prodotti, ricevi suggerimenti intelligenti — tutto dal tuo server di casa."
android:textColor="#94a3b8"
android:textSize="15sp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="28dp" />
<!-- Privacy card (highlighted) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_background"
android:padding="20dp"
android:layout_marginBottom="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🔒 Privacy e dati"
android:textColor="#f1f5f9"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="top"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="22dp"
android:layout_height="wrap_content"
android:text="🏠"
android:textSize="14sp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Funziona completamente offline — i tuoi dati restano sul tuo server, dentro casa tua."
android:textColor="#94a3b8"
android:textSize="14sp"
android:lineSpacingExtra="2dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="top"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="22dp"
android:layout_height="wrap_content"
android:text="🤖"
android:textSize="14sp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Solo l'assistente AI (opzionale) comunica con i server Google Gemini. Puoi usare EverShelf senza AI in modo totalmente offline."
android:textColor="#94a3b8"
android:textSize="14sp"
android:lineSpacingExtra="2dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="top">
<TextView
android:layout_width="22dp"
android:layout_height="wrap_content"
android:text="🚫"
android:textSize="14sp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Nessun account richiesto. Nessun abbonamento. Nessun tracciamento."
android:textColor="#94a3b8"
android:textSize="14sp"
android:lineSpacingExtra="2dp" />
</LinearLayout>
</LinearLayout>
<!-- Feature highlights -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/tip_background"
android:padding="16dp"
android:layout_marginBottom="32dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📺 Schermo sempre acceso, modalità kiosk"
android:textColor="#cbd5e1"
android:textSize="14sp"
android:paddingTop="4dp"
android:paddingBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📷 Scansione barcode dal browser"
android:textColor="#cbd5e1"
android:textSize="14sp"
android:paddingTop="4dp"
android:paddingBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚖️ Bilancia Bluetooth via Gateway app"
android:textColor="#cbd5e1"
android:textSize="14sp"
android:paddingTop="4dp"
android:paddingBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🔄 Aggiornamenti automatici"
android:textColor="#cbd5e1"
android:textSize="14sp"
android:paddingTop="4dp"
android:paddingBottom="4dp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnWelcomeStart"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Inizia la configurazione →"
android:textSize="16sp"
android:textAllCaps="false"
android:backgroundTint="#7c3aed" />
</LinearLayout>
<!-- ════════════════════════════════════════════
STEP 1 — Permissions rationale
════════════════════════════════════════════ -->
<LinearLayout
android:id="@+id/stepPermissions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🔐"
android:textSize="52sp"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Permessi necessari"
android:textColor="#f1f5f9"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="6dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="EverShelf Kiosk ha bisogno di questi permessi per funzionare. Ecco perché:"
android:textColor="#94a3b8"
android:textSize="15sp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="28dp" />
<!-- Camera permission card -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/card_background"
android:padding="16dp"
android:gravity="top"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="44dp"
android:layout_height="wrap_content"
android:text="📷"
android:textSize="24sp"
android:gravity="center" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Fotocamera"
android:textColor="#f1f5f9"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Per scansionare i codici a barre dei prodotti e fotografare ciò che aggiungi alla dispensa."
android:textColor="#94a3b8"
android:textSize="14sp"
android:lineSpacingExtra="2dp" />
</LinearLayout>
</LinearLayout>
<!-- Microphone permission card -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/card_background"
android:padding="16dp"
android:gravity="top"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="44dp"
android:layout_height="wrap_content"
android:text="🎤"
android:textSize="24sp"
android:gravity="center" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Microfono"
android:textColor="#f1f5f9"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Per l'assistente vocale integrato: legge le scadenze e risponde ai comandi vocali."
android:textColor="#94a3b8"
android:textSize="14sp"
android:lineSpacingExtra="2dp" />
</LinearLayout>
</LinearLayout>
<!-- Install packages permission card -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/card_background"
android:padding="16dp"
android:gravity="top"
android:layout_marginBottom="28dp">
<TextView
android:layout_width="44dp"
android:layout_height="wrap_content"
android:text="📦"
android:textSize="24sp"
android:gravity="center" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Installa applicazioni"
android:textColor="#f1f5f9"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Per installare gli aggiornamenti automatici dell'app kiosk e dell'app Scale Gateway direttamente da GitHub."
android:textColor="#94a3b8"
android:textSize="14sp"
android:lineSpacingExtra="2dp" />
</LinearLayout>
</LinearLayout>
<!-- Permissions granted confirmation (hidden until granted) -->
<LinearLayout
android:id="@+id/permsGrantedCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/card_background"
android:padding="16dp"
android:gravity="center_vertical"
android:layout_marginBottom="20dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="✅"
android:textSize="20sp"
android:layout_marginEnd="10dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Permessi concessi! Puoi procedere."
android:textColor="#34d399"
android:textSize="14sp" />
</LinearLayout>
<!-- Grant button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnGrantPerms"
android:layout_width="match_parent"
android:layout_height="52dp"
android:text="🔓 Concedi i permessi"
android:textSize="15sp"
android:textAllCaps="false"
android:backgroundTint="#7c3aed"
android:layout_marginBottom="12dp" />
<!-- Navigation -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPermsBack"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="← Indietro"
android:textSize="14sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#334155"
android:textColor="#64748b"
android:layout_marginEnd="10dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPermsNext"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="2"
android:text="Avanti →"
android:textSize="14sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#7c3aed"
android:textColor="#a78bfa" />
</LinearLayout>
</LinearLayout>
<!-- ════════════════════════════════════════════
STEP 2 — Server URL + auto-discovery
════════════════════════════════════════════ -->
<LinearLayout
android:id="@+id/stepServer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🌐"
android:textSize="52sp"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Trova il server"
android:textColor="#f1f5f9"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="EverShelf gira sul tuo server di casa (Raspberry Pi, NAS, PC...). Cerca automaticamente nella rete locale o inserisci l'indirizzo manualmente."
android:textColor="#94a3b8"
android:textSize="15sp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="28dp" />
<!-- Auto-discover button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDiscover"
android:layout_width="match_parent"
android:layout_height="52dp"
android:text="🔍 Cerca nella rete locale"
android:textSize="15sp"
android:textAllCaps="false"
android:backgroundTint="#0f4c8c"
android:layout_marginBottom="8dp" />
<!-- Discover status text -->
<TextView
android:id="@+id/discoverStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textSize="13sp"
android:visibility="gone"
android:lineSpacingExtra="2dp"
android:layout_marginBottom="16dp" />
<!-- Divider with OR -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:background="#334155" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" oppure inserisci manualmente "
android:textColor="#475569"
android:textSize="12sp" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:background="#334155" />
</LinearLayout>
<!-- URL input label -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="URL del server"
android:textColor="#cbd5e1"
android:textSize="13sp"
android:textStyle="bold"
android:layout_marginBottom="6dp" />
<EditText
android:id="@+id/setupUrlEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="http://192.168.1.100/dispensa/"
android:inputType="textUri"
android:textColor="#f1f5f9"
android:textColorHint="#475569"
android:background="@drawable/input_background"
android:padding="16dp"
android:textSize="15sp"
android:singleLine="true"
android:layout_marginBottom="10dp" />
<!-- Test connection button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSetupTestUrl"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="🔗 Testa connessione"
android:textSize="14sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#334155"
android:textColor="#94a3b8"
android:layout_marginBottom="8dp" />
<!-- URL status -->
<TextView
android:id="@+id/setupUrlStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textSize="13sp"
android:visibility="gone"
android:lineSpacingExtra="2dp"
android:layout_marginBottom="12dp" />
<!-- Tip -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="💡 Suggerimento: copia l'URL esatto dal browser (es. http://192.168.1.50/dispensa/ ) incluso il percorso finale."
android:textColor="#64748b"
android:textSize="12sp"
android:background="@drawable/tip_background"
android:padding="12dp"
android:lineSpacingExtra="2dp"
android:layout_marginBottom="28dp" />
<!-- Navigation -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnServerBack"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="← Indietro"
android:textSize="14sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#334155"
android:textColor="#64748b"
android:layout_marginEnd="10dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnServerNext"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="2"
android:text="Avanti →"
android:textSize="15sp"
android:textAllCaps="false"
android:backgroundTint="#7c3aed" />
</LinearLayout>
</LinearLayout>
<!-- ════════════════════════════════════════════
STEP 3 — Smart scale
════════════════════════════════════════════ -->
<LinearLayout
android:id="@+id/stepScale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="⚖️"
android:textSize="52sp"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bilancia smart"
android:textColor="#f1f5f9"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="EverShelf può integrarsi con le bilance da cucina Bluetooth per pesare automaticamente gli ingredienti."
android:textColor="#94a3b8"
android:textSize="15sp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="24dp" />
<!-- Question card -->
<LinearLayout
android:id="@+id/scaleQuestionCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_background"
android:padding="20dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hai una bilancia Bluetooth compatibile?"
android:textColor="#f1f5f9"
android:textSize="17sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="20dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnScaleYes"
android:layout_width="match_parent"
android:layout_height="52dp"
android:text="✓ Sì, ho una bilancia"
android:textSize="15sp"
android:textAllCaps="false"
android:backgroundTint="#059669"
android:layout_marginBottom="10dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnScaleNo"
android:layout_width="match_parent"
android:layout_height="52dp"
android:text="→ No, salta questo passaggio"
android:textSize="15sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#334155"
android:textColor="#64748b" />
</LinearLayout>
<!-- Gateway info card (shown after YES) -->
<LinearLayout
android:id="@+id/gatewayInfoCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/tip_background"
android:padding="16dp"
android:layout_marginBottom="12dp"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📱 EverShelf Scale Gateway"
android:textColor="#f1f5f9"
android:textSize="15sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Un'app separata che fa da ponte: legge i dati della bilancia Bluetooth e li trasmette al pannello kiosk via rete locale. Rimane in esecuzione in background e si avvia automaticamente insieme a EverShelf."
android:textColor="#94a3b8"
android:textSize="13sp"
android:lineSpacingExtra="3dp" />
</LinearLayout>
<!-- Gateway install status card (shown after YES) -->
<LinearLayout
android:id="@+id/gatewayInstallCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_background"
android:padding="20dp"
android:layout_marginBottom="16dp"
android:visibility="gone">
<TextView
android:id="@+id/gatewayStatusIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📲"
android:textSize="32sp"
android:layout_gravity="center"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/gatewayStatusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Scale Gateway non installato"
android:textColor="#cbd5e1"
android:textSize="16sp"
android:gravity="center"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/gatewayStatusDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textColor="#94a3b8"
android:textSize="13sp"
android:gravity="center"
android:layout_marginBottom="14dp" />
<ProgressBar
android:id="@+id/gatewayProgressBar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_marginBottom="4dp"
android:progressTint="#7c3aed"
android:progressBackgroundTint="#334155"
android:max="100"
android:progress="0"
android:indeterminate="false"
android:visibility="gone" />
<TextView
android:id="@+id/gatewayProgressText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textColor="#94a3b8"
android:textSize="12sp"
android:gravity="center"
android:layout_marginBottom="10dp"
android:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnInstallGateway"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="📥 Installa Scale Gateway"
android:textSize="14sp"
android:textAllCaps="false"
android:backgroundTint="#7c3aed" />
</LinearLayout>
<!-- Step 3 navigation (shown after YES) -->
<LinearLayout
android:id="@+id/step3NextButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnScaleBack"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="← Indietro"
android:textSize="14sp"
android:textAllCaps="false"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:strokeColor="#334155"
android:textColor="#64748b"
android:layout_marginEnd="10dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnScaleNext"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="2"
android:text="Avanti →"
android:textSize="15sp"
android:textAllCaps="false"
android:backgroundTint="#7c3aed" />
</LinearLayout>
</LinearLayout>
<!-- ════════════════════════════════════════════
STEP 4 — Done
════════════════════════════════════════════ -->
<LinearLayout
android:id="@+id/stepDone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🎉"
android:textSize="64sp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tutto pronto!"
android:textColor="#f1f5f9"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="La configurazione è completa. Premi il pulsante per avviare EverShelf in modalità kiosk."
android:textColor="#94a3b8"
android:textSize="15sp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="28dp" />
<!-- Configuration summary -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_background"
android:padding="20dp"
android:layout_marginBottom="32dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Riepilogo configurazione"
android:textColor="#94a3b8"
android:textSize="13sp"
android:textAllCaps="true"
android:letterSpacing="0.08"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/setupSummaryText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textColor="#cbd5e1"
android:textSize="14sp"
android:lineSpacingExtra="4dp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnLaunch"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="🚀 Avvia EverShelf"
android:textSize="18sp"
android:textAllCaps="false"
android:backgroundTint="#059669" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
@@ -2,6 +2,21 @@
<resources>
<string name="app_name">EverShelf Kiosk</string>
<!-- Setup-Assistent Zeichenfolgen -->
<string name="setup_enter_url">Bitte zuerst eine URL eingeben</string>
<string name="setup_testing">Verbindung wird getestet…</string>
<string name="setup_server_found">EverShelf-Server gefunden und API aktiv!</string>
<string name="setup_api_not_found">Server erreichbar, aber EverShelf-API nicht gefunden. Pfad prüfen.</string>
<string name="setup_unreachable">Server nicht erreichbar</string>
<string name="setup_discover_btn">🔍 Lokales Netzwerk durchsuchen</string>
<string name="setup_discovering">Suche läuft…</string>
<string name="setup_discovering_detail">Suche nach EverShelf-Servern im lokalen Netzwerk (kann bis zu 30 s dauern)…</string>
<string name="setup_discover_not_found">Kein EverShelf-Server automatisch gefunden. URL manuell eingeben.</string>
<string name="setup_exit_title">Setup beenden?</string>
<string name="setup_exit_message">Die Einrichtung kann später beim erneuten Öffnen der App abgeschlossen werden.</string>
<string name="setup_exit_confirm">Beenden</string>
<string name="setup_exit_cancel">Weiter</string>
<!-- Wizard Schritt 3: Smart-Waage -->
<string name="wizard_step3_title">Smart-Waage (Optional)</string>
<string name="wizard_step3_description">Um eine Bluetooth-Küchenwaage zu verwenden, musst du die EverShelf Scale Gateway App separat installieren.</string>
@@ -2,6 +2,21 @@
<resources>
<string name="app_name">EverShelf Kiosk</string>
<!-- Stringhe setup wizard -->
<string name="setup_enter_url">Inserisci prima un URL</string>
<string name="setup_testing">Verifica connessione…</string>
<string name="setup_server_found">Server EverShelf trovato e API attiva!</string>
<string name="setup_api_not_found">Server raggiungibile ma API EverShelf non trovata. Verifica il percorso.</string>
<string name="setup_unreachable">Impossibile raggiungere il server</string>
<string name="setup_discover_btn">🔍 Cerca nella rete locale</string>
<string name="setup_discovering">Scansione in corso…</string>
<string name="setup_discovering_detail">Ricerca server EverShelf nella rete locale (potrebbe richiedere fino a 30 s)…</string>
<string name="setup_discover_not_found">Nessun server EverShelf trovato automaticamente. Inserisci l\'URL manualmente.</string>
<string name="setup_exit_title">Uscire dalla configurazione?</string>
<string name="setup_exit_message">Puoi completare la configurazione più tardi riaprendo l\'app.</string>
<string name="setup_exit_confirm">Esci</string>
<string name="setup_exit_cancel">Continua</string>
<!-- Wizard Step 3: Bilancia smart -->
<string name="wizard_step3_title">Bilancia Smart (Opzionale)</string>
<string name="wizard_step3_description">Per usare una bilancia da cucina Bluetooth, devi installare l\'app EverShelf Scale Gateway separatamente.</string>
@@ -1,6 +1,21 @@
<resources>
<string name="app_name">EverShelf Kiosk</string>
<!-- Setup wizard strings -->
<string name="setup_enter_url">Please enter a URL first</string>
<string name="setup_testing">Testing connection…</string>
<string name="setup_server_found">EverShelf server found and API active!</string>
<string name="setup_api_not_found">Server reachable but EverShelf API not found. Check the path.</string>
<string name="setup_unreachable">Cannot reach server</string>
<string name="setup_discover_btn">🔍 Search local network</string>
<string name="setup_discovering">Scanning…</string>
<string name="setup_discovering_detail">Scanning local network for EverShelf servers (this may take up to 30 s)…</string>
<string name="setup_discover_not_found">No EverShelf server found automatically. Enter the URL manually.</string>
<string name="setup_exit_title">Exit setup?</string>
<string name="setup_exit_message">You can complete setup later when you reopen the app.</string>
<string name="setup_exit_confirm">Exit</string>
<string name="setup_exit_cancel">Continue</string>
<!-- Wizard Step 3: Smart scale -->
<string name="wizard_step3_title">Smart Scale (Optional)</string>
<string name="wizard_step3_description">To use a Bluetooth kitchen scale, you need the EverShelf Scale Gateway app installed separately.</string>