feat: add LBJ message notification
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<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"/>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
162
app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt
Normal file
162
app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
app/src/main/res/drawable/ic_notification.xml
Normal file
10
app/src/main/res/drawable/ic_notification.xml
Normal 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>
|
||||
Reference in New Issue
Block a user