Data export working

During export the database will be closed, otherwise the WAL will ruin the export. data will be saved as a .vbhelper file, then the app will be closed to avoid making any changes to the DB
For import, we check if the extension is .vbhelper, otherwise we can corrupt the database, IMPORTANT. Next we delete everything related to RoomDB and we swap it with the new files. Finally the app will be closed to ensure RoomDB is running on the new DB.

Finally, I still have to reintegrate the importApk functionality with the NewSettingsScreenController and get rid of the old one.
This commit is contained in:
Nacho 2025-01-18 00:45:16 +01:00
parent 88163684ca
commit 42dd87f0c4
6 changed files with 198 additions and 14 deletions

View File

@ -21,12 +21,12 @@ import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.characters.Card
import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.domain.characters.Character
import com.github.nacabaro.vbhelper.domain.characters.Dex
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.SettingsScreenController
import com.github.nacabaro.vbhelper.screens.settingsScreen.NewSettingsScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenController
import com.github.nacabaro.vbhelper.source.ApkSecretsImporter
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
import com.github.nacabaro.vbhelper.utils.DeviceType
@ -64,14 +64,15 @@ class MainActivity : ComponentActivity() {
this::handleReceivedNfcCharacter,
this,
this::registerActivityLifecycleListener,
this::unregisterActivityLifecycleListener)
this::unregisterActivityLifecycleListener
)
val newSettingsScreenController = NewSettingsScreenControllerImpl(this)
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
VBHelperTheme {
MainApplication(settingsScreenController, scanScreenController)
MainApplication(settingsScreenController, scanScreenController, newSettingsScreenController)
}
}
Log.i("MainActivity", "Activity onCreated")
@ -190,10 +191,18 @@ class MainActivity : ComponentActivity() {
}
@Composable
private fun MainApplication(settingsScreenController: SettingsScreenController, scanScreenController: ScanScreenControllerImpl) {
private fun MainApplication(
settingsScreenController: SettingsScreenController,
scanScreenController: ScanScreenControllerImpl,
newSettingsScreenController: NewSettingsScreenControllerImpl
) {
AppNavigation(
applicationNavigationHandlers = AppNavigationHandlers(settingsScreenController, scanScreenController),
applicationNavigationHandlers = AppNavigationHandlers(
settingsScreenController,
scanScreenController,
newSettingsScreenController
),
onClickImportCard = {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)

View File

@ -1,6 +1,5 @@
package com.github.nacabaro.vbhelper.navigation
import android.util.Log
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
@ -15,12 +14,17 @@ import com.github.nacabaro.vbhelper.screens.homeScreens.HomeScreen
import com.github.nacabaro.vbhelper.screens.ItemsScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.SettingsScreen
import com.github.nacabaro.vbhelper.screens.SettingsScreenController
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreen
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenController
import com.github.nacabaro.vbhelper.screens.SpriteViewer
import com.github.nacabaro.vbhelper.screens.StorageScreen
import com.github.nacabaro.vbhelper.screens.settingsScreen.NewSettingsScreenControllerImpl
data class AppNavigationHandlers(val settingsScreenController: SettingsScreenController, val scanScreenController: ScanScreenControllerImpl)
data class AppNavigationHandlers(
val settingsScreenController: SettingsScreenController,
val scanScreenController: ScanScreenControllerImpl,
val newSettingsScreenController: NewSettingsScreenControllerImpl
)
@Composable
fun AppNavigation(
@ -72,7 +76,8 @@ fun AppNavigation(
SettingsScreen(
navController = navController,
settingsScreenController = applicationNavigationHandlers.settingsScreenController,
onClickImportCard = onClickImportCard
newSettingsScreenController = applicationNavigationHandlers.newSettingsScreenController,
onClickImportCard = onClickImportCard,
)
}
composable(NavigationItems.Viewer.route) {

View File

@ -0,0 +1,8 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen
import android.net.Uri
interface NewSettingsScreenController {
fun onClickOpenDirectory()
fun onClickImportDatabase()
}

View File

@ -0,0 +1,152 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.github.nacabaro.vbhelper.di.VBHelper
import kotlinx.coroutines.Dispatchers
import java.io.File
import java.io.InputStream
import java.io.OutputStream
class NewSettingsScreenControllerImpl(
private val context: ComponentActivity,
): NewSettingsScreenController {
private val filePickerLauncher: ActivityResultLauncher<String>
private val filePickerOpenerLauncher: ActivityResultLauncher<Array<String>>
init {
filePickerLauncher = context.registerForActivityResult(
ActivityResultContracts.CreateDocument("application/octet-stream")
) { uri ->
if (uri != null) {
exportDatabase("internalDb", uri)
} else {
context.runOnUiThread {
Toast.makeText(context, "No destination selected", Toast.LENGTH_SHORT)
.show()
}
}
}
filePickerOpenerLauncher = context.registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
if (uri != null) {
importDatabase("internalDb", uri)
} else {
context.runOnUiThread {
Toast.makeText(context, "No source selected", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onClickOpenDirectory() {
filePickerLauncher.launch("My application data.vbhelper")
}
override fun onClickImportDatabase() {
filePickerOpenerLauncher.launch(arrayOf("application/octet-stream"))
}
private fun exportDatabase(roomDbName: String, destinationUri: Uri) {
context.lifecycleScope.launch(Dispatchers.IO) {
try {
val application = context.applicationContext as VBHelper
val dbFile = File(context.getDatabasePath(roomDbName).absolutePath)
if (!dbFile.exists()) {
throw IllegalStateException("Database file does not exist!")
}
application.container.db.close()
context.contentResolver.openOutputStream(destinationUri)?.use { outputStream ->
dbFile.inputStream().use { inputStream ->
copyFile(inputStream, outputStream)
}
} ?: throw IllegalArgumentException("Unable to open destination Uri for writing")
context.runOnUiThread {
Toast.makeText(context, "Database exported successfully!", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Closing application to avoid changes.", Toast.LENGTH_LONG).show()
context.finishAffinity()
}
} catch (e: Exception) {
Log.e("ScanScreenController", "Error exporting database $e")
context.runOnUiThread {
Toast.makeText(context, "Error exporting database: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
private fun importDatabase(roomDbName: String, sourceUri: Uri) {
context.lifecycleScope.launch(Dispatchers.IO) {
try {
if (!getFileNameFromUri(sourceUri)!!.endsWith(".vbhelper")) {
context.runOnUiThread {
Toast.makeText(context, "Invalid file format", Toast.LENGTH_SHORT).show()
}
return@launch
}
val dbPath = context.getDatabasePath(roomDbName)
val shmFile = File(dbPath.parent, "$roomDbName-shm")
val walFile = File(dbPath.parent, "$roomDbName-wal")
// Delete existing database files
if (dbPath.exists()) dbPath.delete()
if (shmFile.exists()) shmFile.delete()
if (walFile.exists()) walFile.delete()
val dbFile = File(dbPath.absolutePath)
context.contentResolver.openInputStream(sourceUri)?.use { inputStream ->
dbFile.outputStream().use { outputStream ->
copyFile(inputStream, outputStream)
}
} ?: throw IllegalArgumentException("Unable to open source Uri for reading")
context.runOnUiThread {
Toast.makeText(context, "Database imported successfully!", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Reopen the app to finish import process!", Toast.LENGTH_LONG).show()
context.finishAffinity()
}
} catch (e: Exception) {
Log.e("ScanScreenController", "Error importing database $e")
context.runOnUiThread {
Toast.makeText(context, "Error importing database: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
private fun getFileNameFromUri(uri: Uri): String? {
var fileName: String? = null
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val nameIndex = it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
fileName = it.getString(nameIndex)
}
}
return fileName
}
private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
outputStream.flush()
}
}

View File

@ -1,4 +1,4 @@
package com.github.nacabaro.vbhelper.screens
package com.github.nacabaro.vbhelper.screens.settingsScreen
import android.net.Uri
import androidx.activity.ComponentActivity
@ -22,11 +22,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.navigation.AppNavigation
import com.github.nacabaro.vbhelper.navigation.NavigationItems
@Composable
fun SettingsScreen(
navController: NavController,
settingsScreenController: SettingsScreenController,
newSettingsScreenController: NewSettingsScreenControllerImpl,
onClickImportCard: () -> Unit
) {
Scaffold (
@ -51,6 +54,13 @@ fun SettingsScreen(
SettingsEntry(title = "Import APK", description = "Import Secrets From Vital Arean 2.1.0 APK") {
settingsScreenController.apkFilePickLauncher.launch(arrayOf("*/*"))
}
SettingsSection("Data management")
SettingsEntry(title = "Export data", description = "Export application database") {
newSettingsScreenController.onClickOpenDirectory()
}
SettingsEntry(title = "Import data", description = "Import application database") {
newSettingsScreenController.onClickImportDatabase()
}
SettingsSection("DiM/BEm management")
SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
SettingsEntry(title = "Rename DiM/BEm", description = "Set card name") { }

View File

@ -1,4 +1,4 @@
package com.github.nacabaro.vbhelper.screens
package com.github.nacabaro.vbhelper.screens.settingsScreen
import android.net.Uri
import android.widget.Toast