From 5273bae7b66eba9d07771dbdeda0689d5b65d5fe Mon Sep 17 00:00:00 2001 From: Taveon Nelson <74945303+457R0@users.noreply.github.com> Date: Sat, 18 Apr 2026 07:24:06 -0500 Subject: [PATCH] Update: changes and new files from local development --- app/lint-baseline.xml | 1782 +++++++++++++++++ app/src/main/AndroidManifest.xml | 16 +- .../github/nacabaro/vbhelper/MainActivity.kt | 65 + .../github/nacabaro/vbhelper/daos/CardDao.kt | 17 +- .../nacabaro/vbhelper/daos/CardProgressDao.kt | 5 +- .../vbhelper/daos/UserCharacterDao.kt | 18 +- .../nacabaro/vbhelper/dtos/CharacterDtos.kt | 9 +- .../screens/homeScreens/HomeScreen.kt | 97 +- .../scanScreen/ChooseConnectionScreen.kt | 5 + .../screens/storageScreen/StorageDialog.kt | 10 +- .../screens/storageScreen/StorageScreen.kt | 16 + .../source/VitalWearCharacterExporter.kt | 114 ++ .../source/VitalWearCharacterImporter.kt | 163 ++ .../nacabaro/vbhelper/utils/DeviceType.kt | 12 +- .../cfogrady/vitalwear/protos/Character.proto | 51 + app/src/main/res/values/strings.xml | 7 +- app/src/main/res/xml/provider_paths.xml | 6 + 17 files changed, 2346 insertions(+), 47 deletions(-) create mode 100644 app/lint-baseline.xml create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporter.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt create mode 100644 app/src/main/proto/com/github/cfogrady/vitalwear/protos/Character.proto create mode 100644 app/src/main/res/xml/provider_paths.xml diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml new file mode 100644 index 0000000..48625e4 --- /dev/null +++ b/app/lint-baseline.xml @@ -0,0 +1,1782 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8e59f52..39fa631 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,6 +22,15 @@ android:theme="@style/Theme.VBHelper" android:usesCleartextTraffic="true" tools:targetApi="31"> + + + + + + + + - \ No newline at end of file + 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 c8f358b..fc13bae 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt @@ -1,11 +1,16 @@ package com.github.nacabaro.vbhelper +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.util.Log +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable +import androidx.lifecycle.lifecycleScope +import com.github.cfogrady.vitalwear.protos.Character import com.github.nacabaro.vbhelper.navigation.AppNavigation import com.github.nacabaro.vbhelper.di.VBHelper import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers @@ -17,7 +22,10 @@ import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenContr import com.github.nacabaro.vbhelper.screens.cardScreen.CardScreenControllerImpl import com.github.nacabaro.vbhelper.screens.spriteViewer.SpriteViewerControllerImpl import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreenControllerImpl +import com.github.nacabaro.vbhelper.source.VitalWearCharacterImporter import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { @@ -70,6 +78,7 @@ class MainActivity : ComponentActivity() { } Log.i("MainActivity", "Activity onCreated") + handleImportIntent(intent) } override fun onPause() { @@ -88,6 +97,58 @@ class MainActivity : ComponentActivity() { } } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + handleImportIntent(intent) + } + + private fun handleImportIntent(intent: Intent?) { + val importUri = extractVitalWearImportUri(intent) ?: return + val application = applicationContext as VBHelper + + lifecycleScope.launch(Dispatchers.IO) { + val result = runCatching { + contentResolver.openInputStream(importUri)?.use { inputStream -> + val character = Character.parseFrom(inputStream) + VitalWearCharacterImporter(application.container.db).importCharacter(character) + } ?: VitalWearCharacterImporter.ImportResult( + success = false, + message = "VitalWear import file could not be opened." + ) + }.getOrElse { + VitalWearCharacterImporter.ImportResult( + success = false, + message = "VitalWear import failed: ${it.message ?: "Unknown error"}" + ) + } + + runOnUiThread { + Toast.makeText(this@MainActivity, result.message, Toast.LENGTH_LONG).show() + } + } + } + + private fun extractVitalWearImportUri(intent: Intent?): Uri? { + if (intent == null) { + return null + } + + val isVitalWearImport = intent.type == VITALWEAR_CHARACTER_MIME + if (!isVitalWearImport) { + return null + } + + return when (intent.action) { + Intent.ACTION_SEND -> { + @Suppress("DEPRECATION") + intent.getParcelableExtra(Intent.EXTRA_STREAM) + } + Intent.ACTION_VIEW -> intent.data + else -> null + } + } + @Composable private fun MainApplication( scanScreenController: ScanScreenControllerImpl, @@ -112,4 +173,8 @@ class MainActivity : ComponentActivity() { ) ) } + + companion object { + private const val VITALWEAR_CHARACTER_MIME = "application/x-vitalwear-character" + } } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardDao.kt index c87a1f7..b2d61e0 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardDao.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardDao.kt @@ -19,6 +19,9 @@ interface CardDao { @Query("SELECT * FROM Card WHERE id = :id") fun getCardById(id: Long): Card? + @Query("SELECT * FROM Card WHERE LOWER(name) = LOWER(:name) LIMIT 1") + fun getCardByName(name: String): Card? + @Query( """ SELECT ca.* @@ -30,6 +33,18 @@ interface CardDao { ) fun getCardByCharacterId(id: Long): Flow + @Query( + """ + SELECT ca.* + FROM Card ca + JOIN CardCharacter ch ON ca.id = ch.cardId + JOIN UserCharacter uc ON ch.id = uc.charId + WHERE uc.id = :id + LIMIT 1 + """ + ) + fun getCardByCharacterIdSync(id: Long): Card? + @Query("UPDATE Card SET name = :newName WHERE id = :id") suspend fun renameCard(id: Int, newName: String) @@ -46,4 +61,4 @@ interface CardDao { WHERE cc.id = :charaId """) fun getCardIconByCharaId(charaId: Long): Flow -} \ No newline at end of file +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt index 3fde614..93d86c3 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt @@ -24,6 +24,9 @@ interface CardProgressDao { ) fun getCardProgress(cardId: Long): Flow + @Query("SELECT currentStage FROM CardProgress WHERE cardId = :cardId LIMIT 1") + fun getCardProgressSync(cardId: Long): Int? + @Insert fun insertCardProgress(cardProgress: CardProgress) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/UserCharacterDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/UserCharacterDao.kt index 986ce01..284bca9 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/UserCharacterDao.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/UserCharacterDao.kt @@ -55,6 +55,22 @@ interface UserCharacterDao { ) fun getTransformationHistory(monId: Long): Flow> + @Query( + """ + SELECT + t.stageId as stageId, + c.charaIndex AS monIndex, + ca.name as cardName, + t.transformationDate AS transformationDate + FROM TransformationHistory t + JOIN CardCharacter c ON c.id = t.stageId + JOIN Card ca ON ca.id = c.cardId + WHERE t.monId = :monId + ORDER BY t.transformationDate ASC, t.id ASC + """ + ) + suspend fun getTransformationHistoryForExport(monId: Long): List + @Query( """ SELECT @@ -234,4 +250,4 @@ interface UserCharacterDao { """ ) suspend fun getVBDimCharacters(): List -} \ No newline at end of file +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt b/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt index 3cb7831..0131e6f 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt @@ -52,6 +52,13 @@ object CharacterDtos { val transformationDate: Long ) + data class TransformationHistoryExport( + val stageId: Long, + val monIndex: Int, + val cardName: String, + val transformationDate: Long + ) + data class CardCharaProgress( val id: Long, val spriteIdle: ByteArray, @@ -125,4 +132,4 @@ object CharacterDtos { val discoveredOn: Long?, val fusionAttribute: NfcCharacter.Attribute ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt index 0cacb5d..37551e8 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt @@ -2,6 +2,7 @@ package com.github.nacabaro.vbhelper.screens.homeScreens import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -40,6 +41,8 @@ import com.github.nacabaro.vbhelper.source.StorageRepository import com.github.nacabaro.vbhelper.R import com.github.nacabaro.vbhelper.dtos.CardDtos import com.github.nacabaro.vbhelper.source.CardRepository +import com.github.nacabaro.vbhelper.source.VitalWearCharacterExporter +import android.widget.Toast import com.github.nacabaro.vbhelper.utils.BitmapData import kotlinx.coroutines.flow.flowOf import kotlin.collections.emptyList @@ -136,42 +139,66 @@ fun HomeScreen( Text(text = stringResource(R.string.adventure_empty_state)) } } else { - val cardIcon = BitmapData( - bitmap = cardIconData!!.cardIcon, - width = cardIconData!!.cardIconWidth, - height = cardIconData!!.cardIconHeight - ) + Column(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) { + val cardIcon = BitmapData( + bitmap = cardIconData!!.cardIcon, + width = cardIconData!!.cardIconWidth, + height = cardIconData!!.cardIconHeight + ) - if (activeMon!!.isBemCard && beData != null) { - BEBEmHomeScreen( - activeMon = activeMon!!, - beData = beData!!, - transformationHistory = transformationHistory, - contentPadding = contentPadding, - cardIcon = cardIcon - ) - } else if (!activeMon!!.isBemCard && activeMon!!.characterType == DeviceType.BEDevice && beData != null) { - BEDiMHomeScreen( - activeMon = activeMon!!, - beData = beData!!, - transformationHistory = transformationHistory, - contentPadding = contentPadding, - cardIcon = cardIcon - ) - } else if (vbData != null) { - VBDiMHomeScreen( - activeMon = activeMon!!, - vbData = vbData!!, - transformationHistory = transformationHistory, - contentPadding = contentPadding, - specialMissions = vbSpecialMissions, - homeScreenController = homeScreenController, - onClickCollect = { item, currency -> - collectedItem = item - collectedCurrency = currency + if (activeMon!!.isBemCard && beData != null) { + BEBEmHomeScreen( + activeMon = activeMon!!, + beData = beData!!, + transformationHistory = transformationHistory, + contentPadding = PaddingValues(0.dp), + cardIcon = cardIcon + ) + } else if (!activeMon!!.isBemCard && activeMon!!.characterType == DeviceType.BEDevice && beData != null) { + BEDiMHomeScreen( + activeMon = activeMon!!, + beData = beData!!, + transformationHistory = transformationHistory, + contentPadding = PaddingValues(0.dp), + cardIcon = cardIcon + ) + } else if (vbData != null) { + VBDiMHomeScreen( + activeMon = activeMon!!, + vbData = vbData!!, + transformationHistory = transformationHistory, + contentPadding = PaddingValues(0.dp), + specialMissions = vbSpecialMissions, + homeScreenController = homeScreenController, + onClickCollect = { item, currency -> + collectedItem = item + collectedCurrency = currency + }, + cardIcon = cardIcon + ) + } + + Button( + onClick = { + try { + val intent = VitalWearCharacterExporter(application, application.container.db) + .buildShareIntent(activeMon!!.id) + .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK) + application.startActivity(intent) + } catch (e: Exception) { + Toast.makeText( + application, + "Could not send character to VitalWear: ${e.message}", + Toast.LENGTH_LONG + ).show() + } }, - cardIcon = cardIcon - ) + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text(text = "Send to VitalWear") + } } } } @@ -221,5 +248,3 @@ fun HomeScreen( } } } - - diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ChooseConnectionScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ChooseConnectionScreen.kt index d2025aa..d5fe878 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ChooseConnectionScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ChooseConnectionScreen.kt @@ -42,6 +42,11 @@ fun ChooseConnectOption( .fillMaxSize() .padding(contentPadding) ) { + Text( + text = "Bandai Toys = Vital Bracelet, Vital Hero, BE Bracelet", + fontSize = 14.sp, + modifier = Modifier.padding(bottom = 24.dp) + ) ScanButton( text = stringResource(R.string.scan_vb_to_app), disabled = onClickRead == null, diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageDialog.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageDialog.kt index 33ac860..979f47d 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageDialog.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageDialog.kt @@ -43,6 +43,7 @@ fun StorageDialog( onDismissRequest: () -> Unit, onClickDelete: () -> Unit, onSendToBracelet: () -> Unit, + onSendToVitalWear: () -> Unit, onClickSetActive: () -> Unit, onClickSendToAdventure: (time: Long) -> Unit ) { @@ -136,6 +137,13 @@ fun StorageDialog( Text(text = stringResource(R.string.storage_set_active)) } } + Button( + onClick = onSendToVitalWear, + modifier = Modifier + .fillMaxWidth() + ) { + Text(text = stringResource(R.string.storage_send_to_vitalwear)) + } Button( onClick = { onSendToAdventureClicked = true @@ -171,4 +179,4 @@ fun StorageDialog( onDismissRequest = { onSendToAdventureClicked = false } ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt index e33e6dd..e5e770d 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt @@ -33,6 +33,7 @@ import com.github.nacabaro.vbhelper.di.VBHelper import com.github.nacabaro.vbhelper.navigation.NavigationItems import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenControllerImpl import com.github.nacabaro.vbhelper.source.StorageRepository +import com.github.nacabaro.vbhelper.source.VitalWearCharacterExporter import com.github.nacabaro.vbhelper.utils.BitmapData import androidx.compose.ui.res.stringResource import com.github.nacabaro.vbhelper.R @@ -136,6 +137,21 @@ fun StorageScreen( ) ) }, + onSendToVitalWear = { + try { + val intent = VitalWearCharacterExporter(application, application.container.db) + .buildShareIntent(selectedCharacter!!) + .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK) + application.startActivity(intent) + selectedCharacter = null + } catch (e: Exception) { + Toast.makeText( + application, + "Could not send character to VitalWear: ${e.message}", + Toast.LENGTH_LONG + ).show() + } + }, onClickSendToAdventure = { time -> adventureScreenController .sendCharacterToAdventure( diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporter.kt new file mode 100644 index 0000000..bc3ad95 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporter.kt @@ -0,0 +1,114 @@ +package com.github.nacabaro.vbhelper.source + +import android.content.ClipData +import android.content.Context +import android.content.Intent +import androidx.core.content.FileProvider +import com.github.cfogrady.vitalwear.protos.Character +import com.github.nacabaro.vbhelper.database.AppDatabase +import com.github.nacabaro.vbhelper.utils.DeviceType +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking +import java.io.File + +class VitalWearCharacterExporter( + private val context: Context, + private val database: AppDatabase +) { + fun buildShareIntent(characterId: Long): Intent { + return runBlocking { + val characterWithSprites = database.userCharacterDao().getCharacterWithSprites(characterId) + val userCharacter = database.userCharacterDao().getCharacter(characterId) + val card = database.cardDao().getCardByCharacterIdSync(characterId) + ?: error("Card not found for character $characterId") + val cardProgress = database.cardProgressDao().getCardProgressSync(card.id) ?: 0 + val proto = Character.newBuilder() + .setCardId(card.cardId) + .setCardName(card.name) + .setCharacterStats( + Character.CharacterStats.newBuilder() + .setSlotId(database.userCharacterDao().getCharacterInfo(characterId).charaIndex) + .setVitals(characterWithSprites.vitalPoints) + .setTrainingTimeRemainingInSeconds(resolveTrainingSeconds(characterId, userCharacter.characterType)) + .setTimeUntilNextTransformation(characterWithSprites.transformationCountdown.toLong() * 60L) + .setTrainedBp(resolveTrainedBp(characterId, userCharacter.characterType)) + .setTrainedHp(resolveTrainedHp(characterId, userCharacter.characterType)) + .setTrainedAp(resolveTrainedAp(characterId, userCharacter.characterType)) + .setTrainedPp(characterWithSprites.trophies) + .setInjured(characterWithSprites.injuryStatus.name.lowercase().contains("inj")) + .setAccumulatedDailyInjuries(0) + .setTotalBattles(characterWithSprites.totalBattlesWon + characterWithSprites.totalBattlesLost) + .setCurrentPhaseBattles(characterWithSprites.currentPhaseBattlesWon + characterWithSprites.currentPhaseBattlesLost) + .setTotalWins(characterWithSprites.totalBattlesWon) + .setCurrentPhaseWins(characterWithSprites.currentPhaseBattlesWon) + .setMood(characterWithSprites.mood) + .build() + ) + .setSettings( + Character.Settings.newBuilder() + .setTrainingInBackground(false) + .setAllowedBattles(Character.Settings.AllowedBattles.CARD_ONLY) + .build() + ) + .putMaxAdventureCompletedByCard(card.name, (cardProgress - 1).coerceAtLeast(0)) + .addAllTransformationHistory( + database.userCharacterDao().getTransformationHistoryForExport(characterId).map { + Character.TransformationEvent.newBuilder() + .setCardName(it.cardName) + .setPhase(0) + .setSlotId(it.monIndex) + .build() + } + ) + .build() + + val exportDir = File(context.cacheDir, "exports").apply { mkdirs() } + val exportFile = File(exportDir, "vbhelper_character_$characterId.vitalwear") + exportFile.writeBytes(proto.toByteArray()) + val exportUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", exportFile) + + Intent(Intent.ACTION_SEND).apply { + `package` = "com.github.cfogrady.vitalwear" + type = VITALWEAR_CHARACTER_MIME + putExtra(Intent.EXTRA_STREAM, exportUri) + clipData = ClipData.newRawUri("", exportUri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + } + } + + private fun resolveTrainingSeconds(characterId: Long, deviceType: DeviceType): Long { + if (deviceType != DeviceType.BEDevice) { + return 0L + } + return database.userCharacterDao().getBeData(characterId).valueOrNull()?.remainingTrainingTimeInMinutes?.toLong()?.times(60L) ?: 0L + } + + private fun resolveTrainedBp(characterId: Long, deviceType: DeviceType): Int { + return if (deviceType == DeviceType.BEDevice) { + database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingBp ?: 0 + } else 0 + } + + private fun resolveTrainedHp(characterId: Long, deviceType: DeviceType): Int { + return if (deviceType == DeviceType.BEDevice) { + database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingHp ?: 0 + } else 0 + } + + private fun resolveTrainedAp(characterId: Long, deviceType: DeviceType): Int { + return if (deviceType == DeviceType.BEDevice) { + database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingAp ?: 0 + } else 0 + } + + private fun kotlinx.coroutines.flow.Flow.valueOrNull(): T? { + return runBlocking { + firstOrNull() + } + } + + companion object { + const val VITALWEAR_CHARACTER_MIME = "application/x-vitalwear-character" + } +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt new file mode 100644 index 0000000..6ec3b65 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt @@ -0,0 +1,163 @@ +package com.github.nacabaro.vbhelper.source + +import com.github.cfogrady.vbnfc.data.NfcCharacter +import com.github.cfogrady.vitalwear.protos.Character +import com.github.nacabaro.vbhelper.database.AppDatabase +import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData +import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter +import com.github.nacabaro.vbhelper.utils.DeviceType +import kotlin.math.max + +class VitalWearCharacterImporter( + private val database: AppDatabase +) { + data class ImportResult( + val success: Boolean, + val message: String + ) + + fun importCharacter(character: Character): ImportResult { + val importedCard = resolveCard(character) + ?: return ImportResult( + success = false, + message = "Matching card not found in VBHelper. Import that card first." + ) + + val slotId = character.characterStats.slotId + val cardCharacter = runCatching { + database.characterDao().getCharacterByMonIndex(slotId, importedCard.id) + }.getOrNull() ?: return ImportResult( + success = false, + message = "Character slot $slotId was not found on card ${importedCard.name}." + ) + + database.userCharacterDao().clearActiveCharacter() + + val totalBattles = character.characterStats.totalBattles + val totalWins = character.characterStats.totalWins.coerceAtMost(totalBattles) + val currentPhaseBattles = character.characterStats.currentPhaseBattles + val currentPhaseWins = character.characterStats.currentPhaseWins.coerceAtMost(currentPhaseBattles) + + val userCharacterId = database.userCharacterDao().insertCharacterData( + UserCharacter( + charId = cardCharacter.id, + ageInDays = max(character.transformationHistoryCount - 1, 0), + mood = character.characterStats.mood, + vitalPoints = character.characterStats.vitals, + transformationCountdown = secondsToMinutes(character.characterStats.timeUntilNextTransformation), + injuryStatus = resolveInjuryStatus(character.characterStats.injured), + trophies = character.characterStats.trainedPp, + currentPhaseBattlesWon = currentPhaseWins, + currentPhaseBattlesLost = (currentPhaseBattles - currentPhaseWins).coerceAtLeast(0), + totalBattlesWon = totalWins, + totalBattlesLost = (totalBattles - totalWins).coerceAtLeast(0), + activityLevel = 0, + heartRateCurrent = 0, + characterType = DeviceType.BEDevice, + isActive = true + ) + ) + + database.userCharacterDao().insertBECharacterData( + BECharacterData( + id = userCharacterId, + trainingHp = character.characterStats.trainedHp, + trainingAp = character.characterStats.trainedAp, + trainingBp = character.characterStats.trainedBp, + remainingTrainingTimeInMinutes = secondsToMinutes(character.characterStats.trainingTimeRemainingInSeconds), + itemEffectMentalStateValue = 0, + itemEffectMentalStateMinutesRemaining = 0, + itemEffectActivityLevelValue = 0, + itemEffectActivityLevelMinutesRemaining = 0, + itemEffectVitalPointsChangeValue = 0, + itemEffectVitalPointsChangeMinutesRemaining = 0, + abilityRarity = resolveDefaultAbilityRarity(), + abilityType = 0, + abilityBranch = 0, + abilityReset = 0, + rank = 0, + itemType = 0, + itemMultiplier = 0, + itemRemainingTime = 0, + otp0 = "", + otp1 = "", + minorVersion = 0, + majorVersion = 0 + ) + ) + + val now = System.currentTimeMillis() + database.dexDao().insertCharacter(slotId, importedCard.id, now) + + for (transformation in character.transformationHistoryList) { + val transformationCard = resolveCard(transformation.cardName, character.cardId) + if (transformationCard != null) { + database.userCharacterDao().insertTransformation( + userCharacterId, + transformation.slotId, + transformationCard.id, + now + ) + database.dexDao().insertCharacter(transformation.slotId, transformationCard.id, now) + } + } + + for ((cardName, maxAdventureCompleted) in character.maxAdventureCompletedByCardMap) { + val adventureCard = resolveCard(cardName, null) ?: continue + val currentStage = (maxAdventureCompleted + 1).coerceAtLeast(0) + database.cardProgressDao().updateCardProgress( + currentStage = currentStage, + cardId = adventureCard.id, + unlocked = currentStage > adventureCard.stageCount + ) + } + + return ImportResult( + success = true, + message = "Imported ${importedCard.name} slot $slotId from VitalWear." + ) + } + + private fun resolveCard(character: Character) = resolveCard(character.cardName, character.cardId) + + private fun resolveCard(cardName: String?, cardId: Int?): com.github.nacabaro.vbhelper.domain.card.Card? { + if (!cardName.isNullOrBlank()) { + database.cardDao().getCardByName(cardName)?.let { return it } + } + + if (cardId != null) { + val matches = database.cardDao().getCardByCardId(cardId) + if (matches.size == 1) { + return matches.first() + } + } + + return null + } + + private fun secondsToMinutes(seconds: Long): Int { + if (seconds <= 0L) { + return 0 + } + return (seconds / 60L).coerceAtMost(Int.MAX_VALUE.toLong()).toInt() + } + + private fun resolveInjuryStatus(injured: Boolean): NfcCharacter.InjuryStatus { + val statuses = enumValues() + if (!injured) { + return statuses.firstOrNull { + val normalized = it.name.lowercase() + normalized.contains("none") || normalized.contains("normal") || normalized.contains("healthy") + } ?: statuses.first() + } + + return statuses.firstOrNull { + val normalized = it.name.lowercase() + normalized.contains("inj") + } ?: statuses.last() + } + + private fun resolveDefaultAbilityRarity(): NfcCharacter.AbilityRarity { + return enumValues().first() + } +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/utils/DeviceType.kt b/app/src/main/java/com/github/nacabaro/vbhelper/utils/DeviceType.kt index 5d06c67..7912e82 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/utils/DeviceType.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/utils/DeviceType.kt @@ -1,6 +1,14 @@ package com.github.nacabaro.vbhelper.utils enum class DeviceType { - VBDevice, - BEDevice + VBDevice, // Bandai Vital Bracelet + BEDevice, // Bandai BE Bracelet + VitalWear // WearOS VitalWear app +} + +fun detectDeviceType(deviceName: String): DeviceType = when { + deviceName.startsWith("VitalWear", ignoreCase = true) || deviceName.startsWith("VW-", ignoreCase = true) -> DeviceType.VitalWear + deviceName.startsWith("BE-", ignoreCase = true) -> DeviceType.BEDevice + deviceName.startsWith("VB-", ignoreCase = true) -> DeviceType.VBDevice + else -> DeviceType.VBDevice // Default/fallback, can be refined } \ No newline at end of file diff --git a/app/src/main/proto/com/github/cfogrady/vitalwear/protos/Character.proto b/app/src/main/proto/com/github/cfogrady/vitalwear/protos/Character.proto new file mode 100644 index 0000000..263b4d8 --- /dev/null +++ b/app/src/main/proto/com/github/cfogrady/vitalwear/protos/Character.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +option java_package = "com.github.cfogrady.vitalwear.protos"; +option java_multiple_files = true; + +message Character { + string card_name = 1; + int32 card_id = 2; + + message CharacterStats { + int32 slot_id = 1; + int32 vitals = 2; + int64 training_time_remaining_in_seconds = 3; + int64 time_until_next_transformation = 4; + int32 trained_bp = 5; + int32 trained_hp = 6; + int32 trained_ap = 7; + int32 trained_pp = 8; + bool injured = 9; + int32 accumulated_daily_injuries = 10; + int32 total_battles = 11; + int32 current_phase_battles = 12; + int32 total_wins = 13; + int32 current_phase_wins = 14; + int32 mood = 15; + } + CharacterStats character_stats = 3; + + message Settings { + bool training_in_background = 1; + enum AllowedBattles { + CARD_ONLY = 0; + ALL_FRANCHISE = 1; + ALL_FRANCHISE_AND_DIM = 2; + ALL = 3; + } + AllowedBattles allowed_battles = 2; + optional int32 assumed_franchise = 3; + } + Settings settings = 4; + + message TransformationEvent { + string card_name = 1; + int32 phase = 2; + int32 slot_id = 3; + } + + repeated TransformationEvent transformation_history = 5; + + map max_adventure_completed_by_card = 6; +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5c51fd..886fb53 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,8 +48,8 @@ Scan a Vital Bracelet - Vital Bracelet to App - App to Vital Bracelet + Vital Bracelet to App (Receive Digimon from Bandai Toys: Vital Bracelet, Vital Hero, BE Bracelet) + App to Vital Bracelet (Send Digimon from VBHelper to Bandai Toys: Vital Bracelet, Vital Hero, BE Bracelet) No NFC on device! Tag detected is not VB @@ -199,6 +199,7 @@ Character image Send to watch + Send to VitalWear Set active Send on adventure Delete character @@ -226,4 +227,4 @@ Dismiss Remove - \ No newline at end of file + diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..8a3c842 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ + + + +