feat: add database import and export functions

This commit is contained in:
Nedifinita
2025-08-28 19:46:31 +08:00
parent 077e0e4266
commit 78cc909ec8
7 changed files with 409 additions and 5 deletions

View File

@@ -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")
}

View File

@@ -15,6 +15,9 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
@@ -41,6 +44,12 @@
</intent-filter>
</activity>
<activity
android:name=".FilePickerActivity"
android:exported="false"
android:theme="@style/Theme.LBJConsole"
android:label="数据管理" />
<service
android:name=".BackgroundService"
android:enabled="true"

View File

@@ -0,0 +1,123 @@
package org.noxylva.lbjconsole
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
import org.noxylva.lbjconsole.util.DatabaseExportImportUtil
class FilePickerActivity : ComponentActivity() {
private val exportFilePicker = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri ->
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")
}
}
}
}

View File

@@ -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,

View File

@@ -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<TrainRecordEntity>
)
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<SimpleRecordBackup>() {}.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")
}
}