2 Commits

6 changed files with 369 additions and 213 deletions

View File

@@ -12,8 +12,8 @@ android {
applicationId = "org.noxylva.lbjconsole" applicationId = "org.noxylva.lbjconsole"
minSdk = 29 minSdk = 29
targetSdk = 35 targetSdk = 35
versionCode = 3 versionCode = 4
versionName = "0.0.3" versionName = "0.0.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -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)
)
} }
} }
if (shouldShowDevice) {
Log.d(TAG, "Showing filtered device: $deviceName")
scanCallback?.invoke(device) 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) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val writeResult = gatt.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
Log.d(TAG, "Descriptor write result=$writeResult")
} else {
@Suppress("DEPRECATION")
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
@Suppress("DEPRECATION")
val writeResult = gatt.writeDescriptor(descriptor) val writeResult = gatt.writeDescriptor(descriptor)
Log.d(TAG, "Descriptor write result=$writeResult") 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
} }

View File

@@ -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
@@ -17,6 +18,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.ui.graphics.toArgb
import androidx.core.view.WindowCompat
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -26,10 +29,12 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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 +114,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 = "需要蓝牙和位置权限"
@@ -218,6 +222,10 @@ class MainActivity : ComponentActivity() {
saveSettings() saveSettings()
enableEdgeToEdge() enableEdgeToEdge()
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = false
}
setContent { setContent {
LBJReceiverTheme { LBJReceiverTheme {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -374,16 +382,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 +402,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 +447,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 +511,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 +537,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 +620,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 +704,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
@@ -642,6 +714,7 @@ fun MainContent(
Scaffold( Scaffold(
topBar = { topBar = {
Box {
TopAppBar( TopAppBar(
title = { Text("LBJ Console") }, title = { Text("LBJ Console") },
actions = { actions = {
@@ -673,6 +746,51 @@ fun MainContent(
} }
} }
) )
if (historyEditMode && currentTab == 0) {
TopAppBar(
title = {
Text(
"已选择 ${historySelectedRecords.size} 条记录",
color = MaterialTheme.colorScheme.onPrimary
)
},
navigationIcon = {
IconButton(onClick = {
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
}) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "取消",
tint = MaterialTheme.colorScheme.onPrimary
)
}
},
actions = {
IconButton(
onClick = {
if (historySelectedRecords.isNotEmpty()) {
val recordsToDelete = allRecords.filter {
historySelectedRecords.contains(it.timestamp.time.toString())
}
onDeleteRecords(recordsToDelete)
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
}
}
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "删除所选记录",
tint = MaterialTheme.colorScheme.onPrimary
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary
)
)
}
}
}, },
bottomBar = { bottomBar = {
NavigationBar { NavigationBar {
@@ -712,7 +830,6 @@ fun MainContent(
temporaryStatusMessage = temporaryStatusMessage, temporaryStatusMessage = temporaryStatusMessage,
locoInfoUtil = locoInfoUtil, locoInfoUtil = locoInfoUtil,
onClearRecords = onClearRecords, onClearRecords = onClearRecords,
onRecordClick = onRecordClick, onRecordClick = onRecordClick,
onClearLog = onClearMonitorLog, onClearLog = onClearMonitorLog,
onDeleteRecords = onDeleteRecords, onDeleteRecords = onDeleteRecords,

View File

@@ -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)
} }
} }
} }
@@ -94,11 +95,6 @@ fun HistoryScreen(
records records
} }
fun exitEditMode() {
isInEditMode = false
selectedRecordsList.clear()
onStateChange(false, emptySet(), expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
}
LaunchedEffect(isInEditMode, selectedRecordsList.size) { LaunchedEffect(isInEditMode, selectedRecordsList.size) {
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet() val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
@@ -114,7 +110,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)
} }
@@ -122,7 +117,8 @@ fun HistoryScreen(
LaunchedEffect(selectedRecordsList.size) { LaunchedEffect(selectedRecordsList.size) {
if (selectedRecordsList.isEmpty() && isInEditMode) { if (selectedRecordsList.isEmpty() && isInEditMode) {
exitEditMode() isInEditMode = false
onStateChange(false, emptySet(), expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
} }
} }
@@ -217,11 +213,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 +253,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,6 +291,22 @@ fun HistoryScreen(
} }
} }
} }
}
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 -> ""
}
if (formattedInfo.isNotEmpty() && formattedInfo != "<NUL>") { if (formattedInfo.isNotEmpty() && formattedInfo != "<NUL>") {
Text( Text(
@@ -294,25 +317,6 @@ fun HistoryScreen(
} }
} }
Text(
text = "${record.rssi} dBm",
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(4.dp))
if (recordMap.containsKey("time")) {
recordMap["time"]?.split("\n")?.forEach { timeLine ->
Text(
text = timeLine,
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row( Row(
@@ -537,57 +541,4 @@ fun HistoryScreen(
} }
} }
if (isInEditMode) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.background(MaterialTheme.colorScheme.primary)
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
IconButton(onClick = { exitEditMode() }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "取消",
tint = MaterialTheme.colorScheme.onPrimary
)
}
Text(
"已选择 ${selectedRecordsList.size} 条记录",
color = MaterialTheme.colorScheme.onPrimary
)
}
IconButton(
onClick = {
if (selectedRecordsList.isNotEmpty()) {
onDeleteRecords(selectedRecordsList.toList())
exitEditMode()
}
}
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "删除所选记录",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
}
}
}
} }

View File

@@ -306,7 +306,7 @@ fun MapScreen(
val locationProvider = GpsMyLocationProvider(ctx).apply { val locationProvider = GpsMyLocationProvider(ctx).apply {
locationUpdateMinDistance = 10f locationUpdateMinDistance = 10f
locationUpdateMinTime = 1000 locationUpdateMinTime = 5000
} }

View File

@@ -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)
} }
} }
} }