feat: add BLE disconnection cleanup and enhance record management
This commit is contained in:
@@ -633,4 +633,23 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
||||
fun getConnectionAttempts(): Int = connectionAttempts
|
||||
|
||||
fun getLastKnownDeviceAddress(): String? = lastKnownDeviceAddress
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun disconnectAndCleanup() {
|
||||
isConnected = false
|
||||
autoReconnect = false
|
||||
bluetoothGatt?.let { gatt ->
|
||||
try {
|
||||
gatt.disconnect()
|
||||
gatt.close()
|
||||
Log.d(TAG, "GATT connection cleaned up")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Cleanup error: ${e.message}")
|
||||
}
|
||||
}
|
||||
bluetoothGatt = null
|
||||
deviceAddress = null
|
||||
connectionAttempts = 0
|
||||
Log.d(TAG, "BLE client fully disconnected and cleaned up")
|
||||
}
|
||||
}
|
||||
@@ -274,6 +274,8 @@ class MainActivity : ComponentActivity() {
|
||||
onMergeSettingsChange = { newSettings ->
|
||||
mergeSettings = newSettings
|
||||
trainRecordManager.updateMergeSettings(newSettings)
|
||||
historyEditMode = false
|
||||
historySelectedRecords = emptySet()
|
||||
saveSettings()
|
||||
},
|
||||
|
||||
@@ -334,12 +336,6 @@ class MainActivity : ComponentActivity() {
|
||||
scope.launch {
|
||||
val deletedCount = trainRecordManager.deleteRecords(records)
|
||||
if (deletedCount > 0) {
|
||||
Toast.makeText(
|
||||
this@MainActivity,
|
||||
"已删除 $deletedCount 条记录",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
if (records.contains(latestRecord)) {
|
||||
latestRecord = null
|
||||
}
|
||||
@@ -659,6 +655,10 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
saveSettings()
|
||||
if (isFinishing) {
|
||||
bleClient.disconnectAndCleanup()
|
||||
Log.d(TAG, "App finishing, BLE cleaned up")
|
||||
}
|
||||
Log.d(TAG, "App paused, settings saved")
|
||||
}
|
||||
}
|
||||
@@ -783,8 +783,24 @@ fun MainContent(
|
||||
if (historyEditMode && currentTab == 0) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
val totalSelectedCount = historySelectedRecords.sumOf { selectedId ->
|
||||
allRecords.find { item ->
|
||||
when (item) {
|
||||
is TrainRecord -> item.uniqueId == selectedId
|
||||
is org.noxylva.lbjconsole.model.MergedTrainRecord ->
|
||||
item.records.any { it.uniqueId == selectedId }
|
||||
else -> false
|
||||
}
|
||||
}?.let { item ->
|
||||
when (item) {
|
||||
is TrainRecord -> 1
|
||||
is org.noxylva.lbjconsole.model.MergedTrainRecord -> item.records.size
|
||||
else -> 0
|
||||
}
|
||||
} ?: 0
|
||||
}
|
||||
Text(
|
||||
"已选择 ${historySelectedRecords.size} 条记录",
|
||||
"已选择 $totalSelectedCount 条记录",
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
},
|
||||
@@ -803,24 +819,39 @@ fun MainContent(
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (historySelectedRecords.isNotEmpty()) {
|
||||
val recordsToDelete = mutableListOf<TrainRecord>()
|
||||
val recordsToDelete = mutableSetOf<TrainRecord>()
|
||||
val idToRecordMap = mutableMapOf<String, TrainRecord>()
|
||||
val idToMergedRecordMap = mutableMapOf<String, org.noxylva.lbjconsole.model.MergedTrainRecord>()
|
||||
|
||||
allRecords.forEach { item ->
|
||||
when (item) {
|
||||
is TrainRecord -> {
|
||||
if (historySelectedRecords.contains(item.timestamp.time.toString())) {
|
||||
recordsToDelete.add(item)
|
||||
}
|
||||
idToRecordMap[item.uniqueId] = item
|
||||
}
|
||||
is org.noxylva.lbjconsole.model.MergedTrainRecord -> {
|
||||
item.records.forEach { record ->
|
||||
if (historySelectedRecords.contains(record.timestamp.time.toString())) {
|
||||
recordsToDelete.add(record)
|
||||
}
|
||||
idToRecordMap[record.uniqueId] = record
|
||||
idToMergedRecordMap[record.uniqueId] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onDeleteRecords(recordsToDelete)
|
||||
|
||||
val processedMergedRecords = mutableSetOf<org.noxylva.lbjconsole.model.MergedTrainRecord>()
|
||||
|
||||
historySelectedRecords.forEach { selectedId ->
|
||||
val mergedRecord = idToMergedRecordMap[selectedId]
|
||||
if (mergedRecord != null && !processedMergedRecords.contains(mergedRecord)) {
|
||||
recordsToDelete.addAll(mergedRecord.records)
|
||||
processedMergedRecords.add(mergedRecord)
|
||||
} else if (mergedRecord == null) {
|
||||
idToRecordMap[selectedId]?.let { record ->
|
||||
recordsToDelete.add(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDeleteRecords(recordsToDelete.toList())
|
||||
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
|
||||
}
|
||||
}
|
||||
@@ -902,7 +933,16 @@ fun MainContent(
|
||||
)
|
||||
3 -> MapScreen(
|
||||
records = if (allRecords.isNotEmpty()) {
|
||||
allRecords.filterIsInstance<TrainRecord>()
|
||||
val trainRecords = mutableListOf<TrainRecord>()
|
||||
allRecords.forEach { item ->
|
||||
when (item) {
|
||||
is TrainRecord -> trainRecords.add(item)
|
||||
is org.noxylva.lbjconsole.model.MergedTrainRecord -> {
|
||||
trainRecords.addAll(item.records)
|
||||
}
|
||||
}
|
||||
}
|
||||
trainRecords
|
||||
} else {
|
||||
recentRecords
|
||||
},
|
||||
|
||||
@@ -9,8 +9,15 @@ import org.noxylva.lbjconsole.util.LocationUtils
|
||||
class TrainRecord(jsonData: JSONObject? = null) {
|
||||
companion object {
|
||||
const val TAG = "TrainRecord"
|
||||
private var nextId = 0L
|
||||
|
||||
@Synchronized
|
||||
private fun generateUniqueId(): String {
|
||||
return "${System.currentTimeMillis()}_${++nextId}"
|
||||
}
|
||||
}
|
||||
|
||||
val uniqueId: String
|
||||
var timestamp: Date = Date()
|
||||
var receivedTimestamp: Date = Date()
|
||||
var train: String = ""
|
||||
@@ -29,10 +36,15 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
||||
private var _coordinates: GeoPoint? = null
|
||||
|
||||
init {
|
||||
uniqueId = if (jsonData?.has("uniqueId") == true) {
|
||||
jsonData.getString("uniqueId")
|
||||
} else {
|
||||
generateUniqueId()
|
||||
}
|
||||
|
||||
jsonData?.let {
|
||||
try {
|
||||
if (jsonData.has("timestamp")) {
|
||||
|
||||
timestamp = Date(jsonData.getLong("timestamp"))
|
||||
}
|
||||
|
||||
@@ -166,6 +178,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
||||
|
||||
fun toJSON(): JSONObject {
|
||||
val json = JSONObject()
|
||||
json.put("uniqueId", uniqueId)
|
||||
json.put("timestamp", timestamp.time)
|
||||
json.put("receivedTimestamp", receivedTimestamp.time)
|
||||
json.put("train", train)
|
||||
@@ -181,4 +194,14 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
||||
json.put("rssi", rssi)
|
||||
return json
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is TrainRecord) return false
|
||||
return uniqueId == other.uniqueId
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return uniqueId.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -207,11 +207,11 @@ class TrainRecordManager(private val context: Context) {
|
||||
val mergedRecords = processRecordsForMerging(allRecords, mergeSettings)
|
||||
|
||||
val mergedRecordIds = mergedRecords.flatMap { merged ->
|
||||
merged.records.map { it.timestamp.time.toString() }
|
||||
merged.records.map { it.uniqueId }
|
||||
}.toSet()
|
||||
|
||||
val singleRecords = allRecords.filter { record ->
|
||||
!mergedRecordIds.contains(record.timestamp.time.toString())
|
||||
!mergedRecordIds.contains(record.uniqueId)
|
||||
}
|
||||
|
||||
val mixedList = mutableListOf<Any>()
|
||||
|
||||
@@ -74,7 +74,7 @@ fun TrainRecordItem(
|
||||
if (isInEditMode) {
|
||||
onToggleSelection(record)
|
||||
} else {
|
||||
val id = record.timestamp.time.toString()
|
||||
val id = record.uniqueId
|
||||
expandedStatesMap[id] = !(expandedStatesMap[id] ?: false)
|
||||
if (record == latestRecord) {
|
||||
onRecordClick(record)
|
||||
@@ -91,7 +91,7 @@ fun TrainRecordItem(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
val recordId = record.timestamp.time.toString()
|
||||
val recordId = record.uniqueId
|
||||
val isExpanded = expandedStatesMap[recordId] == true
|
||||
val recordMap = record.toMap(showDetailedTime = true)
|
||||
|
||||
@@ -705,7 +705,7 @@ fun MergedTrainRecordItem(
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
mergedRecord.records.forEach { recordItem ->
|
||||
mergedRecord.records.filter { it != mergedRecord.latestRecord }.forEach { recordItem ->
|
||||
val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
|
||||
Column(
|
||||
@@ -834,13 +834,13 @@ fun HistoryScreen(
|
||||
records.forEach { item ->
|
||||
when (item) {
|
||||
is TrainRecord -> {
|
||||
if (selectedRecords.contains(item.timestamp.time.toString())) {
|
||||
if (selectedRecords.contains(item.uniqueId)) {
|
||||
add(item)
|
||||
}
|
||||
}
|
||||
is MergedTrainRecord -> {
|
||||
item.records.forEach { record ->
|
||||
if (selectedRecords.contains(record.timestamp.time.toString())) {
|
||||
if (selectedRecords.contains(record.uniqueId)) {
|
||||
add(record)
|
||||
}
|
||||
}
|
||||
@@ -881,20 +881,21 @@ fun HistoryScreen(
|
||||
|
||||
|
||||
LaunchedEffect(isInEditMode, selectedRecordsList.size) {
|
||||
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
||||
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
|
||||
LaunchedEffect(expandedStatesMap.toMap()) {
|
||||
if (!isInEditMode) {
|
||||
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
||||
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
||||
delay(50)
|
||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
if (!isInEditMode) {
|
||||
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
||||
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
}
|
||||
@@ -911,7 +912,6 @@ fun HistoryScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.weight(1.0f)
|
||||
) {
|
||||
if (filteredRecords.isEmpty()) {
|
||||
@@ -945,7 +945,8 @@ fun HistoryScreen(
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp)
|
||||
) {
|
||||
items(filteredRecords) { item ->
|
||||
when (item) {
|
||||
|
||||
@@ -99,6 +99,49 @@ fun MapScreen(
|
||||
|
||||
var railwayLayerVisibleState by remember(railwayLayerVisible) { mutableStateOf(railwayLayerVisible) }
|
||||
|
||||
fun updateMarkers() {
|
||||
val mapView = mapViewRef.value ?: return
|
||||
|
||||
mapView.overlays.removeAll { it is Marker }
|
||||
|
||||
validRecords.forEach { record ->
|
||||
record.getCoordinates()?.let { point ->
|
||||
val marker = Marker(mapView).apply {
|
||||
position = point
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
|
||||
val recordMap = record.toMap()
|
||||
title = recordMap["train"]?.toString() ?: "列车"
|
||||
|
||||
val latStr = String.format("%.4f", point.latitude)
|
||||
val lonStr = String.format("%.4f", point.longitude)
|
||||
val coordStr = "${latStr}°N, ${lonStr}°E"
|
||||
snippet = coordStr
|
||||
|
||||
setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
|
||||
|
||||
setOnMarkerClickListener { clickedMarker, _ ->
|
||||
selectedRecord = record
|
||||
dialogPosition = point
|
||||
showDetailDialog = true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
mapView.overlays.add(marker)
|
||||
marker.showInfoWindow()
|
||||
}
|
||||
}
|
||||
|
||||
mapView.invalidate()
|
||||
}
|
||||
|
||||
LaunchedEffect(records) {
|
||||
if (isMapInitialized) {
|
||||
updateMarkers()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
@@ -135,50 +178,7 @@ fun MapScreen(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateMarkers() {
|
||||
val mapView = mapViewRef.value ?: return
|
||||
|
||||
|
||||
mapView.overlays.removeAll { it is Marker }
|
||||
|
||||
|
||||
validRecords.forEach { record ->
|
||||
record.getCoordinates()?.let { point ->
|
||||
val marker = Marker(mapView).apply {
|
||||
position = point
|
||||
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
|
||||
val recordMap = record.toMap()
|
||||
title = recordMap["train"]?.toString() ?: "列车"
|
||||
|
||||
val latStr = String.format("%.4f", point.latitude)
|
||||
val lonStr = String.format("%.4f", point.longitude)
|
||||
val coordStr = "${latStr}°N, ${lonStr}°E"
|
||||
snippet = coordStr
|
||||
|
||||
setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
|
||||
|
||||
setOnMarkerClickListener { clickedMarker, _ ->
|
||||
selectedRecord = record
|
||||
dialogPosition = point
|
||||
showDetailDialog = true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
mapView.overlays.add(marker)
|
||||
marker.showInfoWindow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mapView.invalidate()
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun updateRailwayLayerVisibility(visible: Boolean) {
|
||||
|
||||
@@ -58,7 +58,7 @@ fun MergedHistoryScreen(
|
||||
val selectedRecordsList = remember(selectedRecords) {
|
||||
mutableStateListOf<TrainRecord>().apply {
|
||||
addAll(mergedRecords.flatMap { it.records }.filter {
|
||||
selectedRecords.contains(it.timestamp.time.toString())
|
||||
selectedRecords.contains(it.uniqueId)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ fun MergedHistoryScreen(
|
||||
)
|
||||
|
||||
LaunchedEffect(isInEditMode, selectedRecordsList.size) {
|
||||
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
||||
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(),
|
||||
listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
|
||||
@@ -227,8 +227,7 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
|
||||
Text(
|
||||
text = "LBJ Console v$appVersion by undef-i",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -240,7 +239,7 @@ fun SettingsScreen(
|
||||
.clickable {
|
||||
uriHandler.openUri("https://github.com/undef-i")
|
||||
}
|
||||
.padding(16.dp)
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user