From c8ab5f7ff877634f3288d08b26e49b5ba9ce88da Mon Sep 17 00:00:00 2001 From: Nedifinita Date: Sat, 26 Jul 2025 00:40:45 +0800 Subject: [PATCH] feat: add LBJ message notification --- app/src/main/AndroidManifest.xml | 1 + .../org/noxylva/lbjconsole/MainActivity.kt | 8 + .../noxylva/lbjconsole/NotificationService.kt | 162 ++++++++++++++++++ .../lbjconsole/ui/screens/SettingsScreen.kt | 32 ++++ app/src/main/res/drawable/ic_notification.xml | 10 ++ 5 files changed, 213 insertions(+) create mode 100644 app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt create mode 100644 app/src/main/res/drawable/ic_notification.xml 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