mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-07-30 00:31:54 +00:00
VBH is the companion app now
Got tired of having to reinstall all three apps so i ported the companion apps features to vitalwear, now you only need VBH and Vitalwear. I've also added emblems to items in icons so user isnt confused.
This commit is contained in:
parent
f829efae68
commit
8fd4f6c414
@ -54,6 +54,7 @@
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.VBHelper">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@ -19,11 +19,14 @@ 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.ui.theme.VBHelperTheme
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()
|
||||
private var initialRoute: String? = null
|
||||
private val deepLinkNavigationEvents = MutableSharedFlow<String>(extraBufferCapacity = 1)
|
||||
|
||||
private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
|
||||
if( onActivityLifecycleListeners[key] != null) {
|
||||
@ -57,6 +60,7 @@ class MainActivity : ComponentActivity() {
|
||||
enableEdgeToEdge()
|
||||
|
||||
initialRoute = getInitialRouteFromIntent(intent)
|
||||
initialRoute?.let { deepLinkNavigationEvents.tryEmit(it) }
|
||||
|
||||
setContent {
|
||||
VBHelperTheme {
|
||||
@ -69,7 +73,8 @@ class MainActivity : ComponentActivity() {
|
||||
storageScreenController = storageScreenController,
|
||||
spriteViewerController = spriteViewerController,
|
||||
cardScreenController = cardScreenController,
|
||||
initialRoute = initialRoute
|
||||
initialRoute = initialRoute,
|
||||
deepLinkNavigationEvents = deepLinkNavigationEvents.asSharedFlow(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -97,6 +102,7 @@ class MainActivity : ComponentActivity() {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
initialRoute = getInitialRouteFromIntent(intent)
|
||||
initialRoute?.let { deepLinkNavigationEvents.tryEmit(it) }
|
||||
}
|
||||
|
||||
private fun getInitialRouteFromIntent(intent: Intent?): String? {
|
||||
@ -120,7 +126,8 @@ class MainActivity : ComponentActivity() {
|
||||
homeScreenController: HomeScreenControllerImpl,
|
||||
spriteViewerController: SpriteViewerControllerImpl,
|
||||
cardScreenController: CardScreenControllerImpl,
|
||||
initialRoute: String? = null
|
||||
initialRoute: String? = null,
|
||||
deepLinkNavigationEvents: kotlinx.coroutines.flow.Flow<String>,
|
||||
) {
|
||||
AppNavigation(
|
||||
applicationNavigationHandlers = AppNavigationHandlers(
|
||||
@ -133,7 +140,8 @@ class MainActivity : ComponentActivity() {
|
||||
spriteViewerController,
|
||||
cardScreenController
|
||||
),
|
||||
initialRoute = initialRoute
|
||||
initialRoute = initialRoute,
|
||||
navigationEvents = deepLinkNavigationEvents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.CharacterTransferPolicy
|
||||
|
||||
@Dao
|
||||
interface CharacterTransferPolicyDao {
|
||||
@Query("SELECT * FROM CharacterTransferPolicy WHERE characterId = :characterId")
|
||||
fun getByCharacterId(characterId: Long): CharacterTransferPolicy?
|
||||
|
||||
@Upsert
|
||||
fun upsert(policy: CharacterTransferPolicy)
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.github.nacabaro.vbhelper.daos.AdventureDao
|
||||
import com.github.nacabaro.vbhelper.daos.CardAdventureDao
|
||||
import com.github.nacabaro.vbhelper.daos.CharacterDao
|
||||
import com.github.nacabaro.vbhelper.daos.CharacterTransferPolicyDao
|
||||
import com.github.nacabaro.vbhelper.daos.DexDao
|
||||
import com.github.nacabaro.vbhelper.daos.CardDao
|
||||
import com.github.nacabaro.vbhelper.daos.CardFusionsDao
|
||||
@ -27,6 +28,7 @@ import com.github.nacabaro.vbhelper.domain.characters.Sprite
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Adventure
|
||||
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.CharacterTransferPolicy
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
||||
@ -36,7 +38,7 @@ import com.github.nacabaro.vbhelper.domain.device_data.VitalWearCharacterSetting
|
||||
import com.github.nacabaro.vbhelper.domain.items.Items
|
||||
|
||||
@Database(
|
||||
version = 5,
|
||||
version = 6,
|
||||
entities = [
|
||||
Card::class,
|
||||
CardProgress::class,
|
||||
@ -50,6 +52,7 @@ import com.github.nacabaro.vbhelper.domain.items.Items
|
||||
SpecialMissions::class,
|
||||
TransformationHistory::class,
|
||||
VitalsHistory::class,
|
||||
CharacterTransferPolicy::class,
|
||||
Dex::class,
|
||||
Items::class,
|
||||
Adventure::class,
|
||||
@ -71,6 +74,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun cardAdventureDao(): CardAdventureDao
|
||||
abstract fun cardFusionsDao(): CardFusionsDao
|
||||
abstract fun vitalWearSettingsDao(): VitalWearSettingsDao
|
||||
abstract fun characterTransferPolicyDao(): CharacterTransferPolicyDao
|
||||
|
||||
companion object {
|
||||
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
@ -139,5 +143,27 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_5_6 = object : Migration(5, 6) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `CharacterTransferPolicy` (
|
||||
`characterId` INTEGER NOT NULL,
|
||||
`nativeDeviceType` TEXT NOT NULL,
|
||||
`preferredHceExportFormat` TEXT NOT NULL,
|
||||
`preferredNfcaExportFormat` TEXT NOT NULL,
|
||||
`lastObservedImportFormat` TEXT,
|
||||
`lastTransferTransport` TEXT,
|
||||
`lastTransferTarget` TEXT,
|
||||
`preserveVbRoundTrip` INTEGER NOT NULL,
|
||||
`preserveBeRoundTrip` INTEGER NOT NULL,
|
||||
PRIMARY KEY(`characterId`),
|
||||
FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.github.nacabaro.vbhelper.domain.device_data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.transfer.ExportFormat
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTarget
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTransport
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = UserCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["characterId"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
)
|
||||
]
|
||||
)
|
||||
data class CharacterTransferPolicy(
|
||||
@PrimaryKey val characterId: Long,
|
||||
val nativeDeviceType: DeviceType,
|
||||
val preferredHceExportFormat: ExportFormat,
|
||||
val preferredNfcaExportFormat: ExportFormat,
|
||||
val lastObservedImportFormat: ExportFormat?,
|
||||
val lastTransferTransport: TransferTransport?,
|
||||
val lastTransferTarget: TransferTarget?,
|
||||
val preserveVbRoundTrip: Boolean,
|
||||
val preserveBeRoundTrip: Boolean,
|
||||
)
|
||||
|
||||
@ -6,6 +6,7 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -39,6 +40,7 @@ import com.github.nacabaro.vbhelper.screens.settingsScreen.CreditsScreen
|
||||
import com.github.nacabaro.vbhelper.screens.spriteViewer.SpriteViewerControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.source.StorageRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
data class AppNavigationHandlers(
|
||||
val settingsScreenController: SettingsScreenControllerImpl,
|
||||
@ -54,10 +56,23 @@ data class AppNavigationHandlers(
|
||||
@Composable
|
||||
fun AppNavigation(
|
||||
applicationNavigationHandlers: AppNavigationHandlers,
|
||||
initialRoute: String? = null
|
||||
initialRoute: String? = null,
|
||||
navigationEvents: Flow<String>? = null,
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
LaunchedEffect(navController, navigationEvents) {
|
||||
navigationEvents?.collect { route ->
|
||||
navController.navigate(route) {
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
popUpTo(navController.graph.startDestinationId) {
|
||||
saveState = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomNavigationBar(navController = navController)
|
||||
|
||||
@ -31,9 +31,32 @@ fun BottomNavigationBar(navController: NavController) {
|
||||
selected = currentRoute == item.route,
|
||||
onClick = {
|
||||
if (item == NavigationItems.Home) {
|
||||
// Home should always show the Home root, not a restored nested route
|
||||
// like Settings that was opened from Home previously.
|
||||
val poppedToHome = navController.popBackStack(
|
||||
NavigationItems.Home.route,
|
||||
inclusive = false,
|
||||
)
|
||||
if (!poppedToHome) {
|
||||
navController.navigate(NavigationItems.Home.route) {
|
||||
popUpTo(0) { inclusive = false }
|
||||
popUpTo(navController.graph.startDestinationId) { inclusive = false }
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
} else if (item == NavigationItems.Storage) {
|
||||
// Adventure is launched from Storage; tapping Storage again should always
|
||||
// bring the user back to the Storage root instead of appearing stuck.
|
||||
val poppedToStorage = navController.popBackStack(
|
||||
NavigationItems.Storage.route,
|
||||
inclusive = false,
|
||||
)
|
||||
if (!poppedToStorage) {
|
||||
navController.navigate(NavigationItems.Storage.route) {
|
||||
popUpTo(navController.graph.startDestinationId) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
navController.navigate(item.route) {
|
||||
|
||||
@ -22,9 +22,12 @@ import com.github.nacabaro.vbhelper.screens.scanScreen.converters.FromNfcConvert
|
||||
import com.github.nacabaro.vbhelper.screens.scanScreen.converters.ToNfcConverter
|
||||
import com.github.nacabaro.vbhelper.source.VitalWearCharacterExporter
|
||||
import com.github.nacabaro.vbhelper.source.VitalWearCharacterImporter
|
||||
import com.github.nacabaro.vbhelper.transfer.ExportFormat
|
||||
import com.github.nacabaro.vbhelper.source.getCryptographicTransformerMap
|
||||
import com.github.nacabaro.vbhelper.source.isMissingSecrets
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTarget
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTransport
|
||||
import com.github.nacabaro.vbhelper.transfer.hce.VitalWearHceReaderClient
|
||||
import com.github.nacabaro.vbhelper.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -213,7 +216,42 @@ class ScanScreenControllerImpl(
|
||||
onComplete.invoke(ScanScreenController.WriteResult.MOVE_CONFIRMED)
|
||||
componentActivity.getString(R.string.scan_sent_character_success)
|
||||
} catch (writeError: Throwable) {
|
||||
Log.e("NFC_WRITE_A", "NFC-A write failed; attempting opposite direction", writeError)
|
||||
Log.e("NFC_WRITE_A", "NFC-A write failed; trying alternate payload format", writeError)
|
||||
val characterId = lastRequestedCharacterId
|
||||
val alternateFormat = when (nfcCharacter) {
|
||||
is BENfcCharacter -> ExportFormat.VB
|
||||
is VBNfcCharacter -> ExportFormat.BE
|
||||
else -> null
|
||||
}
|
||||
|
||||
val alternateSent = if (characterId != null && alternateFormat != null) {
|
||||
runCatching {
|
||||
val alternateCharacter = runBlocking {
|
||||
ToNfcConverter(componentActivity = componentActivity).characterToNfc(
|
||||
characterId = characterId,
|
||||
target = TransferTarget.REAL_BRACELET,
|
||||
transport = TransferTransport.NFCA,
|
||||
forcedFormat = alternateFormat,
|
||||
)
|
||||
}
|
||||
when (alternateCharacter) {
|
||||
is VBNfcCharacter -> tagCommunicator.sendCharacter(alternateCharacter)
|
||||
is BENfcCharacter -> tagCommunicator.sendCharacter(alternateCharacter)
|
||||
}
|
||||
true
|
||||
}.getOrElse { alternateError ->
|
||||
Log.e("NFC_WRITE_A", "Alternate payload write failed", alternateError)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (alternateSent) {
|
||||
onComplete.invoke(ScanScreenController.WriteResult.MOVE_CONFIRMED)
|
||||
"Sent character after converting payload for the tapped bracelet."
|
||||
} else {
|
||||
Log.e("NFC_WRITE_A", "NFC-A write failed; attempting opposite direction fallback", writeError)
|
||||
runCatching {
|
||||
val receivedCharacter = tagCommunicator.receiveCharacter()
|
||||
val fallbackMessage = importCharacterFromNfcAFallback(receivedCharacter)
|
||||
@ -224,6 +262,7 @@ class ScanScreenControllerImpl(
|
||||
componentActivity.getString(R.string.scan_error_generic)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isoDepHandler = { isoDep ->
|
||||
val characterId = lastRequestedCharacterId
|
||||
@ -707,7 +746,11 @@ class ScanScreenControllerImpl(
|
||||
|
||||
override suspend fun characterToNfc(characterId: Long): NfcCharacter {
|
||||
lastRequestedCharacterId = characterId
|
||||
val character = ToNfcConverter(componentActivity = componentActivity).characterToNfc(characterId)
|
||||
val character = ToNfcConverter(componentActivity = componentActivity).characterToNfc(
|
||||
characterId = characterId,
|
||||
target = TransferTarget.REAL_BRACELET,
|
||||
transport = TransferTransport.NFCA,
|
||||
)
|
||||
Log.d("CharacterType", character.toString())
|
||||
return character
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@ import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VitalsHistory
|
||||
import com.github.nacabaro.vbhelper.transfer.CharacterTransferPolicyResolver
|
||||
import com.github.nacabaro.vbhelper.transfer.ExportFormat
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
@ -20,6 +22,7 @@ class FromNfcConverter (
|
||||
private val application = componentActivity.applicationContext as VBHelper
|
||||
private val database = application.container.db
|
||||
private val transferSeenDao = application.container.transferSeenDao
|
||||
private val transferPolicyResolver = CharacterTransferPolicyResolver(database.characterTransferPolicyDao())
|
||||
|
||||
|
||||
fun addCharacterUsingCard(
|
||||
@ -134,6 +137,18 @@ class FromNfcConverter (
|
||||
)
|
||||
}
|
||||
|
||||
database
|
||||
.characterTransferPolicyDao()
|
||||
.upsert(
|
||||
transferPolicyResolver.policyForNfcaImport(
|
||||
characterId = characterId,
|
||||
observedFormat = when (nfcCharacter) {
|
||||
is BENfcCharacter -> ExportFormat.BE
|
||||
else -> ExportFormat.VB
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
addTransformationHistoryToDatabase(
|
||||
characterId = characterId,
|
||||
nfcCharacter = nfcCharacter,
|
||||
|
||||
@ -15,6 +15,10 @@ import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
import com.github.nacabaro.vbhelper.transfer.CharacterTransferPolicyResolver
|
||||
import com.github.nacabaro.vbhelper.transfer.ExportFormat
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTarget
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTransport
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
@ -30,11 +34,15 @@ class ToNfcConverter(
|
||||
|
||||
private val application: VBHelper = componentActivity.applicationContext as VBHelper
|
||||
private val database: AppDatabase = application.container.db
|
||||
private val transferPolicyResolver = CharacterTransferPolicyResolver(database.characterTransferPolicyDao())
|
||||
|
||||
|
||||
|
||||
suspend fun characterToNfc(
|
||||
characterId: Long
|
||||
characterId: Long,
|
||||
target: TransferTarget = TransferTarget.REAL_BRACELET,
|
||||
transport: TransferTransport = TransferTransport.NFCA,
|
||||
forcedFormat: ExportFormat? = null,
|
||||
): NfcCharacter {
|
||||
val app = componentActivity.applicationContext as VBHelper
|
||||
val database = app.container.db
|
||||
@ -46,15 +54,20 @@ class ToNfcConverter(
|
||||
val characterInfo = database
|
||||
.characterDao()
|
||||
.getCharacterInfo(userCharacter.charId)
|
||||
val card = database
|
||||
.cardDao()
|
||||
.getCardByCharacterIdSync(characterId)
|
||||
?: error("Card not found for character $characterId")
|
||||
|
||||
// Use the character's own type as the authoritative signal for encoding.
|
||||
// The card's isBEm flag only indicates the *card* is a BEM card, not that the
|
||||
// stored UserCharacter has a BECharacterData row. Mixing those two signals was
|
||||
// the root cause of a NoSuchElementException when a VB-type character was
|
||||
// associated with a BE-type card.
|
||||
val shouldEncodeAsBem = userCharacter.characterType == DeviceType.BEDevice
|
||||
val exportFormat = forcedFormat ?: transferPolicyResolver.resolveExportFormat(
|
||||
characterId = characterId,
|
||||
characterType = userCharacter.characterType,
|
||||
transport = transport,
|
||||
target = target,
|
||||
isBemCard = card.isBEm,
|
||||
)
|
||||
|
||||
return if (shouldEncodeAsBem)
|
||||
return if (exportFormat == ExportFormat.BE)
|
||||
nfcToBENfc(characterId, characterInfo, userCharacter)
|
||||
else
|
||||
nfcToVBNfc(characterId, characterInfo, userCharacter)
|
||||
|
||||
@ -3,6 +3,10 @@ package com.github.nacabaro.vbhelper.source
|
||||
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.transfer.CharacterTransferPolicyResolver
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTarget
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTransport
|
||||
import com.github.nacabaro.vbhelper.transfer.toTransferDeviceType
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
@ -30,6 +34,8 @@ internal fun resolveTrainingSeconds(
|
||||
class VitalWearCharacterExporter(
|
||||
private val database: AppDatabase
|
||||
) {
|
||||
private val transferPolicyResolver = CharacterTransferPolicyResolver(database.characterTransferPolicyDao())
|
||||
|
||||
/**
|
||||
* Builds a Character proto from the stored character data.
|
||||
* Used by the HCE ISO-DEP transfer path.
|
||||
@ -44,6 +50,13 @@ class VitalWearCharacterExporter(
|
||||
val cardProgress = database.cardProgressDao().getCardProgressSync(card.id) ?: 0
|
||||
val beData = database.userCharacterDao().getBeDataOrNull(characterId)
|
||||
val vbData = database.userCharacterDao().getVbDataOrNull(characterId)
|
||||
val exportFormat = transferPolicyResolver.resolveExportFormat(
|
||||
characterId = characterId,
|
||||
characterType = userCharacter.characterType,
|
||||
transport = TransferTransport.HCE,
|
||||
target = TransferTarget.VITAL_WEAR,
|
||||
isBemCard = card.isBEm,
|
||||
)
|
||||
val normalizedTransformationCountdownMinutes = normalizeTransformationCountdownMinutes(
|
||||
transformationCountdownMinutes = userCharacter.transformationCountdown,
|
||||
hasPossibleTransformations = hasPossibleTransformations(userCharacter.charId),
|
||||
@ -68,7 +81,7 @@ class VitalWearCharacterExporter(
|
||||
.setTotalWins(characterWithSprites.totalBattlesWon)
|
||||
.setCurrentPhaseWins(characterWithSprites.currentPhaseBattlesWon)
|
||||
.setMood(characterWithSprites.mood)
|
||||
.setDeviceType(userCharacter.characterType.toTransferDeviceType())
|
||||
.setDeviceType(exportFormat.toTransferDeviceType())
|
||||
.setAgeInDays(userCharacter.ageInDays.coerceAtLeast(0))
|
||||
.setActivityLevel(userCharacter.activityLevel.coerceAtLeast(0))
|
||||
.setHeartRateCurrent(userCharacter.heartRateCurrent.coerceAtLeast(0))
|
||||
|
||||
@ -9,6 +9,8 @@ import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VitalWearCharacterSettings
|
||||
import com.github.nacabaro.vbhelper.transfer.CharacterTransferPolicyResolver
|
||||
import com.github.nacabaro.vbhelper.transfer.ExportFormat
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@ -18,6 +20,8 @@ class VitalWearCharacterImporter(
|
||||
private val database: AppDatabase,
|
||||
private val transferSeenDao: SharedTransferSeenDao,
|
||||
) {
|
||||
private val transferPolicyResolver = CharacterTransferPolicyResolver(database.characterTransferPolicyDao())
|
||||
|
||||
data class ImportResult(
|
||||
val success: Boolean,
|
||||
val message: String
|
||||
@ -58,6 +62,11 @@ class VitalWearCharacterImporter(
|
||||
fallbackIsBeCharacter = fallbackIsBeCharacter,
|
||||
)
|
||||
val isBeCharacter = deviceType == DeviceType.BEDevice
|
||||
val observedImportFormat = resolveObservedHceImportFormat(
|
||||
transferDeviceType = character.characterStats.deviceType,
|
||||
importedCard = importedCard,
|
||||
hasBePayloadStats = hasBePayloadStats,
|
||||
)
|
||||
|
||||
val userCharacterId = database.userCharacterDao().insertCharacterData(
|
||||
UserCharacter(
|
||||
@ -138,6 +147,17 @@ class VitalWearCharacterImporter(
|
||||
)
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
database.characterTransferPolicyDao().upsert(
|
||||
transferPolicyResolver.policyForHceImport(
|
||||
characterId = userCharacterId,
|
||||
importedCardIsBem = importedCard.isBEm,
|
||||
resolvedDeviceType = deviceType,
|
||||
observedFormat = observedImportFormat,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
markSeen(importedCard.name, slotId, importedCard.id, now)
|
||||
|
||||
@ -270,6 +290,23 @@ class VitalWearCharacterImporter(
|
||||
return rarities.getOrElse(rawValue) { resolveDefaultAbilityRarity() }
|
||||
}
|
||||
|
||||
private fun resolveObservedHceImportFormat(
|
||||
transferDeviceType: Character.CharacterStats.TransferDeviceType,
|
||||
importedCard: Card,
|
||||
hasBePayloadStats: Boolean,
|
||||
): ExportFormat {
|
||||
return when (transferDeviceType) {
|
||||
Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_BE -> ExportFormat.BE
|
||||
Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_VB -> ExportFormat.VB
|
||||
Character.CharacterStats.TransferDeviceType.UNRECOGNIZED,
|
||||
Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_UNSPECIFIED -> if (importedCard.isBEm || hasBePayloadStats) {
|
||||
ExportFormat.BE
|
||||
} else {
|
||||
ExportFormat.VB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markSeen(cardName: String, slotId: Int, cardId: Long, timestamp: Long) {
|
||||
database.dexDao().insertCharacter(slotId, cardId, timestamp)
|
||||
transferSeenDao.markSeen(cardName, slotId, timestamp)
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
package com.github.nacabaro.vbhelper.transfer
|
||||
|
||||
import com.github.nacabaro.vbhelper.daos.CharacterTransferPolicyDao
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.CharacterTransferPolicy
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
|
||||
class CharacterTransferPolicyResolver(
|
||||
private val policyDao: CharacterTransferPolicyDao,
|
||||
) {
|
||||
fun resolveExportFormat(
|
||||
characterId: Long,
|
||||
characterType: DeviceType,
|
||||
transport: TransferTransport,
|
||||
target: TransferTarget,
|
||||
isBemCard: Boolean,
|
||||
): ExportFormat {
|
||||
val policy = policyDao.getByCharacterId(characterId)
|
||||
return when (transport) {
|
||||
TransferTransport.HCE -> policy?.preferredHceExportFormat
|
||||
?: defaultHceExportFormat(characterType, isBemCard, target)
|
||||
TransferTransport.NFCA -> policy?.preferredNfcaExportFormat
|
||||
?: defaultNfcaExportFormat(characterType, isBemCard, target)
|
||||
}
|
||||
}
|
||||
|
||||
fun policyForNfcaImport(
|
||||
characterId: Long,
|
||||
observedFormat: ExportFormat,
|
||||
): CharacterTransferPolicy {
|
||||
val nativeDeviceType = observedFormat.toNativeDeviceType()
|
||||
return CharacterTransferPolicy(
|
||||
characterId = characterId,
|
||||
nativeDeviceType = nativeDeviceType,
|
||||
preferredHceExportFormat = defaultHceExportFormat(nativeDeviceType, nativeDeviceType == DeviceType.BEDevice, TransferTarget.VITAL_WEAR),
|
||||
preferredNfcaExportFormat = observedFormat,
|
||||
lastObservedImportFormat = observedFormat,
|
||||
lastTransferTransport = TransferTransport.NFCA,
|
||||
lastTransferTarget = TransferTarget.REAL_BRACELET,
|
||||
preserveVbRoundTrip = observedFormat == ExportFormat.VB,
|
||||
preserveBeRoundTrip = observedFormat == ExportFormat.BE,
|
||||
)
|
||||
}
|
||||
|
||||
fun policyForHceImport(
|
||||
characterId: Long,
|
||||
importedCardIsBem: Boolean,
|
||||
resolvedDeviceType: DeviceType,
|
||||
observedFormat: ExportFormat,
|
||||
): CharacterTransferPolicy {
|
||||
val nativeDeviceType = when {
|
||||
importedCardIsBem -> DeviceType.BEDevice
|
||||
resolvedDeviceType == DeviceType.BEDevice && observedFormat == ExportFormat.BE -> DeviceType.VBDevice
|
||||
resolvedDeviceType == DeviceType.VitalWear -> DeviceType.VBDevice
|
||||
else -> resolvedDeviceType
|
||||
}
|
||||
val preferredNfcaExportFormat = if (importedCardIsBem || nativeDeviceType == DeviceType.BEDevice) {
|
||||
ExportFormat.BE
|
||||
} else {
|
||||
ExportFormat.VB
|
||||
}
|
||||
|
||||
return CharacterTransferPolicy(
|
||||
characterId = characterId,
|
||||
nativeDeviceType = nativeDeviceType,
|
||||
preferredHceExportFormat = defaultHceExportFormat(nativeDeviceType, importedCardIsBem, TransferTarget.VITAL_WEAR),
|
||||
preferredNfcaExportFormat = preferredNfcaExportFormat,
|
||||
lastObservedImportFormat = observedFormat,
|
||||
lastTransferTransport = TransferTransport.HCE,
|
||||
lastTransferTarget = TransferTarget.VITAL_WEAR,
|
||||
preserveVbRoundTrip = preferredNfcaExportFormat == ExportFormat.VB,
|
||||
preserveBeRoundTrip = importedCardIsBem || observedFormat == ExportFormat.BE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun defaultHceExportFormat(
|
||||
characterType: DeviceType,
|
||||
isBemCard: Boolean,
|
||||
target: TransferTarget,
|
||||
): ExportFormat {
|
||||
if (target != TransferTarget.VITAL_WEAR) {
|
||||
return defaultNfcaExportFormat(characterType, isBemCard, target)
|
||||
}
|
||||
|
||||
// VitalWear is the HCE endpoint. Default to BE projection so HCE transfers can carry the
|
||||
// richer payload, while NFCA policy still controls what is sent back to real bracelets.
|
||||
return ExportFormat.BE
|
||||
}
|
||||
|
||||
private fun defaultNfcaExportFormat(
|
||||
characterType: DeviceType,
|
||||
isBemCard: Boolean,
|
||||
target: TransferTarget,
|
||||
): ExportFormat {
|
||||
require(target == TransferTarget.REAL_BRACELET) {
|
||||
"NFCA exports are only supported for real bracelets."
|
||||
}
|
||||
return if (isBemCard || characterType == DeviceType.BEDevice) {
|
||||
ExportFormat.BE
|
||||
} else {
|
||||
ExportFormat.VB
|
||||
}
|
||||
}
|
||||
|
||||
private fun ExportFormat.toNativeDeviceType(): DeviceType {
|
||||
return when (this) {
|
||||
ExportFormat.BE -> DeviceType.BEDevice
|
||||
ExportFormat.VB -> DeviceType.VBDevice
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package com.github.nacabaro.vbhelper.transfer
|
||||
|
||||
import com.github.cfogrady.vitalwear.protos.Character
|
||||
|
||||
/**
|
||||
* Real bracelets talk over NFC-A while VitalWear uses HCE / ISO-DEP.
|
||||
* Keep transport, target, and payload format separate so one canonical character can be
|
||||
* projected differently depending on where it is being transferred.
|
||||
*/
|
||||
enum class TransferTransport {
|
||||
NFCA,
|
||||
HCE,
|
||||
}
|
||||
|
||||
enum class TransferTarget {
|
||||
REAL_BRACELET,
|
||||
VITAL_WEAR,
|
||||
}
|
||||
|
||||
enum class ExportFormat {
|
||||
VB,
|
||||
BE,
|
||||
}
|
||||
|
||||
fun ExportFormat.toTransferDeviceType(): Character.CharacterStats.TransferDeviceType {
|
||||
return when (this) {
|
||||
ExportFormat.VB -> Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_VB
|
||||
ExportFormat.BE -> Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_BE
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user