diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 344884f..733689f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,6 +13,7 @@
+
diff --git a/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt b/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt
index 06ade01..c38fef6 100644
--- a/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt
+++ b/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt
@@ -74,6 +74,7 @@ class MainActivity : ComponentActivity() {
private val bleClient by lazy { BLEClient(this) }
private val trainRecordManager by lazy { TrainRecordManager(this) }
private val locoInfoUtil by lazy { LocoInfoUtil(this) }
+ private val notificationService by lazy { NotificationService(this) }
private var deviceStatus by mutableStateOf("未连接")
@@ -203,6 +204,10 @@ class MainActivity : ComponentActivity() {
Manifest.permission.ACCESS_COARSE_LOCATION
))
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ permissions.add(Manifest.permission.POST_NOTIFICATIONS)
+ }
+
requestPermissions.launch(permissions.toTypedArray())
@@ -568,6 +573,9 @@ class MainActivity : ComponentActivity() {
val record = trainRecordManager.addRecord(jsonData)
Log.d(TAG, "Added record train=${record.train} direction=${record.direction}")
+ if (notificationService.isNotificationEnabled()) {
+ notificationService.showTrainNotification(record)
+ }
latestRecord = record
diff --git a/app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt b/app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt
new file mode 100644
index 0000000..8f6157a
--- /dev/null
+++ b/app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt
@@ -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 != "" &&
+ trimmed != "NA" &&
+ trimmed != "" &&
+ !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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt
index 8663481..7d592ad 100644
--- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt
+++ b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt
@@ -20,6 +20,7 @@ import org.noxylva.lbjconsole.model.GroupBy
import org.noxylva.lbjconsole.model.TimeWindow
import org.noxylva.lbjconsole.SettingsActivity
import org.noxylva.lbjconsole.BackgroundService
+import org.noxylva.lbjconsole.NotificationService
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.LaunchedEffect
@@ -199,6 +200,11 @@ fun SettingsScreen(
mutableStateOf(SettingsActivity.isBackgroundServiceEnabled(context))
}
+ val notificationService = remember { NotificationService(context) }
+ var notificationEnabled by remember {
+ mutableStateOf(notificationService.isNotificationEnabled())
+ }
+
Row(
modifier = Modifier.fillMaxWidth(),
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)
+ }
+ )
+ }
}
}
diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..1c4ea0a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file