refactor: improve layout and formatting of HistoryScreen UI components
This commit is contained in:
@@ -19,8 +19,6 @@ import java.util.*
|
|||||||
class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "LBJ_BT"
|
const val TAG = "LBJ_BT"
|
||||||
const val SCAN_PERIOD = 10000L
|
|
||||||
|
|
||||||
val SERVICE_UUID = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb")
|
val SERVICE_UUID = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb")
|
||||||
val CHAR_UUID = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb")
|
val CHAR_UUID = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
@@ -42,20 +40,69 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
private var targetDeviceName: String? = null
|
private var targetDeviceName: String? = null
|
||||||
private var bluetoothLeScanner: BluetoothLeScanner? = null
|
private var bluetoothLeScanner: BluetoothLeScanner? = null
|
||||||
|
|
||||||
|
private var continuousScanning = false
|
||||||
|
private var autoReconnect = true
|
||||||
|
private var lastKnownDeviceAddress: String? = null
|
||||||
|
private var connectionAttempts = 0
|
||||||
|
private var isReconnecting = false
|
||||||
|
|
||||||
private val leScanCallback = object : ScanCallback() {
|
private val leScanCallback = object : ScanCallback() {
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
val device = result.device
|
val device = result.device
|
||||||
val deviceName = device.name
|
val deviceName = device.name
|
||||||
if (targetDeviceName != null) {
|
|
||||||
if (deviceName == null || !deviceName.equals(targetDeviceName, ignoreCase = true)) {
|
val shouldShowDevice = when {
|
||||||
return
|
targetDeviceName != null -> {
|
||||||
|
deviceName != null && deviceName.equals(targetDeviceName, ignoreCase = true)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
deviceName != null && (
|
||||||
|
deviceName.contains("LBJ", ignoreCase = true) ||
|
||||||
|
deviceName.contains("Receiver", ignoreCase = true) ||
|
||||||
|
deviceName.contains("Train", ignoreCase = true) ||
|
||||||
|
deviceName.contains("Console", ignoreCase = true) ||
|
||||||
|
deviceName.contains("ESP", ignoreCase = true) ||
|
||||||
|
deviceName.contains("Arduino", ignoreCase = true) ||
|
||||||
|
deviceName.contains("BLE", ignoreCase = true) ||
|
||||||
|
deviceName.contains("UART", ignoreCase = true) ||
|
||||||
|
deviceName.contains("Serial", ignoreCase = true)
|
||||||
|
) && !(
|
||||||
|
deviceName.contains("Midea", ignoreCase = true) ||
|
||||||
|
deviceName.contains("TV", ignoreCase = true) ||
|
||||||
|
deviceName.contains("Phone", ignoreCase = true) ||
|
||||||
|
deviceName.contains("Watch", ignoreCase = true) ||
|
||||||
|
deviceName.contains("Headset", ignoreCase = true) ||
|
||||||
|
deviceName.contains("Speaker", ignoreCase = true)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scanCallback?.invoke(device)
|
|
||||||
|
if (shouldShowDevice) {
|
||||||
|
Log.d(TAG, "Showing filtered device: $deviceName")
|
||||||
|
scanCallback?.invoke(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetDeviceName != null && !isConnected && !isReconnecting) {
|
||||||
|
if (deviceName != null && deviceName.equals(targetDeviceName, ignoreCase = true)) {
|
||||||
|
Log.i(TAG, "Found target device: $deviceName, auto-connecting")
|
||||||
|
lastKnownDeviceAddress = device.address
|
||||||
|
connectImmediately(device.address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastKnownDeviceAddress == device.address && !isConnected && !isReconnecting) {
|
||||||
|
Log.i(TAG, "Found known device, reconnecting immediately")
|
||||||
|
connectImmediately(device.address)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScanFailed(errorCode: Int) {
|
override fun onScanFailed(errorCode: Int) {
|
||||||
Log.e(TAG, "BLE scan failed code=$errorCode")
|
Log.e(TAG, "BLE scan failed code=$errorCode")
|
||||||
|
if (continuousScanning) {
|
||||||
|
handler.post {
|
||||||
|
restartScan()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +154,9 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.postDelayed({
|
|
||||||
stopScan()
|
|
||||||
}, SCAN_PERIOD)
|
|
||||||
|
|
||||||
isScanning = true
|
isScanning = true
|
||||||
Log.d(TAG, "Starting BLE scan target=${targetDeviceName ?: "Any"}")
|
continuousScanning = true
|
||||||
|
Log.d(TAG, "Starting continuous BLE scan target=${targetDeviceName ?: "Any"}")
|
||||||
bluetoothLeScanner?.startScan(leScanCallback)
|
bluetoothLeScanner?.startScan(leScanCallback)
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.e(TAG, "Scan security error: ${e.message}")
|
Log.e(TAG, "Scan security error: ${e.message}")
|
||||||
@@ -127,6 +171,40 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
if (isScanning) {
|
if (isScanning) {
|
||||||
bluetoothLeScanner?.stopScan(leScanCallback)
|
bluetoothLeScanner?.stopScan(leScanCallback)
|
||||||
isScanning = false
|
isScanning = false
|
||||||
|
continuousScanning = false
|
||||||
|
Log.d(TAG, "Stopped BLE scan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun restartScan() {
|
||||||
|
if (!continuousScanning) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
bluetoothLeScanner?.stopScan(leScanCallback)
|
||||||
|
bluetoothLeScanner?.startScan(leScanCallback)
|
||||||
|
isScanning = true
|
||||||
|
Log.d(TAG, "Restarted BLE scan")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to restart scan: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectImmediately(address: String) {
|
||||||
|
if (isReconnecting) return
|
||||||
|
isReconnecting = true
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
connect(address) { connected ->
|
||||||
|
isReconnecting = false
|
||||||
|
if (connected) {
|
||||||
|
connectionAttempts = 0
|
||||||
|
Log.i(TAG, "Successfully connected to $address")
|
||||||
|
} else {
|
||||||
|
connectionAttempts++
|
||||||
|
Log.w(TAG, "Connection attempt $connectionAttempts failed for $address")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,16 +263,6 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
Log.d(TAG, "Connecting to address=$address")
|
Log.d(TAG, "Connecting to address=$address")
|
||||||
|
|
||||||
|
|
||||||
handler.postDelayed({
|
|
||||||
if (!isConnected && deviceAddress == address) {
|
|
||||||
Log.e(TAG, "Connection timeout reconnecting")
|
|
||||||
|
|
||||||
bluetoothGatt?.close()
|
|
||||||
bluetoothGatt =
|
|
||||||
device.connectGatt(context, false, this, BluetoothDevice.TRANSPORT_LE)
|
|
||||||
}
|
|
||||||
}, 10000)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Connection failed: ${e.message}")
|
Log.e(TAG, "Connection failed: ${e.message}")
|
||||||
@@ -286,30 +354,30 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
Log.e(TAG, "Connection error status=$status")
|
Log.e(TAG, "Connection error status=$status")
|
||||||
isConnected = false
|
isConnected = false
|
||||||
|
isReconnecting = false
|
||||||
|
|
||||||
if (status == 133 || status == 8) {
|
if (status == 133 || status == 8) {
|
||||||
Log.e(TAG, "GATT error closing connection")
|
Log.e(TAG, "GATT error, attempting immediate reconnection")
|
||||||
try {
|
try {
|
||||||
gatt.close()
|
gatt.close()
|
||||||
bluetoothGatt = null
|
bluetoothGatt = null
|
||||||
|
|
||||||
|
|
||||||
deviceAddress?.let { address ->
|
deviceAddress?.let { address ->
|
||||||
handler.postDelayed({
|
if (autoReconnect) {
|
||||||
Log.d(TAG, "Reconnecting to device")
|
Log.d(TAG, "Immediate reconnection to device")
|
||||||
val device =
|
handler.post {
|
||||||
BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address)
|
val device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address)
|
||||||
bluetoothGatt = device.connectGatt(
|
bluetoothGatt = device.connectGatt(
|
||||||
context,
|
context,
|
||||||
false,
|
false,
|
||||||
this,
|
this,
|
||||||
BluetoothDevice.TRANSPORT_LE
|
BluetoothDevice.TRANSPORT_LE
|
||||||
)
|
)
|
||||||
}, 2000)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Reconnect error: ${e.message}")
|
Log.e(TAG, "Immediate reconnect error: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,32 +388,34 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
when (newState) {
|
when (newState) {
|
||||||
BluetoothProfile.STATE_CONNECTED -> {
|
BluetoothProfile.STATE_CONNECTED -> {
|
||||||
isConnected = true
|
isConnected = true
|
||||||
|
isReconnecting = false
|
||||||
|
connectionAttempts = 0
|
||||||
Log.i(TAG, "Connected to GATT server")
|
Log.i(TAG, "Connected to GATT server")
|
||||||
|
|
||||||
handler.post { connectionStateCallback?.invoke(true) }
|
handler.post { connectionStateCallback?.invoke(true) }
|
||||||
|
|
||||||
|
handler.post {
|
||||||
handler.postDelayed({
|
|
||||||
try {
|
try {
|
||||||
gatt.discoverServices()
|
gatt.discoverServices()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Service discovery failed: ${e.message}")
|
Log.e(TAG, "Service discovery failed: ${e.message}")
|
||||||
}
|
}
|
||||||
}, 500)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||||
isConnected = false
|
isConnected = false
|
||||||
|
isReconnecting = false
|
||||||
Log.i(TAG, "Disconnected from GATT server")
|
Log.i(TAG, "Disconnected from GATT server")
|
||||||
|
|
||||||
handler.post { connectionStateCallback?.invoke(false) }
|
handler.post { connectionStateCallback?.invoke(false) }
|
||||||
|
|
||||||
|
|
||||||
if (!deviceAddress.isNullOrBlank()) {
|
if (!deviceAddress.isNullOrBlank() && autoReconnect) {
|
||||||
handler.postDelayed({
|
handler.post {
|
||||||
Log.d(TAG, "Reconnecting after disconnect")
|
Log.d(TAG, "Immediate reconnection after disconnect")
|
||||||
connect(deviceAddress!!, connectionStateCallback)
|
connect(deviceAddress!!, connectionStateCallback)
|
||||||
}, 3000)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,12 +427,14 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
private var lastDataTime = 0L
|
private var lastDataTime = 0L
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
override fun onCharacteristicChanged(
|
override fun onCharacteristicChanged(
|
||||||
gatt: BluetoothGatt,
|
gatt: BluetoothGatt,
|
||||||
characteristic: BluetoothGattCharacteristic
|
characteristic: BluetoothGattCharacteristic
|
||||||
) {
|
) {
|
||||||
super.onCharacteristicChanged(gatt, characteristic)
|
super.onCharacteristicChanged(gatt, characteristic)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
val newData = characteristic.value?.let {
|
val newData = characteristic.value?.let {
|
||||||
String(it, StandardCharsets.UTF_8)
|
String(it, StandardCharsets.UTF_8)
|
||||||
} ?: return
|
} ?: return
|
||||||
@@ -381,18 +453,17 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
val bufferContent = dataBuffer.toString()
|
val bufferContent = dataBuffer.toString()
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
if (lastDataTime > 0) {
|
||||||
if (lastDataTime > 0 && currentTime - lastDataTime > 5000) {
|
val timeDiff = currentTime - lastDataTime
|
||||||
Log.w(TAG, "Data timeout ${(currentTime - lastDataTime) / 1000}s")
|
if (timeDiff > 10000) {
|
||||||
|
Log.w(TAG, "Long data gap: ${timeDiff / 1000}s")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Buffer size=${dataBuffer.length} bytes")
|
Log.d(TAG, "Buffer size=${dataBuffer.length} bytes")
|
||||||
|
|
||||||
|
|
||||||
tryExtractJson(bufferContent)
|
tryExtractJson(bufferContent)
|
||||||
|
|
||||||
|
|
||||||
lastDataTime = currentTime
|
lastDataTime = currentTime
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,9 +582,16 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
||||||
)
|
)
|
||||||
if (descriptor != null) {
|
if (descriptor != null) {
|
||||||
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val writeResult = gatt.writeDescriptor(descriptor)
|
val writeResult = gatt.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
|
||||||
Log.d(TAG, "Descriptor write result=$writeResult")
|
Log.d(TAG, "Descriptor write result=$writeResult")
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val writeResult = gatt.writeDescriptor(descriptor)
|
||||||
|
Log.d(TAG, "Descriptor write result=$writeResult")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Descriptor not found")
|
Log.e(TAG, "Descriptor not found")
|
||||||
|
|
||||||
@@ -540,10 +618,19 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
|
|
||||||
|
|
||||||
private fun requestDataAfterDelay() {
|
private fun requestDataAfterDelay() {
|
||||||
handler.postDelayed({
|
handler.post {
|
||||||
statusCallback?.let { callback ->
|
statusCallback?.let { callback ->
|
||||||
getStatus(callback)
|
getStatus(callback)
|
||||||
}
|
}
|
||||||
}, 1000)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setAutoReconnect(enabled: Boolean) {
|
||||||
|
autoReconnect = enabled
|
||||||
|
Log.d(TAG, "Auto reconnect set to: $enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConnectionAttempts(): Int = connectionAttempts
|
||||||
|
|
||||||
|
fun getLastKnownDeviceAddress(): String? = lastKnownDeviceAddress
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import android.bluetooth.BluetoothDevice
|
|||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -30,6 +31,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -109,9 +111,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
val locationPermissionsGranted = permissions.filter { it.key.contains("LOCATION") }.all { it.value }
|
val locationPermissionsGranted = permissions.filter { it.key.contains("LOCATION") }.all { it.value }
|
||||||
|
|
||||||
if (bluetoothPermissionsGranted && locationPermissionsGranted) {
|
if (bluetoothPermissionsGranted && locationPermissionsGranted) {
|
||||||
Log.d(TAG, "Permissions granted")
|
Log.d(TAG, "Permissions granted, starting auto scan and connect")
|
||||||
|
startAutoScanAndConnect()
|
||||||
startScan()
|
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Missing permissions: $permissions")
|
Log.e(TAG, "Missing permissions: $permissions")
|
||||||
deviceStatus = "需要蓝牙和位置权限"
|
deviceStatus = "需要蓝牙和位置权限"
|
||||||
@@ -374,16 +375,16 @@ class MainActivity : ComponentActivity() {
|
|||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
deviceStatus = "已连接"
|
deviceStatus = "已连接"
|
||||||
|
temporaryStatusMessage = null
|
||||||
Log.d(TAG, "Connected to device name=${device.name ?: "Unknown"}")
|
Log.d(TAG, "Connected to device name=${device.name ?: "Unknown"}")
|
||||||
} else {
|
} else {
|
||||||
deviceStatus = "连接失败或已断开连接"
|
deviceStatus = "连接失败,正在重试..."
|
||||||
Log.e(TAG, "Connection failed name=${device.name ?: "Unknown"}")
|
Log.e(TAG, "Connection failed, auto-retry enabled for name=${device.name ?: "Unknown"}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceAddress = device.address
|
deviceAddress = device.address
|
||||||
stopScan()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -394,6 +395,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
try {
|
try {
|
||||||
val isTestData = jsonData.optBoolean("test_flag", false)
|
val isTestData = jsonData.optBoolean("test_flag", false)
|
||||||
lastUpdateTime = Date()
|
lastUpdateTime = Date()
|
||||||
|
temporaryStatusMessage = null
|
||||||
|
|
||||||
if (isTestData) {
|
if (isTestData) {
|
||||||
Log.i(TAG, "Received keep-alive signal")
|
Log.i(TAG, "Received keep-alive signal")
|
||||||
@@ -438,14 +440,55 @@ class MainActivity : ComponentActivity() {
|
|||||||
private fun updateTemporaryStatusMessage(message: String) {
|
private fun updateTemporaryStatusMessage(message: String) {
|
||||||
temporaryStatusMessage = message
|
temporaryStatusMessage = message
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
if (temporaryStatusMessage == message) {
|
|
||||||
temporaryStatusMessage = null
|
|
||||||
}
|
|
||||||
}, 3000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun startAutoScanAndConnect() {
|
||||||
|
Log.d(TAG, "Starting auto scan and connect")
|
||||||
|
|
||||||
|
if (!hasBluetoothPermissions()) {
|
||||||
|
Log.e(TAG, "Missing bluetooth permissions for auto scan")
|
||||||
|
deviceStatus = "需要蓝牙和位置权限"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
|
val bluetoothAdapter = bluetoothManager.adapter
|
||||||
|
if (bluetoothAdapter == null) {
|
||||||
|
Log.e(TAG, "Bluetooth adapter unavailable")
|
||||||
|
deviceStatus = "设备不支持蓝牙"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bluetoothAdapter.isEnabled) {
|
||||||
|
Log.e(TAG, "Bluetooth adapter disabled")
|
||||||
|
deviceStatus = "请启用蓝牙"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bleClient.setAutoReconnect(true)
|
||||||
|
|
||||||
|
val targetDeviceName = if (settingsDeviceName.isNotBlank() && settingsDeviceName != "LBJReceiver") {
|
||||||
|
settingsDeviceName
|
||||||
|
} else {
|
||||||
|
"LBJReceiver"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Auto scanning for target device: $targetDeviceName")
|
||||||
|
deviceStatus = "正在自动扫描连接..."
|
||||||
|
|
||||||
|
bleClient.scanDevices(targetDeviceName) { device ->
|
||||||
|
val deviceName = device.name ?: "Unknown"
|
||||||
|
Log.d(TAG, "Auto scan found device: $deviceName")
|
||||||
|
|
||||||
|
if (deviceName.equals(targetDeviceName, ignoreCase = true)) {
|
||||||
|
Log.d(TAG, "Found target device, auto connecting to: $deviceName")
|
||||||
|
bleClient.stopScan()
|
||||||
|
connectToDevice(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startScan() {
|
private fun startScan() {
|
||||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
val bluetoothAdapter = bluetoothManager.adapter
|
val bluetoothAdapter = bluetoothManager.adapter
|
||||||
@@ -461,25 +504,21 @@ class MainActivity : ComponentActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bleClient.setAutoReconnect(true)
|
||||||
|
|
||||||
isScanning = true
|
isScanning = true
|
||||||
foundDevices = emptyList()
|
foundDevices = emptyList()
|
||||||
val targetDeviceName = settingsDeviceName.ifBlank { null }
|
val targetDeviceName = if (settingsDeviceName.isNotBlank() && settingsDeviceName != "LBJReceiver") {
|
||||||
Log.d(TAG, "Starting BLE scan target=${targetDeviceName ?: "Any"}")
|
settingsDeviceName
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Starting continuous BLE scan target=${targetDeviceName ?: "Any"} (settings=${settingsDeviceName})")
|
||||||
|
|
||||||
bleClient.scanDevices(targetDeviceName) { device ->
|
bleClient.scanDevices(targetDeviceName) { device ->
|
||||||
if (!foundDevices.any { it.address == device.address }) {
|
if (!foundDevices.any { it.address == device.address }) {
|
||||||
Log.d(TAG, "Found device name=${device.name ?: "Unknown"} address=${device.address}")
|
Log.d(TAG, "Found device name=${device.name ?: "Unknown"} address=${device.address}")
|
||||||
foundDevices = foundDevices + device
|
foundDevices = foundDevices + device
|
||||||
|
|
||||||
if (targetDeviceName != null && device.name == targetDeviceName) {
|
|
||||||
Log.d(TAG, "Found target=$targetDeviceName, connecting")
|
|
||||||
stopScan()
|
|
||||||
connectToDevice(device)
|
|
||||||
} else {
|
|
||||||
if (targetDeviceName == null) {
|
|
||||||
showConnectionDialog = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,6 +530,21 @@ class MainActivity : ComponentActivity() {
|
|||||||
Log.d(TAG, "Stopped BLE scan")
|
Log.d(TAG, "Stopped BLE scan")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasBluetoothPermissions(): Boolean {
|
||||||
|
val bluetoothPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
ContextCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
|
||||||
|
ContextCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else {
|
||||||
|
ContextCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED &&
|
||||||
|
ContextCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
val locationPermissions = ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
||||||
|
ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
return bluetoothPermissions && locationPermissions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun updateDeviceList() {
|
private fun updateDeviceList() {
|
||||||
foundDevices = scanResults.map { it.device }
|
foundDevices = scanResults.map { it.device }
|
||||||
@@ -559,6 +613,16 @@ class MainActivity : ComponentActivity() {
|
|||||||
Log.d(TAG, "Saved settings deviceName=${settingsDeviceName} tab=${currentTab} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}")
|
Log.d(TAG, "Saved settings deviceName=${settingsDeviceName} tab=${currentTab} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
Log.d(TAG, "App resumed")
|
||||||
|
|
||||||
|
if (hasBluetoothPermissions() && !bleClient.isConnected()) {
|
||||||
|
Log.d(TAG, "App resumed and not connected, starting auto scan")
|
||||||
|
startAutoScanAndConnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -633,7 +697,8 @@ fun MainContent(
|
|||||||
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
||||||
else -> "${diffInSec / 3600}小时前"
|
else -> "${diffInSec / 3600}小时前"
|
||||||
}
|
}
|
||||||
delay(1000)
|
val updateInterval = if (diffInSec < 60) 500L else if (diffInSec < 3600) 30000L else 300000L
|
||||||
|
delay(updateInterval)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
timeSinceLastUpdate.value = null
|
timeSinceLastUpdate.value = null
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ fun HistoryScreen(
|
|||||||
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
||||||
else -> "${diffInSec / 3600}小时前"
|
else -> "${diffInSec / 3600}小时前"
|
||||||
}
|
}
|
||||||
delay(1000)
|
val updateInterval = if (diffInSec < 60) 500L else if (diffInSec < 3600) 30000L else 300000L
|
||||||
|
delay(updateInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +115,6 @@ fun HistoryScreen(
|
|||||||
|
|
||||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||||
if (!isInEditMode) {
|
if (!isInEditMode) {
|
||||||
delay(300)
|
|
||||||
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
||||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||||
}
|
}
|
||||||
@@ -217,11 +217,37 @@ fun HistoryScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
val recordId = record.timestamp.time.toString()
|
val recordId = record.timestamp.time.toString()
|
||||||
val isExpanded = expandedStatesMap[recordId] == true
|
val isExpanded = expandedStatesMap[recordId] == true
|
||||||
val recordMap = record.toMap(showDetailedTime = isExpanded)
|
val recordMap = record.toMap(showDetailedTime = true)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (recordMap.containsKey("time")) {
|
||||||
|
Column {
|
||||||
|
recordMap["time"]?.split("\n")?.forEach { timeLine ->
|
||||||
|
Text(
|
||||||
|
text = timeLine,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${record.rssi} dBm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -231,21 +257,6 @@ fun HistoryScreen(
|
|||||||
val trainDisplay =
|
val trainDisplay =
|
||||||
recordMap["train"]?.toString() ?: "未知列车"
|
recordMap["train"]?.toString() ?: "未知列车"
|
||||||
|
|
||||||
val formattedInfo = when {
|
|
||||||
record.locoType.isNotEmpty() && record.loco.isNotEmpty() -> {
|
|
||||||
val shortLoco = if (record.loco.length > 5) {
|
|
||||||
record.loco.takeLast(5)
|
|
||||||
} else {
|
|
||||||
record.loco
|
|
||||||
}
|
|
||||||
"${record.locoType}-${shortLoco}"
|
|
||||||
}
|
|
||||||
|
|
||||||
record.locoType.isNotEmpty() -> record.locoType
|
|
||||||
record.loco.isNotEmpty() -> record.loco
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
@@ -284,30 +295,27 @@ fun HistoryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formattedInfo.isNotEmpty() && formattedInfo != "<NUL>") {
|
|
||||||
Text(
|
|
||||||
text = formattedInfo,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
val formattedInfo = when {
|
||||||
text = "${record.rssi} dBm",
|
record.locoType.isNotEmpty() && record.loco.isNotEmpty() -> {
|
||||||
fontSize = 10.sp,
|
val shortLoco = if (record.loco.length > 5) {
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
record.loco.takeLast(5)
|
||||||
)
|
} else {
|
||||||
}
|
record.loco
|
||||||
|
}
|
||||||
|
"${record.locoType}-${shortLoco}"
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
record.locoType.isNotEmpty() -> record.locoType
|
||||||
|
record.loco.isNotEmpty() -> record.loco
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
if (recordMap.containsKey("time")) {
|
if (formattedInfo.isNotEmpty() && formattedInfo != "<NUL>") {
|
||||||
recordMap["time"]?.split("\n")?.forEach { timeLine ->
|
|
||||||
Text(
|
Text(
|
||||||
text = timeLine,
|
text = formattedInfo,
|
||||||
fontSize = 10.sp,
|
fontSize = 14.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ fun MapScreen(
|
|||||||
|
|
||||||
val locationProvider = GpsMyLocationProvider(ctx).apply {
|
val locationProvider = GpsMyLocationProvider(ctx).apply {
|
||||||
locationUpdateMinDistance = 10f
|
locationUpdateMinDistance = 10f
|
||||||
locationUpdateMinTime = 1000
|
locationUpdateMinTime = 5000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ fun MonitorScreen(
|
|||||||
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
||||||
else -> "${diffInSec / 3600}小时前"
|
else -> "${diffInSec / 3600}小时前"
|
||||||
}
|
}
|
||||||
delay(1000)
|
val updateInterval = if (diffInSec < 60) 500L else if (diffInSec < 3600) 30000L else 300000L
|
||||||
|
delay(updateInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user