fix: various issues
This commit is contained in:
114
.github/workflows/android-release.yml
vendored
Normal file
114
.github/workflows/android-release.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
name: Android Release Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Cache Gradle packages
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Build Debug APK
|
||||||
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
- name: Build Release APK
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
env:
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Upload Debug APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: debug-apk
|
||||||
|
path: app/build/outputs/apk/debug/*.apk
|
||||||
|
|
||||||
|
- name: Upload Release APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-apk
|
||||||
|
path: app/build/outputs/apk/release/*.apk
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
app/build/outputs/apk/debug/*.apk
|
||||||
|
app/build/outputs/apk/release/*.apk
|
||||||
|
generate_release_notes: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Cache Gradle packages
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: ./gradlew test
|
||||||
|
|
||||||
|
- name: Run lint
|
||||||
|
run: ./gradlew lint
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: |
|
||||||
|
app/build/reports/tests/
|
||||||
|
app/build/reports/lint-results.html
|
||||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,15 +1,15 @@
|
|||||||
*.iml
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
.idea/caches
|
||||||
/.idea/caches
|
.idea/libraries
|
||||||
/.idea/libraries
|
.idea/modules.xml
|
||||||
/.idea/modules.xml
|
.idea/workspace.xml
|
||||||
/.idea/workspace.xml
|
.idea/navEditor.xml
|
||||||
/.idea/navEditor.xml
|
.idea/assetWizardSettings.xml
|
||||||
/.idea/assetWizardSettings.xml
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
build
|
||||||
/captures
|
captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
|
local.properties
|
||||||
|
|||||||
2
.idea/.name
generated
2
.idea/.name
generated
@@ -1 +1 @@
|
|||||||
LBJ_Console
|
LBJ Receiver
|
||||||
4
LICENSE
4
LICENSE
@@ -631,7 +631,7 @@ to attach them to the start of each source file to most effectively
|
|||||||
state the exclusion of warranty; and each file should have at least
|
state the exclusion of warranty; and each file should have at least
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
LBJ_Console is an Android app designed to receive and display LBJ (Locomotive Bell Jingling) messages via BLE from the SX1276_Receive_LBJ device.
|
LBJ Console is an Android app designed to receive and display LBJ (Locomotive Bell Jingling) messages via BLE from the SX1276_Receive_LBJ device.
|
||||||
Copyright (C) 2025 undef-i
|
Copyright (C) 2025 undef-i
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|||||||
If the program does terminal interaction, make it output a short
|
If the program does terminal interaction, make it output a short
|
||||||
notice like this when it starts in an interactive mode:
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
LBJ_Console Copyright (C) 2025 undef-i
|
LBJ Console Copyright (C) 2025 undef-i
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it
|
This is free software, and you are welcome to redistribute it
|
||||||
under certain conditions; type `show c' for details.
|
under certain conditions; type `show c' for details.
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -1,6 +1,14 @@
|
|||||||
# LBJ_Console
|
# LBJ Console
|
||||||
|
|
||||||
LBJ_Console is an Android app designed to receive and display LBJ messages via BLE from the [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) device.
|
LBJ Console is an Android app designed to receive and display LBJ messages via BLE from the [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) device.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
- Tab state persistence
|
||||||
|
- Record filtering (train number, time range)
|
||||||
|
- Record management page optimization
|
||||||
|
- Optional train merge by locomotive/number
|
||||||
|
- Offline data storage
|
||||||
|
- Add record timestamps
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
|
|||||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -5,13 +5,13 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "receiver.lbj"
|
namespace = "org.noxylva.lbjconsole"
|
||||||
compileSdk = 35
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "receiver.lbj"
|
applicationId = "org.noxylva.lbjconsole"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package receiver.lbj
|
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("receiver.lbj", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,11 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:usesPermissionFlags="neverForLocation"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" android:usesPermissionFlags="neverForLocation"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
package receiver.lbj
|
package org.noxylva.lbjconsole
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.*
|
import android.bluetooth.*
|
||||||
|
import android.bluetooth.le.BluetoothLeScanner
|
||||||
|
import android.bluetooth.le.ScanCallback
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class BLEClient(private val context: Context) : BluetoothGattCallback(),
|
class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
||||||
BluetoothAdapter.LeScanCallback {
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "LBJ_BT"
|
const val TAG = "LBJ_BT"
|
||||||
const val SCAN_PERIOD = 10000L
|
const val SCAN_PERIOD = 10000L
|
||||||
@@ -35,6 +40,24 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
|
|||||||
private var trainInfoCallback: ((JSONObject) -> Unit)? = null
|
private var trainInfoCallback: ((JSONObject) -> Unit)? = null
|
||||||
private var handler = Handler(Looper.getMainLooper())
|
private var handler = Handler(Looper.getMainLooper())
|
||||||
private var targetDeviceName: String? = null
|
private var targetDeviceName: String? = null
|
||||||
|
private var bluetoothLeScanner: BluetoothLeScanner? = null
|
||||||
|
|
||||||
|
private val leScanCallback = object : ScanCallback() {
|
||||||
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
|
val device = result.device
|
||||||
|
val deviceName = device.name
|
||||||
|
if (targetDeviceName != null) {
|
||||||
|
if (deviceName == null || !deviceName.equals(targetDeviceName, ignoreCase = true)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scanCallback?.invoke(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScanFailed(errorCode: Int) {
|
||||||
|
Log.e(TAG, "BLE scan failed code=$errorCode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun setTrainInfoCallback(callback: (JSONObject) -> Unit) {
|
fun setTrainInfoCallback(callback: (JSONObject) -> Unit) {
|
||||||
@@ -42,8 +65,28 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun hasBluetoothPermissions(): Boolean {
|
||||||
|
val bluetoothPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
|
||||||
|
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else {
|
||||||
|
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED &&
|
||||||
|
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
val locationPermissions = ContextCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
||||||
|
ContextCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
return bluetoothPermissions && locationPermissions
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun scanDevices(targetDeviceName: String? = null, callback: (BluetoothDevice) -> Unit) {
|
fun scanDevices(targetDeviceName: String? = null, callback: (BluetoothDevice) -> Unit) {
|
||||||
|
if (!hasBluetoothPermissions()) {
|
||||||
|
Log.e(TAG, "Bluetooth permissions not granted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
scanCallback = callback
|
scanCallback = callback
|
||||||
this.targetDeviceName = targetDeviceName
|
this.targetDeviceName = targetDeviceName
|
||||||
@@ -57,13 +100,19 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
|
||||||
|
if (bluetoothLeScanner == null) {
|
||||||
|
Log.e(TAG, "BluetoothLeScanner unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handler.postDelayed({
|
handler.postDelayed({
|
||||||
stopScan()
|
stopScan()
|
||||||
}, SCAN_PERIOD)
|
}, SCAN_PERIOD)
|
||||||
|
|
||||||
isScanning = true
|
isScanning = true
|
||||||
Log.d(TAG, "Starting BLE scan target=${targetDeviceName ?: "Any"}")
|
Log.d(TAG, "Starting BLE scan target=${targetDeviceName ?: "Any"}")
|
||||||
bluetoothAdapter.startLeScan(this)
|
bluetoothLeScanner?.startScan(leScanCallback)
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.e(TAG, "Scan security error: ${e.message}")
|
Log.e(TAG, "Scan security error: ${e.message}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -75,28 +124,25 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun stopScan() {
|
fun stopScan() {
|
||||||
if (isScanning) {
|
if (isScanning) {
|
||||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
bluetoothLeScanner?.stopScan(leScanCallback)
|
||||||
bluetoothAdapter.stopLeScan(this)
|
|
||||||
isScanning = false
|
isScanning = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onLeScan(device: BluetoothDevice, rssi: Int, scanRecord: ByteArray) {
|
|
||||||
|
|
||||||
val deviceName = device.name
|
|
||||||
if (targetDeviceName != null) {
|
|
||||||
|
|
||||||
if (deviceName == null || !deviceName.equals(targetDeviceName, ignoreCase = true)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scanCallback?.invoke(device)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun connect(address: String, onConnectionStateChange: ((Boolean) -> Unit)? = null): Boolean {
|
fun connect(address: String, onConnectionStateChange: ((Boolean) -> Unit)? = null): Boolean {
|
||||||
|
Log.d(TAG, "Attempting to connect to device: $address")
|
||||||
|
|
||||||
|
if (!hasBluetoothPermissions()) {
|
||||||
|
Log.e(TAG, "Bluetooth permissions not granted")
|
||||||
|
handler.post { onConnectionStateChange?.invoke(false) }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (address.isBlank()) {
|
if (address.isBlank()) {
|
||||||
Log.e(TAG, "Connection failed empty address")
|
Log.e(TAG, "Connection failed empty address")
|
||||||
handler.post { onConnectionStateChange?.invoke(false) }
|
handler.post { onConnectionStateChange?.invoke(false) }
|
||||||
@@ -110,6 +156,18 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!bluetoothAdapter.isEnabled) {
|
||||||
|
Log.e(TAG, "Bluetooth adapter is disabled")
|
||||||
|
handler.post { onConnectionStateChange?.invoke(false) }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConnected) {
|
||||||
|
Log.w(TAG, "Already connected to device")
|
||||||
|
handler.post { onConnectionStateChange?.invoke(true) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bluetoothGatt?.close()
|
bluetoothGatt?.close()
|
||||||
bluetoothGatt = null
|
bluetoothGatt = null
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package receiver.lbj
|
package org.noxylva.lbjconsole
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
@@ -6,7 +6,7 @@ import android.bluetooth.BluetoothDevice
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import android.net.Uri
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -19,36 +19,30 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.LocationOn
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.osmdroid.config.Configuration
|
import org.osmdroid.config.Configuration
|
||||||
import receiver.lbj.model.TrainRecord
|
import org.noxylva.lbjconsole.model.TrainRecord
|
||||||
import receiver.lbj.model.TrainRecordManager
|
import org.noxylva.lbjconsole.model.TrainRecordManager
|
||||||
import receiver.lbj.ui.screens.HistoryScreen
|
import org.noxylva.lbjconsole.ui.screens.HistoryScreen
|
||||||
import receiver.lbj.ui.screens.MapScreen
|
import org.noxylva.lbjconsole.ui.screens.MapScreen
|
||||||
import receiver.lbj.ui.screens.SettingsScreen
|
import org.noxylva.lbjconsole.ui.screens.SettingsScreen
|
||||||
import receiver.lbj.ui.screens.MapScreen
|
import org.noxylva.lbjconsole.ui.theme.LBJReceiverTheme
|
||||||
import receiver.lbj.ui.theme.LBJReceiverTheme
|
import org.noxylva.lbjconsole.util.LocoInfoUtil
|
||||||
import receiver.lbj.util.LocoInfoUtil
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import android.bluetooth.le.ScanCallback
|
import android.bluetooth.le.ScanCallback
|
||||||
import android.bluetooth.le.ScanResult
|
import android.bluetooth.le.ScanResult
|
||||||
|
|
||||||
@@ -83,6 +77,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
private var targetDeviceName = "LBJReceiver"
|
private var targetDeviceName = "LBJReceiver"
|
||||||
|
|
||||||
|
|
||||||
|
private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) }
|
||||||
|
|
||||||
|
|
||||||
private val requestPermissions = registerForActivityResult(
|
private val requestPermissions = registerForActivityResult(
|
||||||
ActivityResultContracts.RequestMultiplePermissions()
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
) { permissions ->
|
) { permissions ->
|
||||||
@@ -133,16 +130,30 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
||||||
requestPermissions.launch(arrayOf(
|
loadSettings()
|
||||||
Manifest.permission.BLUETOOTH,
|
|
||||||
Manifest.permission.BLUETOOTH_ADMIN,
|
|
||||||
|
val permissions = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
permissions.addAll(arrayOf(
|
||||||
Manifest.permission.BLUETOOTH_CONNECT,
|
Manifest.permission.BLUETOOTH_CONNECT,
|
||||||
Manifest.permission.BLUETOOTH_SCAN,
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
Manifest.permission.BLUETOOTH_ADVERTISE
|
||||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
))
|
))
|
||||||
|
} else {
|
||||||
|
permissions.addAll(arrayOf(
|
||||||
|
Manifest.permission.BLUETOOTH,
|
||||||
|
Manifest.permission.BLUETOOTH_ADMIN
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
permissions.addAll(arrayOf(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
))
|
||||||
|
|
||||||
|
requestPermissions.launch(permissions.toTypedArray())
|
||||||
|
|
||||||
|
|
||||||
bleClient.setTrainInfoCallback { jsonData ->
|
bleClient.setTrainInfoCallback { jsonData ->
|
||||||
@@ -263,17 +274,36 @@ class MainActivity : ComponentActivity() {
|
|||||||
deviceName = settingsDeviceName,
|
deviceName = settingsDeviceName,
|
||||||
onDeviceNameChange = { newName -> settingsDeviceName = newName },
|
onDeviceNameChange = { newName -> settingsDeviceName = newName },
|
||||||
onApplySettings = {
|
onApplySettings = {
|
||||||
|
saveSettings()
|
||||||
|
targetDeviceName = settingsDeviceName
|
||||||
Toast.makeText(this, "设备名称 '${settingsDeviceName}' 已保存,下次连接时生效", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, "设备名称 '${settingsDeviceName}' 已保存,下次连接时生效", Toast.LENGTH_LONG).show()
|
||||||
Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}")
|
Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}")
|
||||||
},
|
},
|
||||||
locoInfoUtil = locoInfoUtil
|
locoInfoUtil = locoInfoUtil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 显示连接对话框
|
||||||
|
if (showConnectionDialog) {
|
||||||
|
ConnectionDialog(
|
||||||
|
isScanning = isScanning,
|
||||||
|
devices = foundDevices,
|
||||||
|
onDismiss = {
|
||||||
|
showConnectionDialog = false
|
||||||
|
stopScan()
|
||||||
|
},
|
||||||
|
onScan = {
|
||||||
|
if (isScanning) {
|
||||||
|
stopScan()
|
||||||
|
} else {
|
||||||
|
startScan()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onConnect = { device ->
|
||||||
|
showConnectionDialog = false
|
||||||
|
connectToDevice(device)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,7 +314,16 @@ class MainActivity : ComponentActivity() {
|
|||||||
deviceStatus = "正在连接..."
|
deviceStatus = "正在连接..."
|
||||||
Log.d(TAG, "Connecting to device name=${device.name ?: "Unknown"} address=${device.address}")
|
Log.d(TAG, "Connecting to device name=${device.name ?: "Unknown"} address=${device.address}")
|
||||||
|
|
||||||
|
// 检查蓝牙适配器状态
|
||||||
|
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
||||||
|
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) {
|
||||||
|
deviceStatus = "蓝牙未启用"
|
||||||
|
Log.e(TAG, "Bluetooth adapter unavailable or disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
bleClient.connect(device.address) { connected ->
|
bleClient.connect(device.address) { connected ->
|
||||||
|
runOnUiThread {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
deviceStatus = "已连接"
|
deviceStatus = "已连接"
|
||||||
Log.d(TAG, "Connected to device name=${device.name ?: "Unknown"}")
|
Log.d(TAG, "Connected to device name=${device.name ?: "Unknown"}")
|
||||||
@@ -293,6 +332,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
Log.e(TAG, "Connection failed name=${device.name ?: "Unknown"}")
|
Log.e(TAG, "Connection failed name=${device.name ?: "Unknown"}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deviceAddress = device.address
|
deviceAddress = device.address
|
||||||
stopScan()
|
stopScan()
|
||||||
@@ -382,6 +422,19 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
|
|
||||||
private fun startScan() {
|
private fun startScan() {
|
||||||
|
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
||||||
|
if (bluetoothAdapter == null) {
|
||||||
|
Log.e(TAG, "Bluetooth adapter unavailable")
|
||||||
|
deviceStatus = "设备不支持蓝牙"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bluetoothAdapter.isEnabled) {
|
||||||
|
Log.e(TAG, "Bluetooth adapter disabled")
|
||||||
|
deviceStatus = "请启用蓝牙"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isScanning = true
|
isScanning = true
|
||||||
foundDevices = emptyList()
|
foundDevices = emptyList()
|
||||||
val targetDeviceName = settingsDeviceName.ifBlank { null }
|
val targetDeviceName = settingsDeviceName.ifBlank { null }
|
||||||
@@ -396,12 +449,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
Log.d(TAG, "Found target=$targetDeviceName, connecting")
|
Log.d(TAG, "Found target=$targetDeviceName, connecting")
|
||||||
stopScan()
|
stopScan()
|
||||||
connectToDevice(device)
|
connectToDevice(device)
|
||||||
} else if (!foundDevices.any { it.address == device.address }) {
|
} else {
|
||||||
|
// 如果没有指定目标设备名称,或者找到的设备不是目标设备,显示连接对话框
|
||||||
|
if (targetDeviceName == null) {
|
||||||
showConnectionDialog = true
|
showConnectionDialog = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun stopScan() {
|
private fun stopScan() {
|
||||||
@@ -414,6 +470,21 @@ class MainActivity : ComponentActivity() {
|
|||||||
private fun updateDeviceList() {
|
private fun updateDeviceList() {
|
||||||
foundDevices = scanResults.map { it.device }
|
foundDevices = scanResults.map { it.device }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
settingsDeviceName = settingsPrefs.getString("device_name", "LBJReceiver") ?: "LBJReceiver"
|
||||||
|
targetDeviceName = settingsDeviceName
|
||||||
|
Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun saveSettings() {
|
||||||
|
settingsPrefs.edit()
|
||||||
|
.putString("device_name", settingsDeviceName)
|
||||||
|
.apply()
|
||||||
|
Log.d(TAG, "Saved settings deviceName=${settingsDeviceName}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -478,7 +549,7 @@ fun MainContent(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("LBJReceiver") },
|
title = { Text("LBJ Console") },
|
||||||
actions = {
|
actions = {
|
||||||
|
|
||||||
timeSinceLastUpdate.value?.let { time ->
|
timeSinceLastUpdate.value?.let { time ->
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package receiver.lbj.model
|
package org.noxylva.lbjconsole.model
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
import receiver.lbj.util.LocationUtils
|
import org.noxylva.lbjconsole.util.LocationUtils
|
||||||
|
|
||||||
class TrainRecord(jsonData: JSONObject? = null) {
|
class TrainRecord(jsonData: JSONObject? = null) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package receiver.lbj.model
|
package org.noxylva.lbjconsole.model
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package receiver.lbj.ui.components
|
package org.noxylva.lbjconsole.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||||
import org.osmdroid.views.MapView
|
import org.osmdroid.views.MapView
|
||||||
import org.osmdroid.views.overlay.Marker
|
import org.osmdroid.views.overlay.Marker
|
||||||
import receiver.lbj.model.TrainRecord
|
import org.noxylva.lbjconsole.model.TrainRecord
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrainDetailDialog(
|
fun TrainDetailDialog(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package receiver.lbj.ui.components
|
package org.noxylva.lbjconsole.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -10,7 +10,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import receiver.lbj.model.TrainRecord
|
import org.noxylva.lbjconsole.model.TrainRecord
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrainInfoCard(
|
fun TrainInfoCard(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package receiver.lbj.ui.components
|
package org.noxylva.lbjconsole.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -9,7 +9,6 @@ import androidx.compose.material.icons.filled.Clear
|
|||||||
import androidx.compose.material.icons.filled.FilterList
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -18,7 +17,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import receiver.lbj.model.TrainRecord
|
import org.noxylva.lbjconsole.model.TrainRecord
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
package receiver.lbj.ui.screens
|
package org.noxylva.lbjconsole.ui.screens
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.slideInVertically
|
|
||||||
import androidx.compose.animation.slideOutVertically
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -17,18 +12,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
|
||||||
import androidx.compose.material.icons.filled.SignalCellular4Bar
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.input.pointer.positionChange
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
|
||||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -42,8 +30,8 @@ import org.osmdroid.views.overlay.Marker
|
|||||||
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
|
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
|
||||||
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
||||||
import org.osmdroid.views.overlay.TilesOverlay
|
import org.osmdroid.views.overlay.TilesOverlay
|
||||||
import receiver.lbj.model.TrainRecord
|
import org.noxylva.lbjconsole.model.TrainRecord
|
||||||
import receiver.lbj.util.LocoInfoUtil
|
import org.noxylva.lbjconsole.util.LocoInfoUtil
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -1,25 +1,15 @@
|
|||||||
package receiver.lbj.ui.screens
|
package org.noxylva.lbjconsole.ui.screens
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import receiver.lbj.R
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.graphics.PorterDuffColorFilter
|
import android.graphics.PorterDuffColorFilter
|
||||||
import android.location.Location
|
|
||||||
import android.location.LocationListener
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.location.LocationManager
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.MyLocation
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
import androidx.compose.material.icons.filled.Layers
|
import androidx.compose.material.icons.filled.Layers
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@@ -36,19 +26,15 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.osmdroid.config.Configuration
|
import org.osmdroid.config.Configuration
|
||||||
import org.osmdroid.tileprovider.MapTileProviderBasic
|
import org.osmdroid.tileprovider.MapTileProviderBasic
|
||||||
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
|
|
||||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||||
import org.osmdroid.tileprovider.tilesource.XYTileSource
|
import org.osmdroid.tileprovider.tilesource.XYTileSource
|
||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
import org.osmdroid.views.MapView
|
import org.osmdroid.views.MapView
|
||||||
import org.osmdroid.views.overlay.*
|
import org.osmdroid.views.overlay.*
|
||||||
import org.osmdroid.views.overlay.compass.CompassOverlay
|
|
||||||
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
|
|
||||||
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
|
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
|
||||||
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
||||||
import receiver.lbj.model.TrainRecord
|
import org.noxylva.lbjconsole.model.TrainRecord
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -184,13 +170,7 @@ fun MapScreen(
|
|||||||
mapView.invalidate()
|
mapView.invalidate()
|
||||||
|
|
||||||
|
|
||||||
if (!isMapInitialized && validRecords.isNotEmpty()) {
|
|
||||||
validRecords.firstOrNull()?.getCoordinates()?.let { point ->
|
|
||||||
mapView.controller.setZoom(12.0)
|
|
||||||
mapView.controller.setCenter(point)
|
|
||||||
isMapInitialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -258,7 +238,7 @@ fun MapScreen(
|
|||||||
|
|
||||||
val railwayTileSource = XYTileSource(
|
val railwayTileSource = XYTileSource(
|
||||||
"OpenRailwayMap",
|
"OpenRailwayMap",
|
||||||
0, 24, // 穷尽所有可能的缩放级别
|
0, 24,
|
||||||
256,
|
256,
|
||||||
".png",
|
".png",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
@@ -297,7 +277,15 @@ fun MapScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (validRecords.isNotEmpty()) {
|
||||||
|
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
|
||||||
|
controller.setCenter(lastPoint)
|
||||||
|
controller.setZoom(12.0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controller.setCenter(defaultPosition)
|
||||||
controller.setZoom(10.0)
|
controller.setZoom(10.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -307,28 +295,8 @@ fun MapScreen(
|
|||||||
locationUpdateMinTime = 1000
|
locationUpdateMinTime = 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val myLocationOverlay = MyLocationNewOverlay(locationProvider, this).apply {
|
val myLocationOverlay = MyLocationNewOverlay(locationProvider, this).apply {
|
||||||
// 使用我们的自定义人形图标
|
|
||||||
// 使用原生位置/雷达图标
|
|
||||||
val personDrawable = ctx.resources.getDrawable(android.R.drawable.ic_menu_mylocation, ctx.theme)
|
|
||||||
// 设置为黑色
|
|
||||||
personDrawable.setTint(Color.BLACK)
|
|
||||||
|
|
||||||
val bitmap = Bitmap.createBitmap(
|
|
||||||
personDrawable.intrinsicWidth,
|
|
||||||
personDrawable.intrinsicHeight,
|
|
||||||
Bitmap.Config.ARGB_8888
|
|
||||||
)
|
|
||||||
val canvas = Canvas(bitmap)
|
|
||||||
personDrawable.setBounds(0, 0, canvas.width, canvas.height)
|
|
||||||
personDrawable.draw(canvas)
|
|
||||||
|
|
||||||
setPersonIcon(bitmap)
|
|
||||||
// 设置中心对齐
|
|
||||||
setPersonAnchor(0.5f, 0.5f)
|
|
||||||
|
|
||||||
isDrawAccuracyEnabled = false
|
|
||||||
|
|
||||||
enableMyLocation()
|
enableMyLocation()
|
||||||
|
|
||||||
runOnFirstFix {
|
runOnFirstFix {
|
||||||
@@ -337,21 +305,40 @@ fun MapScreen(
|
|||||||
currentLocation = GeoPoint(location.latitude, location.longitude)
|
currentLocation = GeoPoint(location.latitude, location.longitude)
|
||||||
|
|
||||||
if (!isMapInitialized) {
|
if (!isMapInitialized) {
|
||||||
controller.animateTo(location)
|
controller.setCenter(location)
|
||||||
|
controller.setZoom(15.0)
|
||||||
isMapInitialized = true
|
isMapInitialized = true
|
||||||
|
Log.d("MapScreen", "Map initialized with GPS position: $location")
|
||||||
}
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
|
|
||||||
if (!isMapInitialized) {
|
if (!isMapInitialized) {
|
||||||
controller.animateTo(defaultPosition)
|
if (validRecords.isNotEmpty()) {
|
||||||
|
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
|
||||||
|
controller.setCenter(lastPoint)
|
||||||
|
controller.setZoom(12.0)
|
||||||
|
isMapInitialized = true
|
||||||
|
Log.d("MapScreen", "Map initialized with last record position: $lastPoint")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controller.setCenter(defaultPosition)
|
||||||
|
isMapInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
||||||
if (!isMapInitialized) {
|
if (!isMapInitialized) {
|
||||||
controller.animateTo(defaultPosition)
|
if (validRecords.isNotEmpty()) {
|
||||||
|
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
|
||||||
|
controller.setCenter(lastPoint)
|
||||||
|
controller.setZoom(12.0)
|
||||||
|
isMapInitialized = true
|
||||||
|
Log.d("MapScreen", "Map fallback to last record position: $lastPoint")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controller.setCenter(defaultPosition)
|
||||||
|
isMapInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +412,7 @@ fun MapScreen(
|
|||||||
overlay.enableFollowLocation()
|
overlay.enableFollowLocation()
|
||||||
overlay.enableMyLocation()
|
overlay.enableMyLocation()
|
||||||
overlay.myLocation?.let { location ->
|
overlay.myLocation?.let { location ->
|
||||||
mapViewRef.value?.controller?.animateTo(location)
|
mapViewRef.value?.controller?.setCenter(location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -467,14 +454,22 @@ fun MapScreen(
|
|||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
mapViewRef.value?.let { mapView ->
|
mapViewRef.value?.let { mapView ->
|
||||||
|
myLocationOverlayRef.value?.myLocation?.let { gpsLocation ->
|
||||||
|
mapView.controller.setCenter(gpsLocation)
|
||||||
|
mapView.controller.setZoom(15.0)
|
||||||
|
Log.d("MapScreen", "Refresh button: GPS position used")
|
||||||
|
} ?: run {
|
||||||
if (validRecords.isNotEmpty()) {
|
if (validRecords.isNotEmpty()) {
|
||||||
validRecords.firstOrNull()?.getCoordinates()?.let { point ->
|
validRecords.lastOrNull()?.getCoordinates()?.let { point ->
|
||||||
mapView.controller.animateTo(point)
|
mapView.controller.setCenter(point)
|
||||||
mapView.controller.setZoom(12.0)
|
mapView.controller.setZoom(12.0)
|
||||||
|
Log.d("MapScreen", "Refresh button: last record position used")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mapView.controller.animateTo(defaultPosition)
|
mapView.controller.setCenter(defaultPosition)
|
||||||
mapView.controller.setZoom(10.0)
|
mapView.controller.setZoom(10.0)
|
||||||
|
Log.d("MapScreen", "Refresh button: default position used")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onCenterMap()
|
onCenterMap()
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package receiver.lbj.ui.screens
|
package org.noxylva.lbjconsole.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Clear
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -14,8 +12,8 @@ import androidx.compose.ui.unit.TextUnit
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import receiver.lbj.model.TrainRecord
|
import org.noxylva.lbjconsole.model.TrainRecord
|
||||||
import receiver.lbj.ui.components.TrainDetailDialog
|
import org.noxylva.lbjconsole.ui.components.TrainDetailDialog
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.noxylva.lbjconsole.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
deviceName: String,
|
||||||
|
onDeviceNameChange: (String) -> Unit,
|
||||||
|
onApplySettings: () -> Unit
|
||||||
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = deviceName,
|
||||||
|
onValueChange = onDeviceNameChange,
|
||||||
|
label = { Text("蓝牙设备名称") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onApplySettings,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("应用设置")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "LBJ Console v0.0.1 by undef-i",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
uriHandler.openUri("https://github.com/undef-i")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package receiver.lbj.ui.theme
|
package org.noxylva.lbjconsole.ui.theme
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package receiver.lbj.ui.theme
|
package org.noxylva.lbjconsole.ui.theme
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package receiver.lbj.ui.theme
|
package org.noxylva.lbjconsole.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
package receiver.lbj.util
|
package org.noxylva.lbjconsole.util
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
|
|
||||||
object LocationUtils {
|
object LocationUtils {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package receiver.lbj.util
|
package org.noxylva.lbjconsole.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package receiver.lbj.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun SettingsScreen(
|
|
||||||
deviceName: String,
|
|
||||||
onDeviceNameChange: (String) -> Unit,
|
|
||||||
onApplySettings: () -> Unit
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
Text("设置", style = MaterialTheme.typography.headlineMedium)
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = deviceName,
|
|
||||||
onValueChange = onDeviceNameChange,
|
|
||||||
label = { Text("蓝牙设备名称") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
Button(onClick = onApplySettings, modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Text("应用设备名称")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">LBJ Receiver</string>
|
<string name="app_name">LBJ Console</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package receiver.lbj
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
|
|
||||||
class ExampleUnitTest {
|
|
||||||
@Test
|
|
||||||
fun addition_isCorrect() {
|
|
||||||
assertEquals(4, 2 + 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user