M app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt => app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt +16 -7
@@ 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)
M app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt => app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt +10 -5
@@ 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) {
A app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenController.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenController.kt +8 -0
@@ 0,0 1,8 @@
+package com.github.nacabaro.vbhelper.screens.settingsScreen
+
+import android.net.Uri
+
+interface NewSettingsScreenController {
+ fun onClickOpenDirectory()
+ fun onClickImportDatabase()
+}<
\ No newline at end of file
A app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenControllerImpl.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenControllerImpl.kt +152 -0
@@ 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()
+ }
+}<
\ No newline at end of file
R app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt +11 -1
@@ 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") { }
R app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt +1 -1
@@ 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