diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5ecbc5a..93d9afc 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -81,7 +81,7 @@ dependencies {
debugImplementation(libs.androidx.ui.test.manifest)
implementation("org.json:json:20231013")
implementation("androidx.compose.material:material-icons-extended:1.5.4")
-
+ implementation("androidx.appcompat:appcompat:1.6.1")
implementation("org.osmdroid:osmdroid-android:6.1.16")
implementation("org.osmdroid:osmdroid-mapsforge:6.1.16")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8e5c4a7..344884f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,8 @@
+
+
@@ -22,14 +24,14 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@style/Theme.LBJReceiver"
+ android:theme="@style/Theme.LBJConsole"
android:usesCleartextTraffic="true"
tools:targetApi="31">
+ android:theme="@style/Theme.LBJConsole">
@@ -37,6 +39,19 @@
+
+
+
+
if (autoReconnect) {
@@ -819,6 +820,7 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
}
}
bluetoothGatt = null
+ bluetoothLeScanner = null
deviceAddress = null
connectionAttempts = 0
diff --git a/app/src/main/java/org/noxylva/lbjconsole/BackgroundService.kt b/app/src/main/java/org/noxylva/lbjconsole/BackgroundService.kt
new file mode 100644
index 0000000..3b1da88
--- /dev/null
+++ b/app/src/main/java/org/noxylva/lbjconsole/BackgroundService.kt
@@ -0,0 +1,123 @@
+package org.noxylva.lbjconsole
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.os.PowerManager
+import androidx.core.app.NotificationCompat
+
+class BackgroundService : Service() {
+
+ companion object {
+ private const val NOTIFICATION_ID = 1001
+ private const val CHANNEL_ID = "background_service_channel"
+ private const val CHANNEL_NAME = "Background Service"
+
+ fun startService(context: Context) {
+ try {
+ val intent = Intent(context, BackgroundService::class.java)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ } catch (e: Exception) {
+ // Service start failed, ignore silently
+ }
+ }
+
+ fun stopService(context: Context) {
+ val intent = Intent(context, BackgroundService::class.java)
+ context.stopService(intent)
+ }
+ }
+
+ private var wakeLock: PowerManager.WakeLock? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ acquireWakeLock()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ try {
+ val notification = createNotification()
+ startForeground(NOTIFICATION_ID, notification)
+ } catch (e: Exception) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ stopSelf()
+ return START_NOT_STICKY
+ }
+ }
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ releaseWakeLock()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "Keep app running in background"
+ setShowBadge(false)
+ }
+
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun createNotification(): Notification {
+ val intent = Intent(this, MainActivity::class.java)
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ return NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("LBJ Console")
+ .setContentText("Running in background")
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .build()
+ }
+
+ private fun acquireWakeLock() {
+ val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
+ wakeLock = powerManager.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "LBJConsole::BackgroundWakeLock"
+ )
+ wakeLock?.acquire()
+ }
+
+ private fun releaseWakeLock() {
+ wakeLock?.let {
+ if (it.isHeld) {
+ it.release()
+ }
+ }
+ wakeLock = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt b/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt
index a9e03d7..06ade01 100644
--- a/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt
+++ b/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt
@@ -47,6 +47,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
+import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import kotlinx.coroutines.delay
@@ -61,7 +62,7 @@ import org.noxylva.lbjconsole.ui.screens.HistoryScreen
import org.noxylva.lbjconsole.ui.screens.MapScreen
import org.noxylva.lbjconsole.ui.screens.SettingsScreen
-import org.noxylva.lbjconsole.ui.theme.LBJReceiverTheme
+import org.noxylva.lbjconsole.ui.theme.LBJConsoleTheme
import org.noxylva.lbjconsole.util.LocoInfoUtil
import java.util.*
import androidx.lifecycle.lifecycleScope
@@ -256,7 +257,7 @@ class MainActivity : ComponentActivity() {
tileDownloadThreads = 4
tileFileSystemThreads = 4
- setUserAgentValue("LBJReceiver/1.0")
+ setUserAgentValue("LBJConsole/1.0")
}
Log.d(TAG, "OSM cache configured")
@@ -266,13 +267,17 @@ class MainActivity : ComponentActivity() {
saveSettings()
+ if (SettingsActivity.isBackgroundServiceEnabled(this)) {
+ BackgroundService.startService(this)
+ }
+
enableEdgeToEdge()
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = false
}
setContent {
- LBJReceiverTheme {
+ LBJConsoleTheme {
val scope = rememberCoroutineScope()
Surface(modifier = Modifier.fillMaxSize()) {
@@ -406,7 +411,11 @@ class MainActivity : ComponentActivity() {
Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}")
},
appVersion = getAppVersion(),
- locoInfoUtil = locoInfoUtil
+ locoInfoUtil = locoInfoUtil,
+ onOpenSettings = {
+ val intent = Intent(this@MainActivity, SettingsActivity::class.java)
+ startActivity(intent)
+ }
)
if (showConnectionDialog) {
@@ -879,7 +888,9 @@ fun MainContent(
mapCenterPosition: Pair?,
mapZoomLevel: Double,
mapRailwayLayerVisible: Boolean,
- onMapStateChange: (Pair?, Double, Boolean) -> Unit
+ onMapStateChange: (Pair?, Double, Boolean) -> Unit,
+
+ onOpenSettings: () -> Unit
) {
val statusColor = if (isConnected) Color(0xFF4CAF50) else Color(0xFFFF5722)
diff --git a/app/src/main/java/org/noxylva/lbjconsole/SettingsActivity.kt b/app/src/main/java/org/noxylva/lbjconsole/SettingsActivity.kt
new file mode 100644
index 0000000..4c9f564
--- /dev/null
+++ b/app/src/main/java/org/noxylva/lbjconsole/SettingsActivity.kt
@@ -0,0 +1,63 @@
+package org.noxylva.lbjconsole
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.widget.Switch
+import androidx.appcompat.app.AppCompatActivity
+
+class SettingsActivity : AppCompatActivity() {
+
+ companion object {
+ private const val PREFS_NAME = "lbj_console_settings"
+ private const val KEY_BACKGROUND_SERVICE = "background_service_enabled"
+
+ fun isBackgroundServiceEnabled(context: Context): Boolean {
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ return prefs.getBoolean(KEY_BACKGROUND_SERVICE, false)
+ }
+
+ fun setBackgroundServiceEnabled(context: Context, enabled: Boolean) {
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ prefs.edit().putBoolean(KEY_BACKGROUND_SERVICE, enabled).apply()
+ }
+ }
+
+ private lateinit var backgroundServiceSwitch: Switch
+ private lateinit var prefs: SharedPreferences
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_settings)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.title = "Settings"
+
+ prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+
+ initViews()
+ setupListeners()
+ }
+
+ private fun initViews() {
+ backgroundServiceSwitch = findViewById(R.id.switch_background_service)
+ backgroundServiceSwitch.isChecked = isBackgroundServiceEnabled(this)
+ }
+
+ private fun setupListeners() {
+ backgroundServiceSwitch.setOnCheckedChangeListener { _, isChecked ->
+ setBackgroundServiceEnabled(this, isChecked)
+
+ if (isChecked) {
+ BackgroundService.startService(this)
+ } else {
+ BackgroundService.stopService(this)
+ }
+ }
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ onBackPressed()
+ return true
+ }
+}
\ 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 db82a13..8663481 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
@@ -10,6 +10,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -17,6 +18,8 @@ import androidx.compose.ui.unit.dp
import org.noxylva.lbjconsole.model.MergeSettings
import org.noxylva.lbjconsole.model.GroupBy
import org.noxylva.lbjconsole.model.TimeWindow
+import org.noxylva.lbjconsole.SettingsActivity
+import org.noxylva.lbjconsole.BackgroundService
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.LaunchedEffect
@@ -164,6 +167,72 @@ fun SettingsScreen(
}
}
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Settings,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ "应用设置",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+
+ val context = LocalContext.current
+ var backgroundServiceEnabled by remember {
+ mutableStateOf(SettingsActivity.isBackgroundServiceEnabled(context))
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ "后台保活服务",
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Medium
+ )
+ Text(
+ "保持应用在后台运行",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Switch(
+ checked = backgroundServiceEnabled,
+ onCheckedChange = { enabled ->
+ backgroundServiceEnabled = enabled
+ SettingsActivity.setBackgroundServiceEnabled(context, enabled)
+
+ if (enabled) {
+ BackgroundService.startService(context)
+ } else {
+ BackgroundService.stopService(context)
+ }
+ }
+ )
+ }
+ }
+ }
+
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/theme/Theme.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/theme/Theme.kt
index 63e93b2..ffb8c63 100644
--- a/app/src/main/java/org/noxylva/lbjconsole/ui/theme/Theme.kt
+++ b/app/src/main/java/org/noxylva/lbjconsole/ui/theme/Theme.kt
@@ -24,7 +24,7 @@ private val LightColorScheme = lightColorScheme(
)
@Composable
-fun LBJReceiverTheme(
+fun LBJConsoleTheme(
darkTheme: Boolean = true,
dynamicColor: Boolean = true,
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
new file mode 100644
index 0000000..e6718ef
--- /dev/null
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 08a9d46..59712bc 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file