refactor(kiosk): remove built-in scale, add SSL + gateway detection
- Remove BLE/scale code (BleScaleManager, ScaleProtocol, GatewayWebSocketServer, ScaleGatewayService) - Kiosk is now a pure WebView wrapper — scale handled by standalone gateway app - Fix SSL certificate error: accept self-signed certs for local servers (WebView + connection test) - Add gateway APK detection: check if it.dadaloop.evershelf.scalegate is installed - If gateway installed: show green status, auto-launch on finish - If not installed: show download link to GitHub releases - Remove BLE/foreground service permissions from manifest - Remove java-websocket dependency - Bump version to 1.1.0
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "it.dadaloop.evershelf.kiosk"
|
applicationId = "it.dadaloop.evershelf.kiosk"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "1.0.0"
|
versionName = "1.1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -41,8 +41,4 @@ dependencies {
|
|||||||
implementation("com.google.android.material:material:1.11.0")
|
implementation("com.google.android.material:material:1.11.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.webkit:webkit:1.10.0")
|
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")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<!-- BLE permissions for Android < 12 -->
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
|
|
||||||
<!-- BLE permissions for Android 12+ -->
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
|
||||||
android:usesPermissionFlags="neverForLocation" />
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
||||||
|
|
||||||
<!-- Location (required for BLE scanning on Android 6–11) -->
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
||||||
|
|
||||||
<!-- Network -->
|
<!-- Network -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
<!-- Keep screen on / foreground service -->
|
<!-- Keep screen on -->
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -54,11 +34,6 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar" />
|
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".ScaleGatewayService"
|
|
||||||
android:foregroundServiceType="connectedDevice"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -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<BluetoothGattCharacteristic>()
|
|
||||||
|
|
||||||
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<BluetoothGattCharacteristic>()
|
|
||||||
|
|
||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-100
@@ -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<WebSocket> =
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,22 @@
|
|||||||
package it.dadaloop.evershelf.kiosk
|
package it.dadaloop.evershelf.kiosk
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.net.http.SslError
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.webkit.ConsoleMessage
|
import android.webkit.ConsoleMessage
|
||||||
import android.webkit.PermissionRequest
|
import android.webkit.PermissionRequest
|
||||||
|
import android.webkit.SslErrorHandler
|
||||||
import android.webkit.ValueCallback
|
import android.webkit.ValueCallback
|
||||||
import android.webkit.WebChromeClient
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebResourceError
|
import android.webkit.WebResourceError
|
||||||
@@ -26,18 +24,17 @@ import android.webkit.WebResourceRequest
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
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() {
|
class KioskActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -58,34 +55,16 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
private lateinit var scaleStatusText: TextView
|
private lateinit var scaleStatusText: TextView
|
||||||
private lateinit var scaleStatusDetail: 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
|
// File chooser
|
||||||
private var fileChooserCallback: ValueCallback<Array<Uri>>? = null
|
private var fileChooserCallback: ValueCallback<Array<Uri>>? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BLE_PERMISSION_REQUEST = 1001
|
|
||||||
private const val FILE_CHOOSER_REQUEST = 1002
|
private const val FILE_CHOOSER_REQUEST = 1002
|
||||||
private const val PREFS_NAME = "evershelf_kiosk"
|
private const val PREFS_NAME = "evershelf_kiosk"
|
||||||
private const val KEY_URL = "evershelf_url"
|
private const val KEY_URL = "evershelf_url"
|
||||||
private const val KEY_SETUP_COMPLETE = "setup_complete"
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -136,7 +115,6 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
prefs.edit().putString(KEY_URL, url).apply()
|
prefs.edit().putString(KEY_URL, url).apply()
|
||||||
requestBlePermissions()
|
|
||||||
goToStep(3)
|
goToStep(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +123,7 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
goToStep(2)
|
goToStep(2)
|
||||||
}
|
}
|
||||||
findViewById<MaterialButton>(R.id.btnFinish).setOnClickListener {
|
findViewById<MaterialButton>(R.id.btnFinish).setOnClickListener {
|
||||||
|
launchGatewayIfInstalled()
|
||||||
finishWizard()
|
finishWizard()
|
||||||
}
|
}
|
||||||
findViewById<MaterialButton>(R.id.btnSkipScale).setOnClickListener {
|
findViewById<MaterialButton>(R.id.btnSkipScale).setOnClickListener {
|
||||||
@@ -180,7 +159,7 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
updateStepIndicator()
|
updateStepIndicator()
|
||||||
|
|
||||||
if (step == 3) {
|
if (step == 3) {
|
||||||
startScaleGateway()
|
checkGatewayStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +202,57 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
goToStep(1)
|
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<MaterialButton>(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<MaterialButton>(R.id.btnFinish)
|
||||||
|
downloadBtn.text = "🚀 Launch EverShelf (without scale)"
|
||||||
|
|
||||||
|
findViewById<MaterialButton>(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 ───────────────────────────────────────────────────
|
// ── Connection Test ───────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun testConnection() {
|
private fun testConnection() {
|
||||||
@@ -236,17 +266,33 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
val testUrl = if (url.endsWith("/")) "${url}api/" else "${url}/api/"
|
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<TrustManager>(object : X509TrustManager {
|
||||||
|
override fun checkClientTrusted(chain: Array<java.security.cert.X509Certificate>?, authType: String?) {}
|
||||||
|
override fun checkServerTrusted(chain: Array<java.security.cert.X509Certificate>?, authType: 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.connectTimeout = 5000
|
conn.connectTimeout = 5000
|
||||||
conn.readTimeout = 5000
|
conn.readTimeout = 5000
|
||||||
conn.requestMethod = "GET"
|
if (conn is java.net.HttpURLConnection) {
|
||||||
val code = conn.responseCode
|
conn.requestMethod = "GET"
|
||||||
conn.disconnect()
|
val code = conn.responseCode
|
||||||
runOnUiThread {
|
conn.disconnect()
|
||||||
if (code in 200..299) {
|
runOnUiThread {
|
||||||
showUrlStatus("✓ Connected successfully!", true)
|
if (code in 200..299) {
|
||||||
} else {
|
showUrlStatus("✓ Connected successfully!", true)
|
||||||
showUrlStatus("⚠ Server responded with code $code", false)
|
} else {
|
||||||
|
showUrlStatus("⚠ Server responded with code $code", false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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<String>()
|
|
||||||
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 ───────────────────────────────────────────────────────────
|
// ── WebView ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@@ -352,6 +329,13 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
settings.allowFileAccess = true
|
settings.allowFileAccess = true
|
||||||
|
|
||||||
webView.webViewClient = object : WebViewClient() {
|
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(
|
override fun onReceivedError(
|
||||||
view: WebView?, request: WebResourceRequest?,
|
view: WebView?, request: WebResourceRequest?,
|
||||||
error: WebResourceError?
|
error: WebResourceError?
|
||||||
@@ -385,8 +369,8 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
|
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
|
||||||
webView.loadUrl(url)
|
webView.loadUrl(url)
|
||||||
|
|
||||||
// Start scale gateway
|
// Launch gateway app if installed (handles scale in background)
|
||||||
startScaleGateway()
|
launchGatewayIfInstalled()
|
||||||
|
|
||||||
// Keep screen on
|
// Keep screen on
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
@@ -443,17 +427,19 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
enterImmersiveMode()
|
enterImmersiveMode()
|
||||||
// Reload WebView if setup is complete (in case URL changed in settings)
|
|
||||||
if (prefs.getBoolean(KEY_SETUP_COMPLETE, false) && webView.visibility == View.VISIBLE) {
|
if (prefs.getBoolean(KEY_SETUP_COMPLETE, false) && webView.visibility == View.VISIBLE) {
|
||||||
val url = prefs.getString(KEY_URL, "") ?: ""
|
val url = prefs.getString(KEY_URL, "") ?: ""
|
||||||
if (url.isNotEmpty() && webView.url != url) {
|
if (url.isNotEmpty() && webView.url != url) {
|
||||||
webView.loadUrl(url)
|
webView.loadUrl(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if wizard reset was requested
|
|
||||||
if (!prefs.getBoolean(KEY_SETUP_COMPLETE, false) && wizardContainer.visibility != View.VISIBLE) {
|
if (!prefs.getBoolean(KEY_SETUP_COMPLETE, false) && wizardContainer.visibility != View.VISIBLE) {
|
||||||
showWizard()
|
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?) {
|
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() {
|
override fun onBackPressed() {
|
||||||
if (webView.visibility == View.VISIBLE && webView.canGoBack()) {
|
if (webView.visibility == View.VISIBLE && webView.canGoBack()) {
|
||||||
webView.goBack()
|
webView.goBack()
|
||||||
|
|||||||
-194
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
+53
-23
@@ -1,15 +1,21 @@
|
|||||||
package it.dadaloop.evershelf.kiosk
|
package it.dadaloop.evershelf.kiosk
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
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() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -20,7 +26,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
private const val PREFS_NAME = "evershelf_kiosk"
|
private const val PREFS_NAME = "evershelf_kiosk"
|
||||||
private const val KEY_URL = "evershelf_url"
|
private const val KEY_URL = "evershelf_url"
|
||||||
private const val KEY_SETUP_COMPLETE = "setup_complete"
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -30,23 +36,32 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
urlEdit = findViewById(R.id.urlEdit)
|
urlEdit = findViewById(R.id.urlEdit)
|
||||||
|
|
||||||
// Load saved URL
|
|
||||||
urlEdit.setText(prefs.getString(KEY_URL, "") ?: "")
|
urlEdit.setText(prefs.getString(KEY_URL, "") ?: "")
|
||||||
|
|
||||||
// Scale status
|
// Gateway status
|
||||||
val scaleDevice = prefs.getString(KEY_LAST_DEVICE, null)
|
val gatewayInstalled = try {
|
||||||
findViewById<TextView>(R.id.scaleDeviceInfo).text =
|
packageManager.getPackageInfo(GATEWAY_PACKAGE, 0)
|
||||||
if (scaleDevice != null) "Last connected: $scaleDevice" else "No scale connected yet"
|
true
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
// Back button
|
false
|
||||||
findViewById<android.widget.ImageButton>(R.id.btnBack).setOnClickListener {
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
|
val statusView = findViewById<TextView>(R.id.scaleGatewayStatus)
|
||||||
|
val deviceView = findViewById<TextView>(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<android.widget.ImageButton>(R.id.btnBack).setOnClickListener { finish() }
|
||||||
|
|
||||||
// Test connection
|
// Test connection
|
||||||
findViewById<MaterialButton>(R.id.btnTestConnection).setOnClickListener {
|
findViewById<MaterialButton>(R.id.btnTestConnection).setOnClickListener { testConnection() }
|
||||||
testConnection()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run wizard again
|
// Run wizard again
|
||||||
findViewById<MaterialButton>(R.id.btnRunWizard).setOnClickListener {
|
findViewById<MaterialButton>(R.id.btnRunWizard).setOnClickListener {
|
||||||
@@ -78,17 +93,32 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
val testUrl = if (url.endsWith("/")) "${url}api/" else "${url}/api/"
|
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<TrustManager>(object : X509TrustManager {
|
||||||
|
override fun checkClientTrusted(chain: Array<java.security.cert.X509Certificate>?, authType: String?) {}
|
||||||
|
override fun checkServerTrusted(chain: Array<java.security.cert.X509Certificate>?, authType: 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.connectTimeout = 5000
|
conn.connectTimeout = 5000
|
||||||
conn.readTimeout = 5000
|
conn.readTimeout = 5000
|
||||||
conn.requestMethod = "GET"
|
if (conn is java.net.HttpURLConnection) {
|
||||||
val code = conn.responseCode
|
conn.requestMethod = "GET"
|
||||||
conn.disconnect()
|
val code = conn.responseCode
|
||||||
runOnUiThread {
|
conn.disconnect()
|
||||||
if (code in 200..299) {
|
runOnUiThread {
|
||||||
Toast.makeText(this, "✓ Connection successful!", Toast.LENGTH_SHORT).show()
|
if (code in 200..299) {
|
||||||
} else {
|
Toast.makeText(this, "✓ Connection successful!", Toast.LENGTH_SHORT).show()
|
||||||
Toast.makeText(this, "⚠ Server responded: $code", Toast.LENGTH_SHORT).show()
|
} else {
|
||||||
|
Toast.makeText(this, "⚠ Server responded: $code", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
Reference in New Issue
Block a user