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.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"/>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
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.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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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