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 @@
+
+
+
+