feat: add LBJ message notification

This commit is contained in:
Nedifinita
2025-07-26 00:40:45 +08:00
parent e1d02a8a55
commit c8ab5f7ff8
5 changed files with 213 additions and 0 deletions

View File

@@ -13,6 +13,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

View File

@@ -74,6 +74,7 @@ class MainActivity : ComponentActivity() {
private val bleClient by lazy { BLEClient(this) } private val bleClient by lazy { BLEClient(this) }
private val trainRecordManager by lazy { TrainRecordManager(this) } private val trainRecordManager by lazy { TrainRecordManager(this) }
private val locoInfoUtil by lazy { LocoInfoUtil(this) } private val locoInfoUtil by lazy { LocoInfoUtil(this) }
private val notificationService by lazy { NotificationService(this) }
private var deviceStatus by mutableStateOf("未连接") private var deviceStatus by mutableStateOf("未连接")
@@ -203,6 +204,10 @@ class MainActivity : ComponentActivity() {
Manifest.permission.ACCESS_COARSE_LOCATION Manifest.permission.ACCESS_COARSE_LOCATION
)) ))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
}
requestPermissions.launch(permissions.toTypedArray()) requestPermissions.launch(permissions.toTypedArray())
@@ -568,6 +573,9 @@ class MainActivity : ComponentActivity() {
val record = trainRecordManager.addRecord(jsonData) val record = trainRecordManager.addRecord(jsonData)
Log.d(TAG, "Added record train=${record.train} direction=${record.direction}") Log.d(TAG, "Added record train=${record.train} direction=${record.direction}")
if (notificationService.isNotificationEnabled()) {
notificationService.showTrainNotification(record)
}
latestRecord = record latestRecord = record

View File

@@ -0,0 +1,162 @@
package org.noxylva.lbjconsole
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import org.json.JSONObject
import org.noxylva.lbjconsole.model.TrainRecord
class NotificationService(private val context: Context) {
companion object {
const val TAG = "NotificationService"
const val CHANNEL_ID = "lbj_messages"
const val CHANNEL_NAME = "LBJ Messages"
const val NOTIFICATION_ID_BASE = 2000
const val PREFS_NAME = "notification_settings"
const val KEY_ENABLED = "notifications_enabled"
}
private val notificationManager = NotificationManagerCompat.from(context)
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private var notificationIdCounter = NOTIFICATION_ID_BASE
init {
createNotificationChannel()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Real-time LBJ train message notifications"
enableVibration(true)
setShowBadge(true)
}
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
Log.d(TAG, "Notification channel created")
}
}
fun isNotificationEnabled(): Boolean {
return prefs.getBoolean(KEY_ENABLED, false)
}
fun setNotificationEnabled(enabled: Boolean) {
prefs.edit().putBoolean(KEY_ENABLED, enabled).apply()
Log.d(TAG, "Notification enabled set to: $enabled")
}
private fun isValidValue(value: String): Boolean {
val trimmed = value.trim()
return trimmed.isNotEmpty() &&
trimmed != "NUL" &&
trimmed != "<NUL>" &&
trimmed != "NA" &&
trimmed != "<NA>" &&
!trimmed.all { it == '*' }
}
fun showTrainNotification(trainRecord: TrainRecord) {
if (!isNotificationEnabled()) {
Log.d(TAG, "Notifications disabled, skipping")
return
}
try {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val directionText = when (trainRecord.direction) {
1 -> "下行"
3 -> "上行"
else -> "未知"
}
val trainDisplay = if (isValidValue(trainRecord.lbjClass) && isValidValue(trainRecord.train)) {
"${trainRecord.lbjClass.trim()}${trainRecord.train.trim()}"
} else if (isValidValue(trainRecord.lbjClass)) {
trainRecord.lbjClass.trim()
} else if (isValidValue(trainRecord.train)) {
trainRecord.train.trim()
} else "列车"
val title = trainDisplay
val content = buildString {
append(directionText)
if (isValidValue(trainRecord.route)) {
append("\n线路: ${trainRecord.route.trim()}")
}
if (isValidValue(trainRecord.speed)) {
append("\n速度: ${trainRecord.speed.trim()} km/h")
}
if (isValidValue(trainRecord.position)) {
append("\n位置: ${trainRecord.position.trim()} km")
}
}
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(content)
.setStyle(NotificationCompat.BigTextStyle().bigText(content))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setWhen(trainRecord.timestamp.time)
.build()
val notificationId = notificationIdCounter++
if (notificationIdCounter > NOTIFICATION_ID_BASE + 1000) {
notificationIdCounter = NOTIFICATION_ID_BASE
}
notificationManager.notify(notificationId, notification)
Log.d(TAG, "Notification sent for train: ${trainRecord.train}")
} catch (e: Exception) {
Log.e(TAG, "Failed to show notification: ${e.message}", e)
}
}
fun showTrainNotification(jsonData: JSONObject) {
if (!isNotificationEnabled()) {
Log.d(TAG, "Notifications disabled, skipping")
return
}
try {
val trainRecord = TrainRecord(jsonData)
showTrainNotification(trainRecord)
} catch (e: Exception) {
Log.e(TAG, "Failed to create TrainRecord from JSON: ${e.message}", e)
}
}
fun hasNotificationPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationManager.areNotificationsEnabled()
} else {
notificationManager.areNotificationsEnabled()
}
}
}

View File

@@ -20,6 +20,7 @@ import org.noxylva.lbjconsole.model.GroupBy
import org.noxylva.lbjconsole.model.TimeWindow import org.noxylva.lbjconsole.model.TimeWindow
import org.noxylva.lbjconsole.SettingsActivity import org.noxylva.lbjconsole.SettingsActivity
import org.noxylva.lbjconsole.BackgroundService import org.noxylva.lbjconsole.BackgroundService
import org.noxylva.lbjconsole.NotificationService
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -199,6 +200,11 @@ fun SettingsScreen(
mutableStateOf(SettingsActivity.isBackgroundServiceEnabled(context)) mutableStateOf(SettingsActivity.isBackgroundServiceEnabled(context))
} }
val notificationService = remember { NotificationService(context) }
var notificationEnabled by remember {
mutableStateOf(notificationService.isNotificationEnabled())
}
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -230,6 +236,32 @@ fun SettingsScreen(
} }
) )
} }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
"LBJ消息通知",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Text(
"实时接收列车LBJ消息通知",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = notificationEnabled,
onCheckedChange = { enabled ->
notificationEnabled = enabled
notificationService.setNotificationEnabled(enabled)
}
)
}
} }
} }

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09C6.04,10.33 6,10.66 6,11v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>