refactor: migrate data storage from SharedPreferences to Room database
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package org.noxylva.lbjconsole.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
@Database(
|
||||
entities = [TrainRecordEntity::class],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class TrainDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun trainRecordDao(): TrainRecordDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: TrainDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): TrainDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
TrainDatabase::class.java,
|
||||
"train_database"
|
||||
).build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.noxylva.lbjconsole.database
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TrainRecordDao {
|
||||
|
||||
@Query("SELECT * FROM train_records ORDER BY timestamp DESC")
|
||||
suspend fun getAllRecords(): List<TrainRecordEntity>
|
||||
|
||||
@Query("SELECT * FROM train_records ORDER BY timestamp DESC")
|
||||
fun getAllRecordsFlow(): Flow<List<TrainRecordEntity>>
|
||||
|
||||
@Query("SELECT * FROM train_records WHERE uniqueId = :uniqueId")
|
||||
suspend fun getRecordById(uniqueId: String): TrainRecordEntity?
|
||||
|
||||
@Query("SELECT COUNT(*) FROM train_records")
|
||||
suspend fun getRecordCount(): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecord(record: TrainRecordEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecords(records: List<TrainRecordEntity>)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteRecord(record: TrainRecordEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteRecords(records: List<TrainRecordEntity>)
|
||||
|
||||
@Query("DELETE FROM train_records")
|
||||
suspend fun deleteAllRecords()
|
||||
|
||||
@Query("DELETE FROM train_records WHERE uniqueId = :uniqueId")
|
||||
suspend fun deleteRecordById(uniqueId: String)
|
||||
|
||||
@Query("DELETE FROM train_records WHERE uniqueId IN (:uniqueIds)")
|
||||
suspend fun deleteRecordsByIds(uniqueIds: List<String>)
|
||||
|
||||
@Query("SELECT * FROM train_records WHERE train LIKE '%' || :train || '%' AND route LIKE '%' || :route || '%' AND (:direction = '全部' OR (:direction = '上行' AND direction = 3) OR (:direction = '下行' AND direction = 1)) ORDER BY timestamp DESC")
|
||||
suspend fun getFilteredRecords(train: String, route: String, direction: String): List<TrainRecordEntity>
|
||||
|
||||
@Query("SELECT * FROM train_records ORDER BY timestamp DESC LIMIT :limit")
|
||||
suspend fun getLatestRecords(limit: Int): List<TrainRecordEntity>
|
||||
|
||||
@Query("SELECT * FROM train_records WHERE timestamp >= :fromTime ORDER BY timestamp DESC")
|
||||
suspend fun getRecordsFromTime(fromTime: Long): List<TrainRecordEntity>
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.noxylva.lbjconsole.database
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.noxylva.lbjconsole.model.TrainRecord
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "train_records")
|
||||
data class TrainRecordEntity(
|
||||
@PrimaryKey val uniqueId: String,
|
||||
val timestamp: Long,
|
||||
val receivedTimestamp: Long,
|
||||
val train: String,
|
||||
val direction: Int,
|
||||
val speed: String,
|
||||
val position: String,
|
||||
val time: String,
|
||||
val loco: String,
|
||||
val locoType: String,
|
||||
val lbjClass: String,
|
||||
val route: String,
|
||||
val positionInfo: String,
|
||||
val rssi: Double
|
||||
) {
|
||||
fun toTrainRecord(): TrainRecord {
|
||||
val jsonData = JSONObject().apply {
|
||||
put("uniqueId", uniqueId)
|
||||
put("timestamp", timestamp)
|
||||
put("receivedTimestamp", receivedTimestamp)
|
||||
put("train", train)
|
||||
put("dir", direction)
|
||||
put("speed", speed)
|
||||
put("pos", position)
|
||||
put("time", time)
|
||||
put("loco", loco)
|
||||
put("loco_type", locoType)
|
||||
put("lbj_class", lbjClass)
|
||||
put("route", route)
|
||||
put("position_info", positionInfo)
|
||||
put("rssi", rssi)
|
||||
}
|
||||
return TrainRecord(jsonData)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromTrainRecord(record: TrainRecord): TrainRecordEntity {
|
||||
return TrainRecordEntity(
|
||||
uniqueId = record.uniqueId,
|
||||
timestamp = record.timestamp.time,
|
||||
receivedTimestamp = record.receivedTimestamp.time,
|
||||
train = record.train,
|
||||
direction = record.direction,
|
||||
speed = record.speed,
|
||||
position = record.position,
|
||||
time = record.time,
|
||||
loco = record.loco,
|
||||
locoType = record.locoType,
|
||||
lbjClass = record.lbjClass,
|
||||
route = record.route,
|
||||
positionInfo = record.positionInfo,
|
||||
rssi = record.rssi
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import android.util.Log
|
||||
import kotlinx.coroutines.*
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.noxylva.lbjconsole.database.TrainDatabase
|
||||
import org.noxylva.lbjconsole.database.TrainRecordEntity
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -27,6 +29,8 @@ class TrainRecordManager(private val context: Context) {
|
||||
private val trainRecords = CopyOnWriteArrayList<TrainRecord>()
|
||||
private val recordCount = AtomicInteger(0)
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
private val database = TrainDatabase.getDatabase(context)
|
||||
private val trainRecordDao = database.trainRecordDao()
|
||||
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
var mergeSettings = MergeSettings()
|
||||
@@ -34,11 +38,36 @@ class TrainRecordManager(private val context: Context) {
|
||||
|
||||
init {
|
||||
ioScope.launch {
|
||||
migrateFromSharedPreferences()
|
||||
loadRecords()
|
||||
loadMergeSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun migrateFromSharedPreferences() {
|
||||
try {
|
||||
val jsonStr = prefs.getString(KEY_RECORDS, null)
|
||||
if (jsonStr != null && jsonStr != "[]") {
|
||||
val jsonArray = JSONArray(jsonStr)
|
||||
val records = mutableListOf<TrainRecordEntity>()
|
||||
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val jsonObject = jsonArray.getJSONObject(i)
|
||||
val trainRecord = TrainRecord(jsonObject)
|
||||
records.add(TrainRecordEntity.fromTrainRecord(trainRecord))
|
||||
}
|
||||
|
||||
if (records.isNotEmpty()) {
|
||||
trainRecordDao.insertRecords(records)
|
||||
prefs.edit().remove(KEY_RECORDS).apply()
|
||||
Log.d(TAG, "Migrated ${records.size} records from SharedPreferences to Room database")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to migrate records: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var filterTrain: String = ""
|
||||
private var filterRoute: String = ""
|
||||
@@ -52,11 +81,16 @@ class TrainRecordManager(private val context: Context) {
|
||||
|
||||
|
||||
while (trainRecords.size > MAX_RECORDS) {
|
||||
trainRecords.removeAt(trainRecords.size - 1)
|
||||
val removedRecord = trainRecords.removeAt(trainRecords.size - 1)
|
||||
ioScope.launch {
|
||||
trainRecordDao.deleteRecordById(removedRecord.uniqueId)
|
||||
}
|
||||
}
|
||||
|
||||
recordCount.incrementAndGet()
|
||||
saveRecords()
|
||||
ioScope.launch {
|
||||
trainRecordDao.insertRecord(TrainRecordEntity.fromTrainRecord(record))
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
@@ -76,6 +110,16 @@ class TrainRecordManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getFilteredRecordsFromDatabase(): List<TrainRecord> {
|
||||
return try {
|
||||
val entities = trainRecordDao.getFilteredRecords(filterTrain, filterRoute, filterDirection)
|
||||
entities.map { it.toTrainRecord() }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get filtered records from database: ${e.message}")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun matchFilter(record: TrainRecord): Boolean {
|
||||
|
||||
@@ -118,32 +162,56 @@ class TrainRecordManager(private val context: Context) {
|
||||
}
|
||||
|
||||
|
||||
suspend fun refreshRecordsFromDatabase() {
|
||||
try {
|
||||
val entities = trainRecordDao.getAllRecords()
|
||||
trainRecords.clear()
|
||||
entities.forEach { entity ->
|
||||
trainRecords.add(entity.toTrainRecord())
|
||||
}
|
||||
recordCount.set(trainRecords.size)
|
||||
Log.d(TAG, "Refreshed ${trainRecords.size} records from database")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to refresh records from database: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun clearRecords() {
|
||||
trainRecords.clear()
|
||||
recordCount.set(0)
|
||||
saveRecords()
|
||||
ioScope.launch {
|
||||
trainRecordDao.deleteAllRecords()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteRecord(record: TrainRecord): Boolean {
|
||||
val result = trainRecords.remove(record)
|
||||
if (result) {
|
||||
recordCount.decrementAndGet()
|
||||
saveRecords()
|
||||
ioScope.launch {
|
||||
trainRecordDao.deleteRecordById(record.uniqueId)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun deleteRecords(records: List<TrainRecord>): Int {
|
||||
var deletedCount = 0
|
||||
val idsToDelete = mutableListOf<String>()
|
||||
|
||||
records.forEach { record ->
|
||||
if (trainRecords.remove(record)) {
|
||||
deletedCount++
|
||||
idsToDelete.add(record.uniqueId)
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
recordCount.addAndGet(-deletedCount)
|
||||
saveRecords()
|
||||
ioScope.launch {
|
||||
trainRecordDao.deleteRecordsByIds(idsToDelete)
|
||||
}
|
||||
}
|
||||
return deletedCount
|
||||
}
|
||||
@@ -151,12 +219,9 @@ class TrainRecordManager(private val context: Context) {
|
||||
private fun saveRecords() {
|
||||
ioScope.launch {
|
||||
try {
|
||||
val jsonArray = JSONArray()
|
||||
for (record in trainRecords) {
|
||||
jsonArray.put(record.toJSON())
|
||||
}
|
||||
prefs.edit().putString(KEY_RECORDS, jsonArray.toString()).apply()
|
||||
Log.d(TAG, "Saved ${trainRecords.size} records")
|
||||
val entities = trainRecords.map { TrainRecordEntity.fromTrainRecord(it) }
|
||||
trainRecordDao.insertRecords(entities)
|
||||
Log.d(TAG, "Saved ${trainRecords.size} records to database")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to save records: ${e.message}")
|
||||
}
|
||||
@@ -164,19 +229,17 @@ class TrainRecordManager(private val context: Context) {
|
||||
}
|
||||
|
||||
|
||||
private fun loadRecords() {
|
||||
private suspend fun loadRecords() {
|
||||
try {
|
||||
val jsonStr = prefs.getString(KEY_RECORDS, "[]")
|
||||
val jsonArray = JSONArray(jsonStr)
|
||||
val entities = trainRecordDao.getAllRecords()
|
||||
trainRecords.clear()
|
||||
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val jsonObject = jsonArray.getJSONObject(i)
|
||||
trainRecords.add(TrainRecord(jsonObject))
|
||||
entities.forEach { entity ->
|
||||
trainRecords.add(entity.toTrainRecord())
|
||||
}
|
||||
|
||||
recordCount.set(trainRecords.size)
|
||||
Log.d(TAG, "Loaded ${trainRecords.size} records")
|
||||
Log.d(TAG, "Loaded ${trainRecords.size} records from database")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load records: ${e.message}")
|
||||
}
|
||||
@@ -349,4 +412,41 @@ class TrainRecordManager(private val context: Context) {
|
||||
mergeSettings = MergeSettings()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun exportRecordsToJson(): JSONArray {
|
||||
val jsonArray = JSONArray()
|
||||
try {
|
||||
val entities = trainRecordDao.getAllRecords()
|
||||
entities.forEach { entity ->
|
||||
val record = entity.toTrainRecord()
|
||||
jsonArray.put(record.toJSON())
|
||||
}
|
||||
Log.d(TAG, "Exported ${entities.size} records to JSON")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to export records to JSON: ${e.message}")
|
||||
}
|
||||
return jsonArray
|
||||
}
|
||||
|
||||
suspend fun importRecordsFromJson(jsonArray: JSONArray): Int {
|
||||
var importedCount = 0
|
||||
try {
|
||||
val records = mutableListOf<TrainRecordEntity>()
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val jsonObject = jsonArray.getJSONObject(i)
|
||||
val trainRecord = TrainRecord(jsonObject)
|
||||
records.add(TrainRecordEntity.fromTrainRecord(trainRecord))
|
||||
}
|
||||
|
||||
if (records.isNotEmpty()) {
|
||||
trainRecordDao.insertRecords(records)
|
||||
importedCount = records.size
|
||||
refreshRecordsFromDatabase()
|
||||
Log.d(TAG, "Imported $importedCount records from JSON")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to import records from JSON: ${e.message}")
|
||||
}
|
||||
return importedCount
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user