diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt index 033f574..deae7b2 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt @@ -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) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt index 6f5fb75..b0138b5 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt @@ -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) { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenController.kt new file mode 100644 index 0000000..612406b --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenController.kt @@ -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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenControllerImpl.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenControllerImpl.kt new file mode 100644 index 0000000..53e8515 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/NewSettingsScreenControllerImpl.kt @@ -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 + private val filePickerOpenerLauncher: ActivityResultLauncher> + + 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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt similarity index 83% rename from app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt rename to app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt index 104cded..42b6e8d 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt @@ -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") { } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt similarity index 98% rename from app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt rename to app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt index 4afb3d1..6f1f41a 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt @@ -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