mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-01-27 16:05:32 +00:00
Merge pull request #20 from nacabaro/db/data_export
Added data importing/exporting
This commit is contained in:
commit
b89dfb0df3
@ -21,13 +21,11 @@ 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.source.ApkSecretsImporter
|
||||
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -57,21 +55,20 @@ class MainActivity : ComponentActivity() {
|
||||
registerFileActivityResult()
|
||||
|
||||
val application = applicationContext as VBHelper
|
||||
val settingsScreenController = SettingsScreenController.Factory(this, ApkSecretsImporter(), application.container.dataStoreSecretsRepository)
|
||||
.buildSettingScreenHandlers()
|
||||
val scanScreenController = ScanScreenControllerImpl(
|
||||
application.container.dataStoreSecretsRepository.secretsFlow,
|
||||
this::handleReceivedNfcCharacter,
|
||||
this,
|
||||
this::registerActivityLifecycleListener,
|
||||
this::unregisterActivityLifecycleListener)
|
||||
|
||||
this::unregisterActivityLifecycleListener
|
||||
)
|
||||
val settingsScreenController = SettingsScreenControllerImpl(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
VBHelperTheme {
|
||||
MainApplication(settingsScreenController, scanScreenController)
|
||||
MainApplication(scanScreenController, settingsScreenController)
|
||||
}
|
||||
}
|
||||
Log.i("MainActivity", "Activity onCreated")
|
||||
@ -190,10 +187,16 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainApplication(settingsScreenController: SettingsScreenController, scanScreenController: ScanScreenControllerImpl) {
|
||||
private fun MainApplication(
|
||||
scanScreenController: ScanScreenControllerImpl,
|
||||
settingsScreenController: SettingsScreenControllerImpl
|
||||
) {
|
||||
|
||||
AppNavigation(
|
||||
applicationNavigationHandlers = AppNavigationHandlers(settingsScreenController, scanScreenController),
|
||||
applicationNavigationHandlers = AppNavigationHandlers(
|
||||
settingsScreenController,
|
||||
scanScreenController,
|
||||
),
|
||||
onClickImportCard = {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
@ -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,15 @@ 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.SpriteViewer
|
||||
import com.github.nacabaro.vbhelper.screens.StorageScreen
|
||||
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl
|
||||
|
||||
data class AppNavigationHandlers(val settingsScreenController: SettingsScreenController, val scanScreenController: ScanScreenControllerImpl)
|
||||
data class AppNavigationHandlers(
|
||||
val settingsScreenController: SettingsScreenControllerImpl,
|
||||
val scanScreenController: ScanScreenControllerImpl,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AppNavigation(
|
||||
@ -72,7 +74,7 @@ fun AppNavigation(
|
||||
SettingsScreen(
|
||||
navController = navController,
|
||||
settingsScreenController = applicationNavigationHandlers.settingsScreenController,
|
||||
onClickImportCard = onClickImportCard
|
||||
onClickImportCard = onClickImportCard,
|
||||
)
|
||||
}
|
||||
composable(NavigationItems.Viewer.route) {
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
package com.github.nacabaro.vbhelper.screens
|
||||
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.nacabaro.vbhelper.source.SecretsImporter
|
||||
import com.github.nacabaro.vbhelper.source.SecretsRepository
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class SettingsScreenController(val apkFilePickLauncher: ActivityResultLauncher<Array<String>>) {
|
||||
|
||||
class Factory(private val componentActivity: ComponentActivity, private val secretsImporter: SecretsImporter, private val secretsRepository: SecretsRepository) {
|
||||
|
||||
fun buildSettingScreenHandlers(): SettingsScreenController {
|
||||
return SettingsScreenController(
|
||||
apkFilePickLauncher = buildFilePickerActivityLauncher(this::importApk)
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildFilePickerActivityLauncher(onResult : (Uri?) ->Unit): ActivityResultLauncher<Array<String>> {
|
||||
return componentActivity.registerForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
onResult.invoke(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun importApk(uri: Uri?) {
|
||||
if(uri == null) {
|
||||
componentActivity.runOnUiThread {
|
||||
Toast.makeText(componentActivity, "APK Import Cancelled", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
return
|
||||
}
|
||||
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||
componentActivity.contentResolver.openInputStream(uri).use {
|
||||
if(it == null) {
|
||||
componentActivity.runOnUiThread {
|
||||
Toast.makeText(
|
||||
componentActivity,
|
||||
"Selected file is empty!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
var secrets: Secrets? = null
|
||||
try {
|
||||
secrets = secretsImporter.importSecrets(it)
|
||||
} catch (e: Exception) {
|
||||
componentActivity.runOnUiThread {
|
||||
Toast.makeText(componentActivity, "Secrets import failed. Please only select the official Vital Arena App 2.1.0 APK.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||
secretsRepository.updateSecrets(secrets)
|
||||
}.invokeOnCompletion {
|
||||
componentActivity.runOnUiThread {
|
||||
Toast.makeText(componentActivity, "Secrets successfully imported. Connections with devices are now possible.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,5 @@
|
||||
package com.github.nacabaro.vbhelper.screens
|
||||
package com.github.nacabaro.vbhelper.screens.settingsScreen
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -26,7 +22,7 @@ import com.github.nacabaro.vbhelper.components.TopBanner
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
navController: NavController,
|
||||
settingsScreenController: SettingsScreenController,
|
||||
settingsScreenController: SettingsScreenControllerImpl,
|
||||
onClickImportCard: () -> Unit
|
||||
) {
|
||||
Scaffold (
|
||||
@ -49,7 +45,14 @@ fun SettingsScreen(
|
||||
) {
|
||||
SettingsSection("NFC Communication")
|
||||
SettingsEntry(title = "Import APK", description = "Import Secrets From Vital Arean 2.1.0 APK") {
|
||||
settingsScreenController.apkFilePickLauncher.launch(arrayOf("*/*"))
|
||||
settingsScreenController.onClickImportApk()
|
||||
}
|
||||
SettingsSection("Data management")
|
||||
SettingsEntry(title = "Export data", description = "Export application database") {
|
||||
settingsScreenController.onClickOpenDirectory()
|
||||
}
|
||||
SettingsEntry(title = "Import data", description = "Import application database") {
|
||||
settingsScreenController.onClickImportDatabase()
|
||||
}
|
||||
SettingsSection("DiM/BEm management")
|
||||
SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
|
||||
@ -61,12 +64,6 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun buildFilePickLauncher(activity: ComponentActivity, onItemPicked: (Uri?) -> Unit): ActivityResultLauncher<Array<String>> {
|
||||
return activity.registerForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
onItemPicked.invoke(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsEntry(
|
||||
title: String,
|
||||
@ -0,0 +1,211 @@
|
||||
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 com.github.nacabaro.vbhelper.source.ApkSecretsImporter
|
||||
import com.github.nacabaro.vbhelper.source.SecretsImporter
|
||||
import com.github.nacabaro.vbhelper.source.SecretsRepository
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
|
||||
class SettingsScreenControllerImpl(
|
||||
private val context: ComponentActivity,
|
||||
): SettingsScreenController {
|
||||
private val roomDbName = "internalDb"
|
||||
private val filePickerLauncher: ActivityResultLauncher<String>
|
||||
private val filePickerOpenerLauncher: ActivityResultLauncher<Array<String>>
|
||||
private val filePickerApk: ActivityResultLauncher<Array<String>>
|
||||
private val secretsImporter: SecretsImporter = ApkSecretsImporter()
|
||||
private val application = context.applicationContext as VBHelper
|
||||
private val secretsRepository: SecretsRepository = application.container.dataStoreSecretsRepository
|
||||
|
||||
init {
|
||||
filePickerLauncher = context.registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/octet-stream")
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
exportDatabase(uri)
|
||||
} else {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "No destination selected", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filePickerOpenerLauncher = context.registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
importDatabase(uri)
|
||||
} else {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "No source selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filePickerApk = context.registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
importApk(uri)
|
||||
} else {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "APK import cancelled", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClickOpenDirectory() {
|
||||
filePickerLauncher.launch("My application data.vbhelper")
|
||||
}
|
||||
|
||||
override fun onClickImportDatabase() {
|
||||
filePickerOpenerLauncher.launch(arrayOf("application/octet-stream"))
|
||||
}
|
||||
|
||||
override fun onClickImportApk() {
|
||||
filePickerApk.launch(arrayOf("*/*"))
|
||||
}
|
||||
|
||||
private fun exportDatabase(destinationUri: Uri) {
|
||||
context.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
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(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
|
||||
}
|
||||
|
||||
application.container.db.close()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
private fun importApk(uri: Uri) {
|
||||
context.lifecycleScope.launch(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(uri).use {
|
||||
if(it == null) {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Selected file is empty!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
val secrets: Secrets?
|
||||
try {
|
||||
secrets = secretsImporter.importSecrets(it)
|
||||
} catch (e: Exception) {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Secrets import failed. Please only select the official Vital Arena App 2.1.0 APK.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
context.lifecycleScope.launch(Dispatchers.IO) {
|
||||
secretsRepository.updateSecrets(secrets)
|
||||
}.invokeOnCompletion {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Secrets successfully imported. Connections with devices are now possible.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user