fix(kiosk): connection test, gateway detection, splash, triple-tap exit

- Fix 404: test base URL directly instead of appending /api/
- Fix gateway not detected: add <queries> block for Android 11+ package visibility
- Add splash screen with icon + loading spinner (1.5s)
- Add triple-tap on wizard title to exit kiosk mode
- Accept 3xx redirects as valid responses
- New house/shelf vector icon for launcher
- Replace emoji icon with drawable on welcome screen
- Version 1.1.0
This commit is contained in:
dadaloop82
2026-04-16 17:04:25 +00:00
parent 9363bc147e
commit 5991e666ec
5 changed files with 182 additions and 131 deletions
@@ -9,6 +9,11 @@
<!-- Keep screen on -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Query gateway app visibility (required Android 11+) -->
<queries>
<package android:name="it.dadaloop.evershelf.scalegate" />
</queries>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
@@ -10,6 +10,8 @@ import android.net.Uri
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
@@ -28,6 +30,7 @@ import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton
import java.net.URL
@@ -42,6 +45,7 @@ class KioskActivity : AppCompatActivity() {
private var currentStep = 1
// Views
private lateinit var splashContainer: LinearLayout
private lateinit var wizardContainer: ScrollView
private lateinit var webView: WebView
private lateinit var btnSettings: ImageButton
@@ -55,6 +59,11 @@ class KioskActivity : AppCompatActivity() {
private lateinit var scaleStatusText: TextView
private lateinit var scaleStatusDetail: TextView
// Triple-tap to exit
private var tapCount = 0
private val tapHandler = Handler(Looper.getMainLooper())
private val tapResetRunnable = Runnable { tapCount = 0 }
// File chooser
private var fileChooserCallback: ValueCallback<Array<Uri>>? = null
@@ -65,6 +74,7 @@ class KioskActivity : AppCompatActivity() {
private const val KEY_SETUP_COMPLETE = "setup_complete"
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 SPLASH_DURATION = 1500L
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -75,14 +85,19 @@ class KioskActivity : AppCompatActivity() {
bindViews()
enterImmersiveMode()
// Show splash then proceed
Handler(Looper.getMainLooper()).postDelayed({
splashContainer.visibility = View.GONE
if (prefs.getBoolean(KEY_SETUP_COMPLETE, false)) {
launchWebView()
} else {
showWizard()
}
}, SPLASH_DURATION)
}
private fun bindViews() {
splashContainer = findViewById(R.id.splashContainer)
wizardContainer = findViewById(R.id.wizardContainer)
webView = findViewById(R.id.webView)
btnSettings = findViewById(R.id.btnSettings)
@@ -96,12 +111,17 @@ class KioskActivity : AppCompatActivity() {
scaleStatusText = findViewById(R.id.scaleStatusText)
scaleStatusDetail = findViewById(R.id.scaleStatusDetail)
// Step 1 buttons
// Triple-tap on wizard title to exit kiosk
findViewById<TextView>(R.id.wizardTitle).setOnClickListener {
handleTripleTap()
}
// Step 1
findViewById<MaterialButton>(R.id.btnGetStarted).setOnClickListener {
goToStep(2)
}
// Step 2 buttons
// Step 2
findViewById<MaterialButton>(R.id.btnTestUrl).setOnClickListener {
testConnection()
}
@@ -118,7 +138,7 @@ class KioskActivity : AppCompatActivity() {
goToStep(3)
}
// Step 3 buttons
// Step 3
findViewById<MaterialButton>(R.id.btnStep3Back).setOnClickListener {
goToStep(2)
}
@@ -130,18 +150,38 @@ class KioskActivity : AppCompatActivity() {
finishWizard()
}
// Settings button
// Settings
btnSettings.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}
// Pre-fill URL if saved
// Triple-tap on settings gear to exit
btnSettings.setOnLongClickListener {
handleTripleTap()
true
}
// Pre-fill URL
val savedUrl = prefs.getString(KEY_URL, "") ?: ""
if (savedUrl.isNotEmpty()) {
wizardUrl.setText(savedUrl)
}
}
// ── Triple-tap to exit ────────────────────────────────────────────────
private fun handleTripleTap() {
tapCount++
tapHandler.removeCallbacks(tapResetRunnable)
tapHandler.postDelayed(tapResetRunnable, 800)
if (tapCount >= 3) {
tapCount = 0
Toast.makeText(this, "Exiting kiosk mode...", Toast.LENGTH_SHORT).show()
finishAffinity()
}
}
// ── Wizard Flow ───────────────────────────────────────────────────────
private fun showWizard() {
@@ -176,12 +216,10 @@ class KioskActivity : AppCompatActivity() {
val bg = GradientDrawable()
bg.shape = GradientDrawable.OVAL
if (i == currentStep) {
bg.setColor(0xFF7c3aed.toInt())
} else if (i < currentStep) {
bg.setColor(0xFF34d399.toInt())
} else {
bg.setColor(0xFF334155.toInt())
when {
i == currentStep -> bg.setColor(0xFF7c3aed.toInt())
i < currentStep -> bg.setColor(0xFF34d399.toInt())
else -> bg.setColor(0xFF334155.toInt())
}
dot.background = bg
stepIndicator.addView(dot)
@@ -229,17 +267,15 @@ class KioskActivity : AppCompatActivity() {
scaleStatusText.text = "Scale Gateway is installed"
scaleStatusDetail.text = "It will be launched automatically when you finish setup"
scaleStatusDetail.setTextColor(0xFF34d399.toInt())
// Hide skip, show finish prominently
findViewById<MaterialButton>(R.id.btnSkipScale).visibility = View.GONE
findViewById<MaterialButton>(R.id.btnFinish).text = "🚀 Launch EverShelf"
} else {
scaleStatusIcon.text = "📥"
scaleStatusText.text = "Scale Gateway not installed"
scaleStatusDetail.text = "You need the EverShelf Scale Gateway app to use a Bluetooth scale"
scaleStatusDetail.setTextColor(0xFFfbbf24.toInt())
// Show download button in the card
val downloadBtn = findViewById<MaterialButton>(R.id.btnFinish)
downloadBtn.text = "🚀 Launch EverShelf (without scale)"
findViewById<MaterialButton>(R.id.btnFinish).text = "🚀 Launch without scale"
findViewById<MaterialButton>(R.id.btnSkipScale).apply {
text = "📥 Download Scale Gateway"
@@ -265,10 +301,9 @@ class KioskActivity : AppCompatActivity() {
Thread {
try {
val testUrl = if (url.endsWith("/")) "${url}api/" else "${url}/api/"
val conn = URL(testUrl).openConnection()
// Test the base URL directly (not /api/)
val conn = URL(url).openConnection()
// Trust all certs for local/self-signed servers
if (conn is HttpsURLConnection) {
val trustAll = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<java.security.cert.X509Certificate>?, authType: String?) {}
@@ -288,7 +323,7 @@ class KioskActivity : AppCompatActivity() {
val code = conn.responseCode
conn.disconnect()
runOnUiThread {
if (code in 200..299) {
if (code in 200..399) {
showUrlStatus("✓ Connected successfully!", true)
} else {
showUrlStatus("⚠ Server responded with code $code", false)
@@ -332,7 +367,6 @@ class KioskActivity : AppCompatActivity() {
override fun onReceivedSslError(
view: WebView?, handler: SslErrorHandler?, error: SslError?
) {
// Accept self-signed certs for local network servers
handler?.proceed()
}
@@ -369,7 +403,7 @@ class KioskActivity : AppCompatActivity() {
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
webView.loadUrl(url)
// Launch gateway app if installed (handles scale in background)
// Launch gateway if installed
launchGatewayIfInstalled()
// Keep screen on
@@ -433,10 +467,11 @@ class KioskActivity : AppCompatActivity() {
webView.loadUrl(url)
}
}
if (!prefs.getBoolean(KEY_SETUP_COMPLETE, false) && wizardContainer.visibility != View.VISIBLE) {
if (!prefs.getBoolean(KEY_SETUP_COMPLETE, false) &&
wizardContainer.visibility != View.VISIBLE &&
splashContainer.visibility != View.VISIBLE) {
showWizard()
}
// Re-check gateway status if on step 3
if (currentStep == 3 && wizardContainer.visibility == View.VISIBLE) {
checkGatewayStatus()
}
@@ -457,6 +492,5 @@ class KioskActivity : AppCompatActivity() {
if (webView.visibility == View.VISIBLE && webView.canGoBack()) {
webView.goBack()
}
// Block back button in kiosk mode
}
}
@@ -92,8 +92,7 @@ class SettingsActivity : AppCompatActivity() {
Thread {
try {
val testUrl = if (url.endsWith("/")) "${url}api/" else "${url}/api/"
val conn = URL(testUrl).openConnection()
val conn = URL(url).openConnection()
if (conn is HttpsURLConnection) {
val trustAll = arrayOf<TrustManager>(object : X509TrustManager {
@@ -114,7 +113,7 @@ class SettingsActivity : AppCompatActivity() {
val code = conn.responseCode
conn.disconnect()
runOnUiThread {
if (code in 200..299) {
if (code in 200..399) {
Toast.makeText(this, "✓ Connection successful!", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "⚠ Server responded: $code", Toast.LENGTH_SHORT).show()
@@ -1,70 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Shelf icon: stylized pantry/shelf -->
<!-- Outer frame -->
<!-- House outline -->
<path
android:fillColor="#FFFFFF"
android:pathData="M30,28 L78,28 L78,80 L30,80 Z"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"/>
<!-- Shelf lines -->
android:pathData="M54,24 L26,46 L26,80 L82,80 L82,46 Z" />
<path
android:fillColor="#10B981"
android:pathData="M34,28 L34,80 M74,28 L74,80"/>
android:fillColor="#059669"
android:pathData="M54,28 L30,48 L30,76 L78,76 L78,48 Z" />
<!-- Door -->
<path
android:fillColor="#065F46"
android:pathData="M48,76 L48,58 L60,58 L60,76 Z" />
<!-- Shelves inside -->
<path
android:fillColor="#34D399"
android:pathData="M34,52 L46,52 L46,54 L34,54 Z" />
<path
android:fillColor="#34D399"
android:pathData="M62,52 L74,52 L74,54 L62,54 Z" />
<path
android:fillColor="#34D399"
android:pathData="M34,60 L46,60 L46,62 L34,62 Z" />
<path
android:fillColor="#34D399"
android:pathData="M62,60 L74,60 L74,62 L62,62 Z" />
<!-- Items on shelves (small boxes/jars) -->
<path
android:fillColor="#A7F3D0"
android:pathData="M36,48 L40,48 L40,52 L36,52 Z" />
<path
android:fillColor="#6EE7B7"
android:pathData="M42,49 L45,49 L45,52 L42,52 Z" />
<path
android:fillColor="#A7F3D0"
android:pathData="M64,48 L68,48 L68,52 L64,52 Z" />
<path
android:fillColor="#6EE7B7"
android:pathData="M70,49 L73,49 L73,52 L70,52 Z" />
<path
android:fillColor="#A7F3D0"
android:pathData="M36,56 L40,56 L40,60 L36,60 Z" />
<path
android:fillColor="#6EE7B7"
android:pathData="M42,57 L45,57 L45,60 L42,60 Z" />
<path
android:fillColor="#A7F3D0"
android:pathData="M64,56 L68,56 L68,60 L64,60 Z" />
<path
android:fillColor="#6EE7B7"
android:pathData="M70,57 L73,57 L73,60 L70,60 Z" />
<!-- Chimney -->
<path
android:fillColor="#FFFFFF"
android:pathData="M32,28 L76,28 L76,80 L32,80 Z"
android:strokeColor="#FFFFFF"
android:strokeWidth="3"
android:fillAlpha="0"/>
<!-- Top shelf -->
android:pathData="M68,30 L74,30 L74,42 L68,42 Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M32,45 L76,45"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"/>
<!-- Bottom shelf -->
<path
android:fillColor="#FFFFFF"
android:pathData="M32,62 L76,62"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"/>
<!-- Items on top shelf: jar -->
<path
android:fillColor="#FFFFFF"
android:fillAlpha="0.9"
android:pathData="M40,33 L48,33 L48,44 L40,44 Z"/>
<!-- Items on top shelf: bottle -->
<path
android:fillColor="#FFFFFF"
android:fillAlpha="0.9"
android:pathData="M55,30 L59,30 L59,33 L61,33 L61,44 L53,44 L53,33 L55,33 Z"/>
<!-- Items on middle shelf: box -->
<path
android:fillColor="#FFFFFF"
android:fillAlpha="0.9"
android:pathData="M38,50 L50,50 L50,61 L38,61 Z"/>
<!-- Items on middle shelf: can -->
<path
android:fillColor="#FFFFFF"
android:fillAlpha="0.9"
android:pathData="M58,52 Q58,49 62,49 Q66,49 66,52 L66,61 L58,61 Z"/>
<!-- Items on bottom shelf: bag -->
<path
android:fillColor="#FFFFFF"
android:fillAlpha="0.9"
android:pathData="M42,66 L54,66 L56,79 L40,79 Z"/>
<!-- Scale icon (small, bottom right) -->
<path
android:fillColor="#FFFFFF"
android:fillAlpha="0.8"
android:pathData="M64,70 L72,70 L72,78 L64,78 Z"/>
<path
android:fillColor="#10B981"
android:pathData="M65,73 L71,73"
android:strokeColor="#10B981"
android:strokeWidth="1"/>
android:fillColor="#059669"
android:pathData="M69,31 L73,31 L73,42 L69,42 Z" />
</vector>
@@ -4,7 +4,46 @@
android:layout_height="match_parent"
android:background="#0f172a">
<!-- Setup wizard container (shown on first launch) -->
<!-- Splash screen (shown briefly on launch) -->
<LinearLayout
android:id="@+id/splashContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:visibility="visible">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/ic_launcher_foreground"
android:layout_marginBottom="24dp"
android:contentDescription="EverShelf" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="EverShelf"
android:textColor="#f1f5f9"
android:textSize="32sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Smart Pantry Manager"
android:textColor="#64748b"
android:textSize="16sp"
android:layout_marginBottom="48dp" />
<ProgressBar
android:layout_width="32dp"
android:layout_height="32dp"
android:indeterminateTint="#7c3aed" />
</LinearLayout>
<!-- Setup wizard container -->
<ScrollView
android:id="@+id/wizardContainer"
android:layout_width="match_parent"
@@ -40,14 +79,15 @@
android:gravity="center_horizontal"
android:visibility="visible">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🏠"
android:textSize="64sp"
android:layout_marginBottom="16dp" />
<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"
@@ -87,7 +127,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚖️ Built-in BLE scale gateway"
android:text="⚖️ Bluetooth scale via Gateway app"
android:textColor="#cbd5e1"
android:textSize="15sp"
android:paddingTop="8dp"
@@ -150,7 +190,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enter the URL of your EverShelf server. This is the address you use to access EverShelf from a browser."
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"
@@ -170,7 +210,7 @@
android:id="@+id/wizardUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="https://192.168.1.100/dispensa"
android:hint="https://192.168.1.100/dispensa/"
android:inputType="textUri"
android:textColor="#f1f5f9"
android:textColorHint="#475569"
@@ -204,7 +244,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="💡 Tip: You can find this URL in your browser's address bar when EverShelf is open."
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"
@@ -271,7 +311,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This app includes a built-in BLE scale gateway. If you have a Bluetooth kitchen scale, it will be detected automatically."
android:text="To use a Bluetooth kitchen scale, you need the EverShelf Scale Gateway app installed separately."
android:textColor="#94a3b8"
android:textSize="15sp"
android:gravity="center"
@@ -301,7 +341,7 @@
android:id="@+id/scaleStatusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Searching for scales..."
android:text="Checking..."
android:textColor="#cbd5e1"
android:textSize="16sp"
android:gravity="center"
@@ -311,43 +351,18 @@
android:id="@+id/scaleStatusDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Turn on your scale and place it nearby"
android:text=""
android:textColor="#64748b"
android:textSize="13sp"
android:gravity="center" />
</LinearLayout>
<!-- Gateway info -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/tip_background"
android:padding="14dp"
android:layout_marginBottom="32dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="️ Gateway runs on ws://localhost:8765"
android:textColor="#94a3b8"
android:textSize="13sp"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="EverShelf will auto-configure the scale connection when running in kiosk mode."
android:textColor="#64748b"
android:textSize="12sp"
android:lineSpacingExtra="2dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
android:gravity="center"
android:layout_marginTop="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStep3Back"