From 78cc909ec834f02fc4b0aeb9afdb0653efbfbb3b Mon Sep 17 00:00:00 2001 From: Nedifinita Date: Thu, 28 Aug 2025 19:46:31 +0800 Subject: [PATCH] feat: add database import and export functions --- .gitignore | 144 +++++++++++++++++- README.md | 4 +- app/build.gradle.kts | 5 +- app/src/main/AndroidManifest.xml | 9 ++ .../noxylva/lbjconsole/FilePickerActivity.kt | 123 +++++++++++++++ .../lbjconsole/ui/screens/SettingsScreen.kt | 62 ++++++++ .../util/DatabaseExportImportUtil.kt | 67 ++++++++ 7 files changed, 409 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/noxylva/lbjconsole/FilePickerActivity.kt create mode 100644 app/src/main/java/org/noxylva/lbjconsole/util/DatabaseExportImportUtil.kt diff --git a/.gitignore b/.gitignore index 4bdae4d..fcfadf8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,146 @@ local.properties docs linux windows -android_original \ No newline at end of file +flutter/ephemeral/ +*.suo +*.user +*.userosscache +*.sln.docstates +x64/ +x86/ +*.[Cc]ache +!*.[Cc]ache/ +.gradle/ +build/ +local.properties +*.log +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml +*.jks +*.keystore +google-services.json +*.hprof +gen-external-apklibs +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.fvm/flutter_sdk +.packages +.pub-cache/ +.pub/ +coverage/ +lib/generated_plugin_registrant.dart +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/key.properties +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +*.ap_ +*.aab +*.dex +*.class +bin/ +gen/ +out/ +.gradle +.signing/ +proguard/ +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +.navigation/ +*.ipr +*~ +*.swp +.externalNativeBuild +obj/ +*.iws +/out/ +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml +.classpath +.project +.cproject +.settings/ +.mtj.tmp/ +*.war +*.ear +hs_err_pid* +.idea_modules/ +atlassian-ide-plugin.xml +.idea/mongoSettings.xml +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +!/gradle/wrapper/gradle-wrapper.jar +macos/Flutter/ephemeral/flutter_export_environment.sh +macos/Flutter/ephemeral/Flutter-Generated.xcconfig diff --git a/README.md b/README.md index 0f8f23c..a131f1d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # LBJ Console -LBJ Console 是一款 Android 应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括: +LBJ Console 是一款应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括: - 接收列车预警消息,支持可选的手机推送通知。 - 在地图上显示预警消息的 GPS 信息。 - 基于内置数据文件显示机车配属,机车类型和车次类型。 - +主分支目前只适配了 Android 。如需在其它平台上面使用,请参考 [flutter](https://github.com/undef-i/LBJ_Console/tree/flutter) 分支自行编译。 ## 数据文件 LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 75e3f70..f2292d1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "org.noxylva.lbjconsole" minSdk = 29 targetSdk = 35 - versionCode = 13 - versionName = "0.1.3" + versionCode = 14 + versionName = "0.1.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -92,4 +92,5 @@ dependencies { implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) implementation(libs.androidx.startup.runtime) + implementation("com.google.code.gson:gson:2.10.1") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc2ae1f..5ab5764 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,9 @@ + + @@ -41,6 +44,12 @@ + + + uri?.let { exportDatabase(it) } + } + + private val importFilePicker = registerForActivityResult( + ActivityResultContracts.OpenDocument() + ) { uri -> + uri?.let { importDatabase(it) } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val action = intent.getStringExtra("action") ?: "export" + + setContent { + val coroutineScope = rememberCoroutineScope() + + when (action) { + "export" -> { + val fileName = "lbj_console_backup_${System.currentTimeMillis()}.json" + exportFilePicker.launch(fileName) + } + "import" -> { + importFilePicker.launch(arrayOf("application/json", "text/plain")) + } + } + } + } + + private fun exportDatabase(uri: Uri) { + val coroutineScope = kotlinx.coroutines.MainScope() + coroutineScope.launch { + try { + val databaseUtil = DatabaseExportImportUtil(this@FilePickerActivity) + val json = databaseUtil.exportDatabase() + + contentResolver.openOutputStream(uri)?.use { outputStream -> + outputStream.write(json.toByteArray()) + } + + Toast.makeText( + this@FilePickerActivity, + "数据导出成功", + Toast.LENGTH_SHORT + ).show() + + } catch (e: Exception) { + Toast.makeText( + this@FilePickerActivity, + "导出失败: ${e.message}", + Toast.LENGTH_SHORT + ).show() + } finally { + finish() + } + } + } + + private fun importDatabase(uri: Uri) { + val coroutineScope = kotlinx.coroutines.MainScope() + coroutineScope.launch { + try { + val databaseUtil = DatabaseExportImportUtil(this@FilePickerActivity) + val success = databaseUtil.importDatabase(uri) + + if (success) { + Toast.makeText( + this@FilePickerActivity, + "数据导入成功", + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + this@FilePickerActivity, + "数据导入失败", + Toast.LENGTH_SHORT + ).show() + } + + } catch (e: Exception) { + Toast.makeText( + this@FilePickerActivity, + "导入失败: ${e.message}", + Toast.LENGTH_SHORT + ).show() + } finally { + finish() + } + } + } + + companion object { + fun createExportIntent(context: android.content.Context): Intent { + return Intent(context, FilePickerActivity::class.java).apply { + putExtra("action", "export") + } + } + + fun createImportIntent(context: android.content.Context): Intent { + return Intent(context, FilePickerActivity::class.java).apply { + putExtra("action", "import") + } + } + } +} \ 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 e21296b..58cd4fc 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 @@ -1,5 +1,6 @@ package org.noxylva.lbjconsole.ui.screens +import android.content.Intent import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -22,6 +23,7 @@ import org.noxylva.lbjconsole.model.TimeWindow import org.noxylva.lbjconsole.database.AppSettingsRepository import org.noxylva.lbjconsole.BackgroundService import org.noxylva.lbjconsole.NotificationService +import org.noxylva.lbjconsole.FilePickerActivity import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.DisposableEffect @@ -426,6 +428,66 @@ 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.Storage, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Text( + "数据管理", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + + val context = LocalContext.current + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Button( + onClick = { + val intent = FilePickerActivity.createExportIntent(context) + context.startActivity(intent) + }, + modifier = Modifier.weight(1f).padding(horizontal = 4.dp) + ) { + Icon(Icons.Default.Upload, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Text("导出") + } + + Button( + onClick = { + val intent = FilePickerActivity.createImportIntent(context) + context.startActivity(intent) + }, + modifier = Modifier.weight(1f).padding(horizontal = 4.dp) + ) { + Icon(Icons.Default.Download, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Text("导入") + } + } + } + } + Text( text = "LBJ Console v$appVersion by undef-i", style = MaterialTheme.typography.bodySmall, diff --git a/app/src/main/java/org/noxylva/lbjconsole/util/DatabaseExportImportUtil.kt b/app/src/main/java/org/noxylva/lbjconsole/util/DatabaseExportImportUtil.kt new file mode 100644 index 0000000..a05ac3d --- /dev/null +++ b/app/src/main/java/org/noxylva/lbjconsole/util/DatabaseExportImportUtil.kt @@ -0,0 +1,67 @@ +package org.noxylva.lbjconsole.util + +import android.content.Context +import android.net.Uri +import android.util.Log +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.noxylva.lbjconsole.database.AppSettingsEntity +import org.noxylva.lbjconsole.database.TrainDatabase +import org.noxylva.lbjconsole.database.TrainRecordEntity +import java.io.* +import java.util.* +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class DatabaseExportImportUtil(private val context: Context) { + private val gson = Gson() + private val database = TrainDatabase.getDatabase(context) + + data class SimpleRecordBackup( + val records: List + ) + + suspend fun exportDatabase(): String = withContext(Dispatchers.IO) { + try { + val trainRecords = database.trainRecordDao().getAllRecords() + + val backup = SimpleRecordBackup( + records = trainRecords + ) + + val json = gson.toJson(backup) + json + } catch (e: Exception) { + Log.e("DatabaseExport", "导出失败", e) + throw e + } + } + + suspend fun importDatabase(uri: Uri): Boolean = withContext(Dispatchers.IO) { + try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val json = inputStream.bufferedReader().use { it.readText() } + val backup: SimpleRecordBackup = gson.fromJson(json, object : TypeToken() {}.type) + + database.trainRecordDao().deleteAllRecords() + + if (backup.records.isNotEmpty()) { + database.trainRecordDao().insertRecords(backup.records) + } + + true + } ?: false + } catch (e: Exception) { + Log.e("DatabaseImport", "导入失败", e) + false + } + } + + suspend fun getExportFileUri(): Uri { + val filePath = exportDatabase() + return Uri.parse("file://$filePath") + } +} \ No newline at end of file