feat: add custom train information notification layout

This commit is contained in:
Nedifinita
2025-07-26 17:31:09 +08:00
parent cd3128c24b
commit be8dc6bc72
4 changed files with 218 additions and 29 deletions

View File

@@ -275,6 +275,33 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
fun isConnected(): Boolean { fun isConnected(): Boolean {
return isConnected return isConnected
} }
@SuppressLint("MissingPermission")
fun checkActualConnectionState(): Boolean {
bluetoothGatt?.let { gatt ->
try {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val connectedDevices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
val isActuallyConnected = connectedDevices.any { it.address == deviceAddress }
if (isActuallyConnected && !isConnected) {
Log.d(TAG, "Found existing GATT connection, updating internal state")
isConnected = true
return true
} else if (!isActuallyConnected && isConnected) {
Log.d(TAG, "GATT connection lost, updating internal state")
isConnected = false
return false
}
return isActuallyConnected
} catch (e: Exception) {
Log.e(TAG, "Error checking actual connection state: ${e.message}")
return isConnected
}
}
return isConnected
}
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")

View File

@@ -825,12 +825,20 @@ class MainActivity : ComponentActivity() {
bleClient.setHighFrequencyReconnect(true) bleClient.setHighFrequencyReconnect(true)
if (hasBluetoothPermissions() && !bleClient.isConnected() && autoConnectEnabled) { if (hasBluetoothPermissions()) {
Log.d(TAG, "App resumed and not connected, starting auto scan") val actuallyConnected = bleClient.checkActualConnectionState()
startAutoScanAndConnect()
} else if (bleClient.isConnected()) { if (actuallyConnected) {
showDisconnectButton = true showDisconnectButton = true
deviceStatus = "已连接" deviceStatus = "已连接"
Log.d(TAG, "App resumed - connection verified")
} else if (autoConnectEnabled) {
Log.d(TAG, "App resumed and not connected, starting auto scan")
startAutoScanAndConnect()
} else {
deviceStatus = "未连接"
showDisconnectButton = false
}
} }
} }

View File

@@ -8,11 +8,14 @@ import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import org.json.JSONObject import org.json.JSONObject
import org.noxylva.lbjconsole.model.TrainRecord import org.noxylva.lbjconsole.model.TrainRecord
class NotificationService(private val context: Context) { class NotificationService(private val context: Context) {
companion object { companion object {
const val TAG = "NotificationService" const val TAG = "NotificationService"
@@ -86,12 +89,7 @@ class NotificationService(private val context: Context) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
val directionText = when (trainRecord.direction) { val remoteViews = RemoteViews(context.packageName, R.layout.notification_train_record)
1 -> "下行"
3 -> "上行"
else -> "未知"
}
val trainDisplay = if (isValidValue(trainRecord.lbjClass) && isValidValue(trainRecord.train)) { val trainDisplay = if (isValidValue(trainRecord.lbjClass) && isValidValue(trainRecord.train)) {
"${trainRecord.lbjClass.trim()}${trainRecord.train.trim()}" "${trainRecord.lbjClass.trim()}${trainRecord.train.trim()}"
} else if (isValidValue(trainRecord.lbjClass)) { } else if (isValidValue(trainRecord.lbjClass)) {
@@ -99,26 +97,83 @@ class NotificationService(private val context: Context) {
} else if (isValidValue(trainRecord.train)) { } else if (isValidValue(trainRecord.train)) {
trainRecord.train.trim() trainRecord.train.trim()
} else "列车" } else "列车"
remoteViews.setTextViewText(R.id.notification_train_number, trainDisplay)
val title = trainDisplay
val content = buildString { val directionText = when (trainRecord.direction) {
append(directionText) 1 -> ""
if (isValidValue(trainRecord.route)) { 3 -> ""
append("\n线路: ${trainRecord.route.trim()}") else -> ""
}
if (isValidValue(trainRecord.speed)) {
append("\n速度: ${trainRecord.speed.trim()} km/h")
}
if (isValidValue(trainRecord.position)) {
append("\n位置: ${trainRecord.position.trim()} km")
}
} }
if (directionText.isNotEmpty()) {
remoteViews.setTextViewText(R.id.notification_direction, directionText)
remoteViews.setViewVisibility(R.id.notification_direction, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_direction, View.GONE)
}
val locoInfo = when {
isValidValue(trainRecord.locoType) && isValidValue(trainRecord.loco) -> {
val shortLoco = if (trainRecord.loco.length > 5) {
trainRecord.loco.takeLast(5)
} else {
trainRecord.loco
}
"${trainRecord.locoType}-${shortLoco}"
}
isValidValue(trainRecord.locoType) -> trainRecord.locoType
isValidValue(trainRecord.loco) -> trainRecord.loco
else -> ""
}
if (locoInfo.isNotEmpty()) {
remoteViews.setTextViewText(R.id.notification_loco_info, locoInfo)
remoteViews.setViewVisibility(R.id.notification_loco_info, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_loco_info, View.GONE)
}
if (isValidValue(trainRecord.route)) {
remoteViews.setTextViewText(R.id.notification_route, trainRecord.route.trim())
remoteViews.setViewVisibility(R.id.notification_route, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_route, View.GONE)
}
if (isValidValue(trainRecord.position)) {
remoteViews.setTextViewText(R.id.notification_position, "${trainRecord.position.trim()}K")
remoteViews.setViewVisibility(R.id.notification_position, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_position, View.GONE)
}
if (isValidValue(trainRecord.speed)) {
remoteViews.setTextViewText(R.id.notification_speed, "${trainRecord.speed.trim()} km/h")
remoteViews.setViewVisibility(R.id.notification_speed, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_speed, View.GONE)
}
remoteViews.setOnClickPendingIntent(R.id.notification_train_number, pendingIntent)
val summaryParts = mutableListOf<String>()
val routeAndDirection = when {
isValidValue(trainRecord.route) && directionText.isNotEmpty() -> "${trainRecord.route.trim()}${directionText}"
isValidValue(trainRecord.route) -> trainRecord.route.trim()
directionText.isNotEmpty() -> "${directionText}"
else -> null
}
routeAndDirection?.let { summaryParts.add(it) }
if (locoInfo.isNotEmpty()) summaryParts.add(locoInfo)
val summaryText = summaryParts.joinToString("")
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title) .setContentTitle(trainDisplay)
.setContentText(content) .setContentText(summaryText)
.setStyle(NotificationCompat.BigTextStyle().bigText(content)) .setCustomContentView(remoteViews)
.setCustomBigContentView(remoteViews)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
@@ -131,7 +186,7 @@ class NotificationService(private val context: Context) {
} }
notificationManager.notify(notificationId, notification) notificationManager.notify(notificationId, notification)
Log.d(TAG, "Notification sent for train: ${trainRecord.train}") Log.d(TAG, "Custom notification sent for train: ${trainRecord.train}")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to show notification: ${e.message}", e) Log.e(TAG, "Failed to show notification: ${e.message}", e)

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp"
android:background="@android:color/transparent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/notification_train_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="G1234"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/notification_direction"
android:layout_width="16dp"
android:layout_height="16dp"
android:text="下"
android:textSize="10sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:background="@android:color/black"
android:gravity="center"
android:visibility="gone" />
</LinearLayout>
<TextView
android:id="@+id/notification_loco_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CRH380D-1234"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/notification_route"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="京沪高铁"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/notification_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1234K"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<TextView
android:id="@+id/notification_speed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="300 km/h"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
</LinearLayout>