diff --git a/evershelf-kiosk/app/build.gradle.kts b/evershelf-kiosk/app/build.gradle.kts
index 10d007f..e771eb0 100644
--- a/evershelf-kiosk/app/build.gradle.kts
+++ b/evershelf-kiosk/app/build.gradle.kts
@@ -11,8 +11,8 @@ android {
applicationId = "it.dadaloop.evershelf.kiosk"
minSdk = 24
targetSdk = 34
- versionCode = 1
- versionName = "1.0.0"
+ versionCode = 2
+ versionName = "1.1.0"
}
buildTypes {
@@ -41,8 +41,4 @@ dependencies {
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.webkit:webkit:1.10.0")
- // WebSocket server (for scale gateway)
- implementation("org.java-websocket:Java-WebSocket:1.5.5")
- // Coroutines
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
diff --git a/evershelf-kiosk/app/src/main/AndroidManifest.xml b/evershelf-kiosk/app/src/main/AndroidManifest.xml
index 85e6f44..ab89e1d 100644
--- a/evershelf-kiosk/app/src/main/AndroidManifest.xml
+++ b/evershelf-kiosk/app/src/main/AndroidManifest.xml
@@ -1,33 +1,13 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/BleScaleManager.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/BleScaleManager.kt
deleted file mode 100644
index 6bc4967..0000000
--- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/BleScaleManager.kt
+++ /dev/null
@@ -1,320 +0,0 @@
-package it.dadaloop.evershelf.kiosk
-
-import android.Manifest
-import android.bluetooth.*
-import android.bluetooth.le.*
-import android.content.Context
-import android.content.pm.PackageManager
-import android.os.Build
-import android.os.Handler
-import android.os.Looper
-import android.util.Log
-import androidx.core.content.ContextCompat
-
-private const val TAG = "BleScaleManager"
-private const val SCAN_PERIOD_MS = 15_000L
-private const val PREFS_NAME = "evershelf_kiosk"
-private const val PREF_LAST_DEVICE = "last_device_address"
-
-data class BleDeviceInfo(
- val device: BluetoothDevice,
- val name: String,
- val rssi: Int,
- val proximity: String,
- val scaleScore: Int,
-)
-
-interface BleScaleListener {
- fun onDeviceFound(info: BleDeviceInfo)
- fun onConnecting(device: BluetoothDevice)
- fun onConnected(deviceName: String)
- fun onDisconnected()
- fun onWeightReceived(reading: WeightReading)
- fun onBatteryReceived(level: Int)
- fun onError(message: String)
- fun onScanStopped()
- fun onDebugEvent(message: String)
-}
-
-class BleScaleManager(
- private val context: Context,
- private val listener: BleScaleListener,
-) {
- private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
- private val bluetoothAdapter: BluetoothAdapter? get() = bluetoothManager.adapter
- private val mainHandler = Handler(Looper.getMainLooper())
-
- private var leScanner: BluetoothLeScanner? = null
- private var gatt: BluetoothGatt? = null
- private var isScanning = false
- private var connectedDeviceName: String = ""
- private var autoConnectAddress: String? = null
- private val pendingSubscriptions = ArrayDeque()
-
- val isConnected: Boolean get() = gatt != null && connectedDeviceName.isNotEmpty()
-
- fun getSavedDeviceAddress(): String? {
- return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
- .getString(PREF_LAST_DEVICE, null)
- }
-
- private fun saveDeviceAddress(address: String) {
- context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
- .edit().putString(PREF_LAST_DEVICE, address).apply()
- }
-
- fun enableAutoConnect() {
- autoConnectAddress = getSavedDeviceAddress()
- }
-
- fun hasRequiredPermissions(): Boolean {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
- ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
- } else {
- ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
- }
- }
-
- fun startScan() {
- val adapter = bluetoothAdapter ?: run {
- listener.onError("Bluetooth not available.")
- return
- }
- if (!adapter.isEnabled) {
- listener.onError("Bluetooth is off.")
- return
- }
- if (isScanning) stopScan()
-
- leScanner = adapter.bluetoothLeScanner
- val settings = ScanSettings.Builder()
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
- .build()
-
- isScanning = true
- try {
- leScanner?.startScan(null, settings, scanCallback)
- } catch (e: Exception) {
- leScanner?.startScan(scanCallback)
- }
-
- mainHandler.postDelayed({
- stopScan()
- listener.onScanStopped()
- }, SCAN_PERIOD_MS)
- }
-
- fun stopScan() {
- if (!isScanning) return
- isScanning = false
- try { leScanner?.stopScan(scanCallback) } catch (_: Exception) {}
- leScanner = null
- }
-
- private val scanCallback = object : ScanCallback() {
- override fun onScanResult(callbackType: Int, result: ScanResult) {
- val device = result.device
- val name = result.scanRecord?.deviceName?.takeIf { it.isNotBlank() }
- ?: getDeviceName(device)
- val proximity = rssiToProximity(result.rssi)
- val score = scoreLikelyScale(name, result.scanRecord)
- val info = BleDeviceInfo(device, name, result.rssi, proximity, score)
- mainHandler.post { listener.onDeviceFound(info) }
-
- if (autoConnectAddress != null && device.address == autoConnectAddress && !isConnected) {
- autoConnectAddress = null
- mainHandler.post { connect(device) }
- }
- }
-
- override fun onScanFailed(errorCode: Int) {
- isScanning = false
- mainHandler.post { listener.onError("BLE scan failed (code: $errorCode)") }
- }
- }
-
- private fun getDeviceName(device: BluetoothDevice): String {
- return try { device.name?.takeIf { it.isNotBlank() } ?: "Unnamed" } catch (_: SecurityException) { "Unnamed" }
- }
-
- private fun rssiToProximity(rssi: Int) = when {
- rssi >= -60 -> "Near"; rssi >= -80 -> "Medium"; else -> "Far"
- }
-
- private fun scoreLikelyScale(name: String, scanRecord: android.bluetooth.le.ScanRecord?): Int {
- var score = 0
- val lower = name.lowercase()
- val foodKeywords = listOf("scale", "bilancia", "kitchen", "food", "cucina", "coffee", "caffe",
- "balance", "weight", "waage", "arboleaf", "ck10", "ck20", "ek-", "acaia", "felicita",
- "decent", "skale", "timemore", "brewista", "hario", "greater goods", "ozeri", "etekcity",
- "nutri", "nicewell", "koios", "renpho", "eatsmart")
- if (foodKeywords.any { lower.contains(it) }) score += 10
- val bodyKeywords = listOf("body", "fat", "bmi", "composition", "fitness", "mi body", "lepulse", "qardio", "garmin", "withings")
- if (bodyKeywords.any { lower.contains(it) }) score -= 5
- scanRecord?.serviceUuids?.let { uuids ->
- val us = uuids.map { it.uuid.toString().lowercase() }
- if (us.any { it.startsWith("0000181d") }) score += 15
- if (us.any { it.startsWith("0000ffe0") || it.startsWith("0000fff0") }) score += 10
- if (us.any { it.startsWith("49535343") }) score += 20
- if (us.any { it.startsWith("0000181b") }) score -= 10
- }
- return score
- }
-
- fun connect(device: BluetoothDevice) {
- stopScan()
- disconnect()
- connectedDeviceName = ""
- ScaleProtocol.resetState()
- mainHandler.post { listener.onConnecting(device) }
- try {
- gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
- } else {
- device.connectGatt(context, false, gattCallback)
- }
- } catch (e: SecurityException) {
- mainHandler.post { listener.onError("Missing permission: ${e.message}") }
- }
- }
-
- fun disconnect() {
- pendingSubscriptions.clear()
- try { gatt?.disconnect(); gatt?.close() } catch (_: Exception) {}
- gatt = null
- connectedDeviceName = ""
- }
-
- private val gattCallback = object : BluetoothGattCallback() {
- override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
- when (newState) {
- BluetoothProfile.STATE_CONNECTED -> {
- mainHandler.postDelayed({ gatt.discoverServices() }, 500)
- }
- BluetoothProfile.STATE_DISCONNECTED -> {
- this@BleScaleManager.gatt?.close()
- this@BleScaleManager.gatt = null
- connectedDeviceName = ""
- mainHandler.post { listener.onDisconnected() }
- }
- }
- }
-
- override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
- if (status != BluetoothGatt.GATT_SUCCESS) {
- mainHandler.post { listener.onError("GATT services not found (status=$status)") }
- return
- }
-
- val targetChars = mutableListOf()
-
- gatt.getService(BleUuids.WEIGHT_SCALE_SERVICE)
- ?.getCharacteristic(BleUuids.WEIGHT_MEASUREMENT_CHAR)?.let { targetChars.add(it) }
- gatt.getService(BleUuids.FFE0)?.let { svc ->
- svc.getCharacteristic(BleUuids.FFE1)?.let { targetChars.add(it) }
- }
- gatt.getService(BleUuids.FFF0)?.let { svc ->
- svc.getCharacteristic(BleUuids.FFF4)?.let { targetChars.add(it) }
- ?: svc.getCharacteristic(BleUuids.FFF1)?.let { targetChars.add(it) }
- }
- gatt.getService(BleUuids.ACAIA_SERVICE)?.let { svc ->
- svc.getCharacteristic(BleUuids.ACAIA_CHAR)?.let { targetChars.add(it) }
- }
-
- if (targetChars.isEmpty()) {
- for (service in gatt.services) {
- if (service.uuid.toString().startsWith("00001800") ||
- service.uuid.toString().startsWith("00001801")) continue
- for (char in service.characteristics) {
- val props = char.properties
- if ((props and BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 ||
- (props and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
- if (!targetChars.contains(char)) targetChars.add(char)
- }
- }
- }
- }
-
- if (targetChars.isEmpty()) {
- mainHandler.post { listener.onError("No weight characteristic found.") }
- return
- }
-
- gatt.getService(BleUuids.BATTERY_SERVICE)
- ?.getCharacteristic(BleUuids.BATTERY_LEVEL_CHAR)?.let { targetChars.add(it) }
-
- try { gatt.device?.address?.let { saveDeviceAddress(it) } } catch (_: SecurityException) {}
-
- pendingSubscriptions.clear()
- pendingSubscriptions.addAll(targetChars)
-
- val deviceName = try { gatt.device?.name ?: "Scale" } catch (_: SecurityException) { "Scale" }
- connectedDeviceName = deviceName
- mainHandler.post { listener.onConnected(deviceName) }
- subscribeNext(gatt)
- }
-
- override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
- subscribeNext(gatt)
- }
-
- @Suppress("DEPRECATION")
- override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
- val data = characteristic.value ?: return
- processCharacteristicData(characteristic, data)
- }
-
- override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
- processCharacteristicData(characteristic, value)
- }
-
- @Suppress("DEPRECATION")
- override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
- if (status == BluetoothGatt.GATT_SUCCESS && characteristic.uuid == BleUuids.BATTERY_LEVEL_CHAR) {
- val level = characteristic.value?.firstOrNull()?.toInt()?.and(0xFF)
- if (level != null) mainHandler.post { listener.onBatteryReceived(level) }
- }
- }
- }
-
- private fun subscribeNext(gatt: BluetoothGatt) {
- val char = pendingSubscriptions.removeFirstOrNull() ?: return
- if (char.uuid == BleUuids.BATTERY_LEVEL_CHAR) {
- try { gatt.readCharacteristic(char) } catch (_: SecurityException) {}
- return
- }
- val props = char.properties
- val notifyType = when {
- (props and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0 ->
- BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
- else -> BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
- }
- try {
- gatt.setCharacteristicNotification(char, true)
- val descriptor = char.getDescriptor(CCCD_UUID) ?: run { subscribeNext(gatt); return }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- gatt.writeDescriptor(descriptor, notifyType)
- } else {
- @Suppress("DEPRECATION")
- descriptor.value = notifyType
- @Suppress("DEPRECATION")
- gatt.writeDescriptor(descriptor)
- }
- } catch (e: SecurityException) {
- Log.e(TAG, "SecurityException enabling notification", e)
- }
- }
-
- private fun processCharacteristicData(char: BluetoothGattCharacteristic, data: ByteArray) {
- if (char.uuid == BleUuids.BATTERY_LEVEL_CHAR && data.isNotEmpty()) {
- val level = data[0].toInt() and 0xFF
- mainHandler.post { listener.onBatteryReceived(level) }
- return
- }
- val reading = ScaleProtocol.parse(char, data)
- if (reading != null && reading.value > 0f) {
- mainHandler.post { listener.onWeightReceived(reading) }
- }
- }
-}
diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/GatewayWebSocketServer.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/GatewayWebSocketServer.kt
deleted file mode 100644
index e158809..0000000
--- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/GatewayWebSocketServer.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package it.dadaloop.evershelf.kiosk
-
-import android.util.Log
-import org.java_websocket.WebSocket
-import org.java_websocket.handshake.ClientHandshake
-import org.java_websocket.server.WebSocketServer
-import org.json.JSONObject
-import java.net.InetSocketAddress
-import java.util.Collections
-
-private const val TAG = "GatewayWsServer"
-
-interface ServerEventListener {
- fun onClientConnected(address: String)
- fun onClientDisconnected(address: String)
- fun onClientRequestedWeight()
-}
-
-class GatewayWebSocketServer(
- port: Int,
- private val eventListener: ServerEventListener?,
-) : WebSocketServer(InetSocketAddress(port)) {
-
- private val pendingWeightRequests: MutableSet =
- Collections.synchronizedSet(mutableSetOf())
-
- @Volatile private var lastStatusJson: String = buildStatusJson("disconnected", null, null)
- @Volatile private var lastWeightJson: String? = null
-
- override fun onStart() {
- Log.i(TAG, "WebSocket server started on port ${address.port}")
- connectionLostTimeout = 30
- }
-
- override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
- val addr = conn.remoteSocketAddress?.toString() ?: "?"
- conn.send(lastStatusJson)
- lastWeightJson?.let { conn.send(it) }
- eventListener?.onClientConnected(addr)
- }
-
- override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) {
- val addr = conn.remoteSocketAddress?.toString() ?: "?"
- pendingWeightRequests.remove(conn)
- eventListener?.onClientDisconnected(addr)
- }
-
- override fun onMessage(conn: WebSocket, message: String) {
- try {
- val json = JSONObject(message)
- when (json.optString("type")) {
- "ping" -> conn.send("""{"type":"pong"}""")
- "get_status" -> conn.send(lastStatusJson)
- "get_weight" -> {
- pendingWeightRequests.add(conn)
- eventListener?.onClientRequestedWeight()
- lastWeightJson?.let { conn.send(it) }
- }
- }
- } catch (_: Exception) {}
- }
-
- override fun onError(conn: WebSocket?, ex: Exception) {
- Log.e(TAG, "WebSocket error", ex)
- }
-
- fun publishStatus(state: String, deviceName: String?, battery: Int?) {
- lastStatusJson = buildStatusJson(state, deviceName, battery)
- broadcast(lastStatusJson)
- }
-
- fun publishWeight(value: Float, unit: String, stable: Boolean, battery: Int? = null) {
- val json = buildWeightJson(value, unit, stable)
- lastWeightJson = json
- broadcast(json)
- if (stable) {
- synchronized(pendingWeightRequests) { pendingWeightRequests.clear() }
- }
- }
-
- private fun buildStatusJson(state: String, device: String?, battery: Int?): String {
- val obj = JSONObject()
- obj.put("type", "status")
- obj.put("state", state)
- if (device != null) obj.put("device", device)
- if (battery != null) obj.put("battery", battery)
- return obj.toString()
- }
-
- private fun buildWeightJson(value: Float, unit: String, stable: Boolean): String {
- val obj = JSONObject()
- obj.put("type", "weight")
- val rounded = Math.round(value * 10f) / 10.0
- obj.put("value", rounded)
- obj.put("unit", unit)
- obj.put("stable", stable)
- obj.put("timestamp", System.currentTimeMillis())
- return obj.toString()
- }
-}
diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt
index 61b4aeb..31f95f8 100644
--- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt
+++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt
@@ -1,24 +1,22 @@
package it.dadaloop.evershelf.kiosk
-import android.Manifest
import android.annotation.SuppressLint
-import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.ServiceConnection
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.drawable.GradientDrawable
import android.net.Uri
+import android.net.http.SslError
import android.os.Build
import android.os.Bundle
-import android.os.IBinder
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import android.webkit.ConsoleMessage
import android.webkit.PermissionRequest
+import android.webkit.SslErrorHandler
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
@@ -26,18 +24,17 @@ import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.EditText
-import android.widget.FrameLayout
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 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 javax.net.ssl.HttpsURLConnection
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
class KioskActivity : AppCompatActivity() {
@@ -58,34 +55,16 @@ class KioskActivity : AppCompatActivity() {
private lateinit var scaleStatusText: TextView
private lateinit var scaleStatusDetail: TextView
- // Scale service
- private var scaleService: ScaleGatewayService? = null
- private var serviceBound = false
- private val serviceConnection = object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
- val localBinder = binder as ScaleGatewayService.LocalBinder
- scaleService = localBinder.getService()
- serviceBound = true
- scaleService?.statusCallback = { status, device, battery ->
- runOnUiThread { updateScaleStatusUI(status, device, battery) }
- }
- }
- override fun onServiceDisconnected(name: ComponentName?) {
- scaleService = null
- serviceBound = false
- }
- }
-
// File chooser
private var fileChooserCallback: ValueCallback>? = null
companion object {
- private const val BLE_PERMISSION_REQUEST = 1001
private const val FILE_CHOOSER_REQUEST = 1002
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_LAST_DEVICE = "last_device_address"
+ 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"
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -136,7 +115,6 @@ class KioskActivity : AppCompatActivity() {
return@setOnClickListener
}
prefs.edit().putString(KEY_URL, url).apply()
- requestBlePermissions()
goToStep(3)
}
@@ -145,6 +123,7 @@ class KioskActivity : AppCompatActivity() {
goToStep(2)
}
findViewById(R.id.btnFinish).setOnClickListener {
+ launchGatewayIfInstalled()
finishWizard()
}
findViewById(R.id.btnSkipScale).setOnClickListener {
@@ -180,7 +159,7 @@ class KioskActivity : AppCompatActivity() {
updateStepIndicator()
if (step == 3) {
- startScaleGateway()
+ checkGatewayStatus()
}
}
@@ -223,6 +202,57 @@ class KioskActivity : AppCompatActivity() {
goToStep(1)
}
+ // ── Gateway Detection & Launch ────────────────────────────────────────
+
+ private fun isGatewayInstalled(): Boolean {
+ return try {
+ packageManager.getPackageInfo(GATEWAY_PACKAGE, 0)
+ true
+ } catch (e: PackageManager.NameNotFoundException) {
+ false
+ }
+ }
+
+ private fun launchGatewayIfInstalled() {
+ if (isGatewayInstalled()) {
+ val launchIntent = packageManager.getLaunchIntentForPackage(GATEWAY_PACKAGE)
+ if (launchIntent != null) {
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(launchIntent)
+ }
+ }
+ }
+
+ private fun checkGatewayStatus() {
+ if (isGatewayInstalled()) {
+ scaleStatusIcon.text = "✅"
+ 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(R.id.btnSkipScale).visibility = View.GONE
+ } 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(R.id.btnFinish)
+ downloadBtn.text = "🚀 Launch EverShelf (without scale)"
+
+ findViewById(R.id.btnSkipScale).apply {
+ text = "📥 Download Scale Gateway"
+ setTextColor(0xFF7c3aed.toInt())
+ visibility = View.VISIBLE
+ setOnClickListener {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(GATEWAY_DOWNLOAD_URL))
+ startActivity(intent)
+ }
+ }
+ }
+ }
+
// ── Connection Test ───────────────────────────────────────────────────
private fun testConnection() {
@@ -236,17 +266,33 @@ class KioskActivity : AppCompatActivity() {
Thread {
try {
val testUrl = if (url.endsWith("/")) "${url}api/" else "${url}/api/"
- val conn = URL(testUrl).openConnection() as HttpURLConnection
+ val conn = URL(testUrl).openConnection()
+
+ // Trust all certs for local/self-signed servers
+ if (conn is HttpsURLConnection) {
+ val trustAll = arrayOf(object : X509TrustManager {
+ override fun checkClientTrusted(chain: Array?, authType: String?) {}
+ override fun checkServerTrusted(chain: Array?, authType: String?) {}
+ override fun getAcceptedIssuers(): Array = 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.connectTimeout = 5000
conn.readTimeout = 5000
- conn.requestMethod = "GET"
- val code = conn.responseCode
- conn.disconnect()
- runOnUiThread {
- if (code in 200..299) {
- showUrlStatus("✓ Connected successfully!", true)
- } else {
- showUrlStatus("⚠ Server responded with code $code", false)
+ if (conn is java.net.HttpURLConnection) {
+ conn.requestMethod = "GET"
+ val code = conn.responseCode
+ conn.disconnect()
+ runOnUiThread {
+ if (code in 200..299) {
+ showUrlStatus("✓ Connected successfully!", true)
+ } else {
+ showUrlStatus("⚠ Server responded with code $code", false)
+ }
}
}
} catch (e: Exception) {
@@ -269,75 +315,6 @@ class KioskActivity : AppCompatActivity() {
)
}
- // ── Scale Gateway ─────────────────────────────────────────────────────
-
- private fun startScaleGateway() {
- val intent = Intent(this, ScaleGatewayService::class.java)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- startForegroundService(intent)
- } else {
- startService(intent)
- }
- bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
- }
-
- private fun updateScaleStatusUI(status: String, device: String?, battery: Int?) {
- when {
- status.contains("Connected", ignoreCase = true) -> {
- scaleStatusIcon.text = "✅"
- scaleStatusText.text = "Scale connected!"
- val detail = buildString {
- append(device ?: status)
- if (battery != null) append(" • Battery: $battery%")
- }
- scaleStatusDetail.text = detail
- scaleStatusDetail.setTextColor(0xFF34d399.toInt())
- }
- status.contains("Scanning", ignoreCase = true) ||
- status.contains("search", ignoreCase = true) -> {
- scaleStatusIcon.text = "🔍"
- scaleStatusText.text = "Scanning for scales..."
- scaleStatusDetail.text = "Turn on your scale and place it nearby"
- scaleStatusDetail.setTextColor(0xFF64748b.toInt())
- }
- status.contains("Ready", ignoreCase = true) ||
- status.contains("running", ignoreCase = true) -> {
- scaleStatusIcon.text = "📡"
- scaleStatusText.text = "Gateway is running"
- scaleStatusDetail.text = status
- scaleStatusDetail.setTextColor(0xFF94a3b8.toInt())
- }
- else -> {
- scaleStatusIcon.text = "📡"
- scaleStatusText.text = "Gateway active"
- scaleStatusDetail.text = status
- scaleStatusDetail.setTextColor(0xFF94a3b8.toInt())
- }
- }
- }
-
- // ── BLE Permissions ───────────────────────────────────────────────────
-
- private fun requestBlePermissions() {
- val perms = mutableListOf()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
- perms.add(Manifest.permission.BLUETOOTH_SCAN)
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED)
- perms.add(Manifest.permission.BLUETOOTH_CONNECT)
- } else {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)
- perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED)
- perms.add(Manifest.permission.POST_NOTIFICATIONS)
- }
- if (perms.isNotEmpty()) {
- ActivityCompat.requestPermissions(this, perms.toTypedArray(), BLE_PERMISSION_REQUEST)
- }
- }
-
// ── WebView ───────────────────────────────────────────────────────────
@SuppressLint("SetJavaScriptEnabled")
@@ -352,6 +329,13 @@ class KioskActivity : AppCompatActivity() {
settings.allowFileAccess = true
webView.webViewClient = object : WebViewClient() {
+ override fun onReceivedSslError(
+ view: WebView?, handler: SslErrorHandler?, error: SslError?
+ ) {
+ // Accept self-signed certs for local network servers
+ handler?.proceed()
+ }
+
override fun onReceivedError(
view: WebView?, request: WebResourceRequest?,
error: WebResourceError?
@@ -385,8 +369,8 @@ class KioskActivity : AppCompatActivity() {
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
webView.loadUrl(url)
- // Start scale gateway
- startScaleGateway()
+ // Launch gateway app if installed (handles scale in background)
+ launchGatewayIfInstalled()
// Keep screen on
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@@ -443,17 +427,19 @@ class KioskActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
enterImmersiveMode()
- // Reload WebView if setup is complete (in case URL changed in settings)
if (prefs.getBoolean(KEY_SETUP_COMPLETE, false) && webView.visibility == View.VISIBLE) {
val url = prefs.getString(KEY_URL, "") ?: ""
if (url.isNotEmpty() && webView.url != url) {
webView.loadUrl(url)
}
}
- // Check if wizard reset was requested
if (!prefs.getBoolean(KEY_SETUP_COMPLETE, false) && wizardContainer.visibility != View.VISIBLE) {
showWizard()
}
+ // Re-check gateway status if on step 3
+ if (currentStep == 3 && wizardContainer.visibility == View.VISIBLE) {
+ checkGatewayStatus()
+ }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -467,14 +453,6 @@ class KioskActivity : AppCompatActivity() {
}
}
- override fun onDestroy() {
- super.onDestroy()
- if (serviceBound) {
- unbindService(serviceConnection)
- serviceBound = false
- }
- }
-
override fun onBackPressed() {
if (webView.visibility == View.VISIBLE && webView.canGoBack()) {
webView.goBack()
diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ScaleGatewayService.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ScaleGatewayService.kt
deleted file mode 100644
index 9721337..0000000
--- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ScaleGatewayService.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-package it.dadaloop.evershelf.kiosk
-
-import android.app.*
-import android.bluetooth.BluetoothDevice
-import android.content.Context
-import android.content.Intent
-import android.os.Binder
-import android.os.Build
-import android.os.Handler
-import android.os.IBinder
-import android.os.Looper
-import android.util.Log
-import androidx.core.app.NotificationCompat
-
-private const val TAG = "ScaleGtwService"
-private const val CHANNEL_ID = "scale_gateway"
-private const val NOTIFICATION_ID = 1
-private const val WS_PORT = 8765
-private const val RECONNECT_DELAY_MS = 5000L
-
-class ScaleGatewayService : Service(), BleScaleListener, ServerEventListener {
-
- private var bleManager: BleScaleManager? = null
- private var wsServer: GatewayWebSocketServer? = null
- private var lastBattery: Int? = null
- private var connectedDeviceName: String? = null
- private val mainHandler = Handler(Looper.getMainLooper())
-
- // Binder so KioskActivity can get status updates
- inner class LocalBinder : Binder() {
- fun getService(): ScaleGatewayService = this@ScaleGatewayService
- }
- private val binder = LocalBinder()
-
- // Callbacks for the activity
- var statusCallback: ((String, String?, Int?) -> Unit)? = null // state, device, battery
- var weightCallback: ((Float, String, Boolean) -> Unit)? = null // value, unit, stable
-
- override fun onBind(intent: Intent?): IBinder = binder
-
- override fun onCreate() {
- super.onCreate()
- createNotificationChannel()
- startForeground(NOTIFICATION_ID, buildNotification("Starting..."))
-
- // Start WebSocket server
- wsServer = GatewayWebSocketServer(WS_PORT, this).also {
- try { it.start() } catch (e: Exception) {
- Log.e(TAG, "Failed to start WS server", e)
- }
- }
-
- // Start BLE manager
- bleManager = BleScaleManager(this, this).also {
- if (it.hasRequiredPermissions()) {
- it.enableAutoConnect()
- it.startScan()
- }
- }
- }
-
- override fun onDestroy() {
- bleManager?.disconnect()
- bleManager?.stopScan()
- try { wsServer?.stop(1000) } catch (_: Exception) {}
- super.onDestroy()
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- return START_STICKY
- }
-
- fun startScaleScan() {
- bleManager?.let {
- if (it.hasRequiredPermissions()) {
- it.enableAutoConnect()
- it.startScan()
- }
- }
- }
-
- fun disconnectScale() {
- bleManager?.disconnect()
- connectedDeviceName = null
- wsServer?.publishStatus("disconnected", null, null)
- updateNotification("Gateway active — no scale")
- statusCallback?.invoke("disconnected", null, null)
- }
-
- fun connectDevice(device: BluetoothDevice) {
- bleManager?.connect(device)
- }
-
- val isScaleConnected: Boolean get() = bleManager?.isConnected == true
-
- // ─── BleScaleListener ──────────────────────────────────────────────────
-
- override fun onDeviceFound(info: BleDeviceInfo) {}
- override fun onConnecting(device: BluetoothDevice) {
- updateNotification("Connecting...")
- statusCallback?.invoke("connecting", null, null)
- }
-
- override fun onConnected(deviceName: String) {
- connectedDeviceName = deviceName
- wsServer?.publishStatus("connected", deviceName, lastBattery)
- updateNotification("Connected: $deviceName")
- statusCallback?.invoke("connected", deviceName, lastBattery)
- }
-
- override fun onDisconnected() {
- connectedDeviceName = null
- wsServer?.publishStatus("disconnected", null, null)
- updateNotification("Scale disconnected — reconnecting...")
- statusCallback?.invoke("disconnected", null, null)
- // Auto-reconnect
- mainHandler.postDelayed({
- bleManager?.let {
- if (!it.isConnected && it.hasRequiredPermissions()) {
- it.enableAutoConnect()
- it.startScan()
- }
- }
- }, RECONNECT_DELAY_MS)
- }
-
- override fun onWeightReceived(reading: WeightReading) {
- wsServer?.publishWeight(reading.value, reading.unit, reading.stable, lastBattery)
- weightCallback?.invoke(reading.value, reading.unit, reading.stable)
- }
-
- override fun onBatteryReceived(level: Int) {
- lastBattery = level
- wsServer?.publishStatus("connected", connectedDeviceName, level)
- }
-
- override fun onError(message: String) {
- Log.w(TAG, "BLE error: $message")
- }
-
- override fun onScanStopped() {}
- override fun onDebugEvent(message: String) {}
-
- // ─── ServerEventListener ───────────────────────────────────────────────
-
- override fun onClientConnected(address: String) {
- Log.d(TAG, "WS client connected: $address")
- }
-
- override fun onClientDisconnected(address: String) {
- Log.d(TAG, "WS client disconnected: $address")
- }
-
- override fun onClientRequestedWeight() {}
-
- // ─── Notification ──────────────────────────────────────────────────────
-
- private fun createNotificationChannel() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val channel = NotificationChannel(
- CHANNEL_ID,
- "Scale Gateway",
- NotificationManager.IMPORTANCE_LOW
- ).apply {
- description = "EverShelf Scale Gateway running"
- setShowBadge(false)
- }
- (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
- .createNotificationChannel(channel)
- }
- }
-
- private fun buildNotification(text: String): Notification {
- val intent = Intent(this, KioskActivity::class.java).apply {
- flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
- }
- val pendingIntent = PendingIntent.getActivity(
- this, 0, intent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
- return NotificationCompat.Builder(this, CHANNEL_ID)
- .setContentTitle("EverShelf Gateway")
- .setContentText(text)
- .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
- .setContentIntent(pendingIntent)
- .setOngoing(true)
- .build()
- }
-
- private fun updateNotification(text: String) {
- val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- nm.notify(NOTIFICATION_ID, buildNotification(text))
- }
-}
diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ScaleProtocol.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ScaleProtocol.kt
deleted file mode 100644
index 3052247..0000000
--- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/ScaleProtocol.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-package it.dadaloop.evershelf.kiosk
-
-import android.bluetooth.BluetoothGattCharacteristic
-import java.util.UUID
-
-data class WeightReading(
- val value: Float,
- val unit: String,
- val stable: Boolean,
-)
-
-val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
-
-object BleUuids {
- val WEIGHT_SCALE_SERVICE = UUID.fromString("0000181d-0000-1000-8000-00805f9b34fb")
- val WEIGHT_MEASUREMENT_CHAR = UUID.fromString("00002a9d-0000-1000-8000-00805f9b34fb")
- val BATTERY_SERVICE = UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb")
- val BATTERY_LEVEL_CHAR = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")
- val FFE0 = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb")
- val FFE1 = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb")
- val FFF0 = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb")
- val FFF1 = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb")
- val FFF4 = UUID.fromString("0000fff4-0000-1000-8000-00805f9b34fb")
- val ACAIA_SERVICE = UUID.fromString("49535343-fe7d-4ae5-8fa9-9fafd205e455")
- val ACAIA_CHAR = UUID.fromString("49535343-8841-43f4-a8d4-ecbe34729bb3")
- val QN_AE00 = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb")
- val QN_AE02 = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb")
-}
-
-object ScaleProtocol {
- private const val MAX_GRAMS = 15000f
- private const val MIN_GRAMS = 0.5f
-
- fun resetState() {}
-
- fun parse(
- char: BluetoothGattCharacteristic,
- data: ByteArray,
- debug: ((String) -> Unit)? = null,
- ): WeightReading? {
- if (data.size < 2) return null
-
- when (char.uuid) {
- BleUuids.WEIGHT_MEASUREMENT_CHAR -> return parseSigWeight(data, debug)
- }
-
- if (data.size == 18
- && (data[0].toInt() and 0xFF) == 0x10
- && (data[1].toInt() and 0xFF) == 0x12) {
- return parseQNFood(data, debug)
- }
-
- return parseGeneric(data, debug)
- }
-
- private fun parseSigWeight(data: ByteArray, debug: ((String) -> Unit)? = null): WeightReading? {
- if (data.size < 3) return null
- val flags = data[0].toInt() and 0xFF
- val isImperial = (flags and 0x01) != 0
- val raw = u16le(data, 1)
- return if (isImperial) {
- val lb = raw * 0.01f
- if (lb < 0.01f || lb > 33f) null else WeightReading(lb, "lb", stable = true)
- } else {
- val g = raw * 5f
- if (g < MIN_GRAMS || g > MAX_GRAMS) null else WeightReading(g, "g", stable = true)
- }
- }
-
- private fun parseQNFood(data: ByteArray, debug: ((String) -> Unit)? = null): WeightReading? {
- val calc = data.take(17).sumOf { it.toInt() and 0xFF } and 0xFF
- if (calc != (data[17].toInt() and 0xFF)) return null
- val rawValue = u16be(data, 9)
- val stable = (data[8].toInt() and 0x08) != 0
- val unit = when (data[4].toInt() and 0xFF) {
- 0x01 -> "g"; 0x02 -> "oz"; 0x03 -> "ml"; 0x04 -> "ml"; else -> "g"
- }
- val value = rawValue / 10f
- if (rawValue == 0) return null
- val valueG = if (unit == "oz") value * 28.3495f else value
- if (valueG < MIN_GRAMS || valueG > MAX_GRAMS) return null
- return WeightReading(value, unit, stable)
- }
-
- private fun parseGeneric(data: ByteArray, debug: ((String) -> Unit)? = null): WeightReading? {
- if (data.size < 3) return null
- data class C(val pos: Int, val be: Boolean, val div: Float, val label: String)
- val candidates = listOf(
- C(1, false, 1f, "p1LEg"), C(1, true, 1f, "p1BEg"),
- C(2, false, 1f, "p2LEg"), C(2, true, 1f, "p2BEg"),
- C(3, false, 1f, "p3LEg"), C(3, true, 1f, "p3BEg"),
- C(1, false, 10f, "p1LE.1g"), C(1, true, 10f, "p1BE.1g"),
- C(2, false, 10f, "p2LE.1g"), C(2, true, 10f, "p2BE.1g"),
- C(3, false, 10f, "p3LE.1g"), C(3, true, 10f, "p3BE.1g"),
- C(1, false, 2f, "p1LE.5g"), C(1, true, 2f, "p1BE.5g"),
- C(1, false, 0.1f, "p1LEcg"), C(1, true, 0.1f, "p1BEcg"),
- C(3, false, 0.1f, "p3LEcg"), C(3, true, 0.1f, "p3BEcg"),
- )
- for (c in candidates) {
- if (c.pos + 1 >= data.size) continue
- val raw = if (c.be) u16be(data, c.pos) else u16le(data, c.pos)
- if (raw == 0) continue
- val g = raw / c.div
- if (g in MIN_GRAMS..MAX_GRAMS) return WeightReading(g, "g", stable = false)
- }
- return null
- }
-
- private fun u16le(b: ByteArray, off: Int): Int =
- (b[off].toInt() and 0xFF) or ((b[off + 1].toInt() and 0xFF) shl 8)
- private fun u16be(b: ByteArray, off: Int): Int =
- ((b[off].toInt() and 0xFF) shl 8) or (b[off + 1].toInt() and 0xFF)
-}
diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SettingsActivity.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SettingsActivity.kt
index 9ca0f4f..7d793ab 100644
--- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SettingsActivity.kt
+++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/SettingsActivity.kt
@@ -1,15 +1,21 @@
package it.dadaloop.evershelf.kiosk
import android.content.Context
+import android.content.Intent
import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.net.Uri
import android.os.Bundle
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton
-import java.net.HttpURLConnection
import java.net.URL
+import javax.net.ssl.HttpsURLConnection
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
class SettingsActivity : AppCompatActivity() {
@@ -20,7 +26,7 @@ class SettingsActivity : AppCompatActivity() {
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_LAST_DEVICE = "last_device_address"
+ private const val GATEWAY_PACKAGE = "it.dadaloop.evershelf.scalegate"
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -30,23 +36,32 @@ class SettingsActivity : AppCompatActivity() {
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
urlEdit = findViewById(R.id.urlEdit)
- // Load saved URL
urlEdit.setText(prefs.getString(KEY_URL, "") ?: "")
- // Scale status
- val scaleDevice = prefs.getString(KEY_LAST_DEVICE, null)
- findViewById(R.id.scaleDeviceInfo).text =
- if (scaleDevice != null) "Last connected: $scaleDevice" else "No scale connected yet"
-
- // Back button
- findViewById(R.id.btnBack).setOnClickListener {
- finish()
+ // Gateway status
+ val gatewayInstalled = try {
+ packageManager.getPackageInfo(GATEWAY_PACKAGE, 0)
+ true
+ } catch (e: PackageManager.NameNotFoundException) {
+ false
}
+ val statusView = findViewById(R.id.scaleGatewayStatus)
+ val deviceView = findViewById(R.id.scaleDeviceInfo)
+ if (gatewayInstalled) {
+ statusView.text = "Installed"
+ statusView.setTextColor(0xFF34d399.toInt())
+ deviceView.text = "EverShelf Scale Gateway app is installed"
+ } else {
+ statusView.text = "Not installed"
+ statusView.setTextColor(0xFFfbbf24.toInt())
+ deviceView.text = "Install the Scale Gateway app to use a Bluetooth scale"
+ }
+
+ // Back
+ findViewById(R.id.btnBack).setOnClickListener { finish() }
// Test connection
- findViewById(R.id.btnTestConnection).setOnClickListener {
- testConnection()
- }
+ findViewById(R.id.btnTestConnection).setOnClickListener { testConnection() }
// Run wizard again
findViewById(R.id.btnRunWizard).setOnClickListener {
@@ -78,17 +93,32 @@ class SettingsActivity : AppCompatActivity() {
Thread {
try {
val testUrl = if (url.endsWith("/")) "${url}api/" else "${url}/api/"
- val conn = URL(testUrl).openConnection() as HttpURLConnection
+ val conn = URL(testUrl).openConnection()
+
+ if (conn is HttpsURLConnection) {
+ val trustAll = arrayOf(object : X509TrustManager {
+ override fun checkClientTrusted(chain: Array?, authType: String?) {}
+ override fun checkServerTrusted(chain: Array?, authType: String?) {}
+ override fun getAcceptedIssuers(): Array = 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.connectTimeout = 5000
conn.readTimeout = 5000
- conn.requestMethod = "GET"
- val code = conn.responseCode
- conn.disconnect()
- runOnUiThread {
- if (code in 200..299) {
- Toast.makeText(this, "✓ Connection successful!", Toast.LENGTH_SHORT).show()
- } else {
- Toast.makeText(this, "⚠ Server responded: $code", Toast.LENGTH_SHORT).show()
+ if (conn is java.net.HttpURLConnection) {
+ conn.requestMethod = "GET"
+ val code = conn.responseCode
+ conn.disconnect()
+ runOnUiThread {
+ if (code in 200..299) {
+ Toast.makeText(this, "✓ Connection successful!", Toast.LENGTH_SHORT).show()
+ } else {
+ Toast.makeText(this, "⚠ Server responded: $code", Toast.LENGTH_SHORT).show()
+ }
}
}
} catch (e: Exception) {