feat: add database import and export functions
This commit is contained in:
@@ -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")
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
123
app/src/main/java/org/noxylva/lbjconsole/FilePickerActivity.kt
Normal file
123
app/src/main/java/org/noxylva/lbjconsole/FilePickerActivity.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user