From 6c257b2080227d960dceb729681dd2a9528fd150 Mon Sep 17 00:00:00 2001
From: Taveon Nelson <74945303+457R0@users.noreply.github.com>
Date: Tue, 26 May 2026 03:36:45 -0500
Subject: [PATCH] HCE Savepoint
This is HCE before companion merge
---
app/build.gradle.kts | 18 +-
app/src/main/AndroidManifest.xml | 40 +-
.../github/nacabaro/vbhelper/MainActivity.kt | 81 +-
.../vbhelper/battle/AnimatedSpriteImage.kt | 17 +-
.../vbhelper/battle/ArenaBattleSystem.kt | 68 +-
.../vbhelper/battle/AuthInterceptor.kt | 1 -
.../vbhelper/battle/AuthenticateResponse.kt | 27 +-
.../vbhelper/battle/DigimonAnimationState.kt | 44 +-
.../vbhelper/battle/HitEffectComposables.kt | 4 +-
.../vbhelper/battle/HitEffectSpriteManager.kt | 118 ++-
.../battle/IndividualSpriteManager.kt | 70 +-
.../vbhelper/battle/RetrofitHelper.kt | 147 +---
.../vbhelper/battle/SpriteFileManager.kt | 301 ++++++-
.../nacabaro/vbhelper/battle/SpriteImage.kt | 6 +-
.../nacabaro/vbhelper/daos/AdventureDao.kt | 2 -
.../github/nacabaro/vbhelper/daos/CardDao.kt | 3 -
.../nacabaro/vbhelper/daos/CardFusionsDao.kt | 3 +-
.../nacabaro/vbhelper/daos/CharacterDao.kt | 6 -
.../nacabaro/vbhelper/daos/SpriteDao.kt | 3 -
.../vbhelper/daos/UserCharacterDao.kt | 17 -
.../nacabaro/vbhelper/database/AppDatabase.kt | 98 +--
.../nacabaro/vbhelper/di/AppContainer.kt | 4 -
.../vbhelper/di/DefaultAppContainer.kt | 16 +-
.../github/nacabaro/vbhelper/di/VBHelper.kt | 25 -
.../vbhelper/domain/card/Background.kt | 2 -
.../vbhelper/domain/card/CardAdventure.kt | 5 -
.../vbhelper/domain/card/CardCharacter.kt | 10 +-
.../vbhelper/domain/card/CardFusions.kt | 5 -
.../domain/card/PossibleTransformations.kt | 5 -
.../domain/device_data/SpecialMissions.kt | 2 -
.../device_data/TransformationHistory.kt | 5 -
.../domain/device_data/UserCharacter.kt | 2 -
.../domain/device_data/VitalsHistory.kt | 2 -
.../vbhelper/navigation/AppNavigation.kt | 17 -
.../navigation/BottomNavigationBar.kt | 18 +-
.../vbhelper/navigation/NavigationItems.kt | 4 +-
.../vbhelper/screens/BattlesScreen.kt | 504 +++++-------
.../screens/homeScreens/HomeScreen.kt | 63 +-
.../screens/itemsScreen/ItemElement.kt | 56 --
.../screens/itemsScreen/ItemsScreen.kt | 4 +-
.../itemsScreen/ItemsScreenControllerImpl.kt | 21 +-
.../scanScreen/ChooseConnectionScreen.kt | 2 +-
.../vbhelper/screens/scanScreen/ScanScreen.kt | 10 +-
.../scanScreen/ScanScreenController.kt | 17 +-
.../scanScreen/ScanScreenControllerImpl.kt | 743 +++---------------
.../scanScreen/converters/FromNfcConverter.kt | 61 +-
.../scanScreen/converters/ToNfcConverter.kt | 91 +--
.../scanScreen/screens/ActionScreen.kt | 77 +-
.../scanScreen/screens/ReadingScreen.kt | 102 +--
.../scanScreen/screens/WritingScreen.kt | 79 +-
.../screens/settingsScreen/SettingsScreen.kt | 72 --
.../SettingsScreenController.kt | 4 -
.../SettingsScreenControllerImpl.kt | 40 +-
.../controllers/CardImportController.kt | 53 +-
.../DatabaseManagementController.kt | 141 ----
.../vbhelper/source/AuthRepository.kt | 6 -
.../source/VitalWearCharacterExporter.kt | 265 +++----
.../source/VitalWearCharacterImporter.kt | 564 ++-----------
.../nacabaro/vbhelper/utils/BitmapData.kt | 4 +-
.../nacabhelper/screens/BattlesScreen.kt | 1 +
.../cfogrady/vitalwear/protos/Character.proto | 68 --
app/src/main/res/values-pt-rBR/strings.xml | 10 +-
app/src/main/res/values/strings.xml | 43 +-
app/src/main/res/xml/provider_paths.xml | 3 -
gradle/libs.versions.toml | 15 +-
settings.gradle.kts | 8 -
66 files changed, 1328 insertions(+), 2995 deletions(-)
create mode 100644 app/src/main/java/com/github/nacabhelper/screens/BattlesScreen.kt
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a2f1042..99c898f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -73,12 +73,8 @@ protobuf {
dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.vb.nfc.reader)
- // Temporarily commented out due to Lombok compilation issues in VB-DIM-Reader
- // implementation(libs.dim.reader)
- implementation(files("../../VB-DIM-Reader-2.0.0/build/libs/VB-DIM-Reader.jar"))
- //VB-DIM-Reader runtime dependency required by the local JAR
- implementation("at.favre.lib:bytes:1.5.0")
-
+ implementation(libs.dim.reader)
+ implementation(libs.androidx.core.ktx)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.navigation.compose)
@@ -94,13 +90,6 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.material.icons.extended)
- implementation(libs.play.services.wearable)
- implementation(libs.kotlinx.coroutines.play.services)
- implementation(libs.timber)
- implementation(libs.tiny.log)
- implementation(libs.tiny.log.impl)
- implementation(libs.slf4j.api)
- runtimeOnly(libs.slf4j.timber)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
@@ -129,7 +118,4 @@ dependencies {
// HTTP request logging
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
-
- // In-app browser for OAuth login
- implementation("androidx.browser:browser:1.8.0")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 538abe9..39fa631 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,16 +2,8 @@
-
-
-
-
@@ -39,25 +31,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
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 c883fbc..9d178a1 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt
@@ -1,19 +1,19 @@
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.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
+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
-import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.screens.homeScreens.HomeScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
@@ -22,12 +22,15 @@ 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() {
private val onActivityLifecycleListeners = HashMap()
- private var initialRoute: String? by mutableStateOf(null)
+ private var initialRoute: String? = null
private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
if( onActivityLifecycleListeners[key] != null) {
@@ -79,6 +82,7 @@ class MainActivity : ComponentActivity() {
}
Log.i("MainActivity", "Activity onCreated")
+ handleImportIntent(intent)
}
override fun onPause() {
@@ -101,25 +105,62 @@ class MainActivity : ComponentActivity() {
super.onNewIntent(intent)
setIntent(intent)
initialRoute = getInitialRouteFromIntent(intent)
+ // Optionally, you may want to trigger navigation here if needed
+ 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
+ }
}
private fun getInitialRouteFromIntent(intent: Intent?): String? {
if (intent == null) return null
val data = intent.data
if (intent.action == Intent.ACTION_VIEW && data != null) {
- val isAppAuthCallback = data.scheme == "vbhelper" && data.host == "auth"
- val isLocalhostAuthCallback =
- (data.scheme == "http" || data.scheme == "https") &&
- (data.host == "localhost" || data.host == "127.0.0.1") &&
- data.path?.startsWith("/authenticate") == true
- val token = data.getQueryParameter("c") ?: data.getQueryParameter("token")
- val hasAuthToken = !token.isNullOrEmpty()
-
- if (isAppAuthCallback || isLocalhostAuthCallback || hasAuthToken) {
- // BattlesScreen consumes the callback intent and exchanges the single-use token
- // for a durable session token. Writing partial auth state here can clear an
- // already-valid session token and cause the browser login loop to restart.
- return NavigationItems.Battles.route
+ if (data.scheme == "vbhelper" && data.host == "auth") {
+ return "Battle"
}
}
return null
@@ -151,4 +192,8 @@ class MainActivity : ComponentActivity() {
initialRoute = initialRoute
)
}
+
+ companion object {
+ private const val VITALWEAR_CHARACTER_MIME = "application/x-vitalwear-character"
+ }
}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt
index 7d6f6cc..f75b935 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt
@@ -5,7 +5,9 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.launch
+import kotlinx.coroutines.delay
@Composable
fun AnimatedSpriteImage(
@@ -13,10 +15,12 @@ fun AnimatedSpriteImage(
animationType: DigimonAnimationType = DigimonAnimationType.IDLE,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
+ reloadMappings: Boolean = false,
animationOffset: Long = 0L // New parameter for offsetting animation timing
) {
- val spriteManager = remember { IndividualSpriteManager() }
-
+ val context = LocalContext.current
+ val spriteManager = remember { IndividualSpriteManager(context) }
+
// Calculate frame offset based on animation offset
// 750ms is the idle animation duration, so we calculate how many frames to offset
val frameOffset = if (animationOffset > 0L) {
@@ -26,11 +30,18 @@ fun AnimatedSpriteImage(
0
}
- val animationStateMachine = remember { DigimonAnimationStateMachine(characterId, frameOffset, animationOffset) }
+ val animationStateMachine = remember { DigimonAnimationStateMachine(characterId, context, frameOffset, animationOffset) }
val coroutineScope = rememberCoroutineScope()
var bitmap by remember { mutableStateOf(null) }
+ // Reload mappings when reloadMappings parameter changes
+ LaunchedEffect(reloadMappings) {
+ if (reloadMappings) {
+ animationStateMachine.reloadMappings()
+ }
+ }
+
// Start the animation when the component is first created
LaunchedEffect(characterId) {
coroutineScope.launch {
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt
index 5a6beaf..cac56be 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt
@@ -1,11 +1,18 @@
package com.github.nacabaro.vbhelper.battle
+import android.util.Log
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.State
class ArenaBattleSystem {
- // Attack phases: 0=Idle, 1=Player attack on player screen, 2=Player attack on opponent screen,
+ companion object {
+ private const val TAG = "ArenaBattleSystem"
+ }
+
+ // Attack phases: 0=Idle, 1=Player attack on player screen, 2=Player attack on opponent screen,
// 3=Opponent attack on opponent screen, 4=Opponent attack on player screen
private var _attackPhase by mutableStateOf(0)
val attackPhase: Int get() = _attackPhase
@@ -37,6 +44,19 @@ class ArenaBattleSystem {
private var _critBarProgress by mutableStateOf(0)
val critBarProgress: Int get() = _critBarProgress
+ // Dodge animation states
+ private var _isDodging by mutableStateOf(false)
+ val isDodging: Boolean get() = _isDodging
+
+ private var _dodgeProgress by mutableStateOf(0f)
+ val dodgeProgress: Float get() = _dodgeProgress
+
+ private var _dodgeDirection by mutableStateOf(1f) // 1f = up, -1f = down
+ val dodgeDirection: Float get() = _dodgeDirection
+
+ private var _isHit by mutableStateOf(false)
+ val isHit: Boolean get() = _isHit
+
private var _hitProgress by mutableStateOf(0f)
val hitProgress: Float get() = _hitProgress
@@ -160,6 +180,10 @@ class ArenaBattleSystem {
_isPlayerAttacking = false
_attackIsHit = false
_currentView = 0
+ _isDodging = false
+ _dodgeProgress = 0f
+ _dodgeDirection = 1f
+ _isHit = false
_hitProgress = 0f
_isPlayerDodging = false
_isOpponentDodging = false
@@ -191,10 +215,41 @@ class ArenaBattleSystem {
//Log.d(TAG, "Updated crit bar progress: $progress")
}
+ // Dodge animation methods
+ fun startDodge() {
+ _isDodging = true
+ _dodgeProgress = 0f
+ _dodgeDirection = 1f // Start moving up
+ }
+
+ fun setDodgeProgress(progress: Float) {
+ _dodgeProgress = progress
+ }
+
+ fun setDodgeDirection(direction: Float) {
+ _dodgeDirection = direction
+ }
+
+ fun endDodge() {
+ _isDodging = false
+ _dodgeProgress = 0f
+ }
+
+ // Hit animation methods
+ fun startHit() {
+ _isHit = true
+ _hitProgress = 0f
+ }
+
fun setHitProgress(progress: Float) {
_hitProgress = progress
}
+ fun endHit() {
+ _isHit = false
+ _hitProgress = 0f
+ }
+
// Player-specific dodge methods
fun startPlayerDodge() {
_isPlayerDodging = true
@@ -290,6 +345,17 @@ class ArenaBattleSystem {
_isOpponentShakeDelayed = false
}
+ // Combined method to handle attack result
+ fun handleAttackResult(isHit: Boolean) {
+ _attackIsHit = isHit
+ if (isHit) {
+ // Player attack hit - opponent gets hit
+ startOpponentHit()
+ } else {
+ // Player attack missed - opponent dodges
+ startOpponentDodge()
+ }
+ }
// Method to handle opponent attack result
fun handleOpponentAttackResult(isHit: Boolean) {
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthInterceptor.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthInterceptor.kt
index 6488586..df03762 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthInterceptor.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthInterceptor.kt
@@ -20,7 +20,6 @@ class AuthInterceptor(private val token: String) : Interceptor {
// Use X-Session-Token header (preferred) or Authorization: Bearer
val authenticatedRequest = originalRequest.newBuilder()
.header("X-Session-Token", token)
- .header("Authorization", "Bearer $token")
.build()
// Debug: Log which header is being used (first few chars of token for security)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt
index 5b1f1b0..cb1f4fd 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt
@@ -1,46 +1,21 @@
package com.github.nacabaro.vbhelper.battle
-import com.google.gson.annotations.SerializedName
-
data class AdditionalInfo(
- @SerializedName(value = "avatar", alternate = ["avatar_url"])
val avatar: String? = null,
- @SerializedName(value = "id", alternate = ["user_id"])
val id: Long? = null,
- @SerializedName(value = "name", alternate = ["username", "display_name"])
val name: String? = null,
val status: String? = null
)
data class UserInfo(
- @SerializedName(value = "userId", alternate = ["user_id", "id"])
val userId: String? = null,
- @SerializedName(value = "additionalInfo", alternate = ["additional_info"])
val additionalInfo: AdditionalInfo? = null
)
data class AuthenticateResponse(
- @SerializedName(value = "success", alternate = ["ok"])
val success: Boolean,
val message: String? = null,
- @SerializedName(value = "userInfo", alternate = ["user_info", "user"])
val userInfo: UserInfo? = null,
- @SerializedName(value = "sessionToken", alternate = ["session_token", "token", "session"])
- val sessionToken: String? = null,
- @SerializedName(value = "userId", alternate = ["user_id"])
- val topLevelUserId: String? = null,
- @SerializedName(value = "id", alternate = ["uid"])
- val topLevelId: Long? = null,
+ val sessionToken: String? = null
)
-fun AuthenticateResponse.extractSessionToken(): String? {
- return sessionToken?.takeIf { it.isNotBlank() }
-}
-
-fun AuthenticateResponse.extractUserId(): Long? {
- return userInfo?.userId?.toLongOrNull()
- ?: topLevelUserId?.toLongOrNull()
- ?: userInfo?.additionalInfo?.id
- ?: topLevelId
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt
index 40c4b00..c2ee41e 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt
@@ -4,13 +4,22 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.delay
+import android.content.Context
+import java.io.File
enum class DigimonAnimationType {
IDLE,
IDLE2,
WALK,
+ WALK2,
+ RUN,
+ RUN2,
+ WORKOUT,
+ WORKOUT2,
+ HAPPY,
SLEEP,
- ATTACK
+ ATTACK,
+ FLEE
}
data class AnimationState(
@@ -22,6 +31,7 @@ data class AnimationState(
class DigimonAnimationStateMachine(
private val characterId: String,
+ private val context: Context,
private val initialFrameOffset: Int = 0, // New parameter for offsetting the starting frame
private val timingOffset: Long = 0L // New parameter for offsetting the timing
) {
@@ -34,13 +44,21 @@ class DigimonAnimationStateMachine(
var isPlaying by mutableStateOf(false)
private set
- // Direct mapping of frame numbers used by current battle animations.
+ // Direct mapping of frame numbers (1-12) to animation types
+ // This is based on the standard Digimon sprite frame order
private val frameToAnimationType = mapOf(
1 to DigimonAnimationType.IDLE,
2 to DigimonAnimationType.IDLE2,
3 to DigimonAnimationType.WALK,
+ 4 to DigimonAnimationType.WALK2,
+ 5 to DigimonAnimationType.RUN,
+ 6 to DigimonAnimationType.RUN2,
+ 7 to DigimonAnimationType.WORKOUT,
+ 8 to DigimonAnimationType.WORKOUT2,
+ 9 to DigimonAnimationType.HAPPY,
10 to DigimonAnimationType.SLEEP,
- 11 to DigimonAnimationType.ATTACK
+ 11 to DigimonAnimationType.ATTACK,
+ 12 to DigimonAnimationType.FLEE
)
// Reverse mapping for getting frame numbers for each animation type
@@ -51,8 +69,15 @@ class DigimonAnimationStateMachine(
DigimonAnimationType.IDLE to 750L,
DigimonAnimationType.IDLE2 to 750L,
DigimonAnimationType.WALK to 200L,
+ DigimonAnimationType.WALK2 to 200L,
+ DigimonAnimationType.RUN to 150L,
+ DigimonAnimationType.RUN2 to 150L,
+ DigimonAnimationType.WORKOUT to 300L,
+ DigimonAnimationType.WORKOUT2 to 300L,
+ DigimonAnimationType.HAPPY to 400L,
DigimonAnimationType.SLEEP to 1500L,
- DigimonAnimationType.ATTACK to 650L
+ DigimonAnimationType.ATTACK to 650L,
+ DigimonAnimationType.FLEE to 150L
)
/*
@@ -130,4 +155,13 @@ class DigimonAnimationStateMachine(
return currentFrameNumber
}
-}
\ No newline at end of file
+ fun getCurrentCharacterId(): String {
+ return characterId
+ }
+
+ // Method to reload mappings (useful for testing)
+ fun reloadMappings() {
+ println("Reloading mappings for character: $characterId")
+ // No need to reload since we use direct frame mapping
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt
index 800a0f3..e4a3383 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt
@@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@@ -24,9 +25,10 @@ fun HitEffectOverlay(
) {
if (!isVisible) return
+ val context = LocalContext.current
val configuration = LocalConfiguration.current
val isLandscapeMode = configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
- val hitEffectManager = remember { HitEffectSpriteManager() }
+ val hitEffectManager = remember { HitEffectSpriteManager(context) }
val coroutineScope = rememberCoroutineScope()
var currentFrame by remember { mutableStateOf(0) }
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt
index e6fae95..fa116be 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt
@@ -1,10 +1,13 @@
package com.github.nacabaro.vbhelper.battle
+import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
+import android.graphics.Rect
+import android.os.Environment
import java.io.File
-class HitEffectSpriteManager {
+class HitEffectSpriteManager(private val context: Context) {
private val spriteCache = mutableMapOf()
// Get the external storage directory for hit effect sprites
@@ -53,4 +56,117 @@ class HitEffectSpriteManager {
}
}
+ /**
+ * Load a damage effect sprite from spritesheet
+ * @param spritesheetName The spritesheet name (e.g., "dmg_ef1", "dmg_ef2")
+ * @param frameIndex The frame index (0-3 for dmg_ef1 and dmg_ef2, 0 for dmg_ef3)
+ * @return Bitmap of the damage effect frame, or null if not found
+ */
+ fun loadDamageEffectSprite(spritesheetName: String, frameIndex: Int = 0): Bitmap? {
+ val cacheKey = "dmg_${spritesheetName}_frame_${frameIndex}"
+
+ // Check cache first
+ if (spriteCache.containsKey(cacheKey)) {
+ return spriteCache[cacheKey]
+ }
+
+ try {
+ val spritesheetFile = File(getHitSpritesDir(), "$spritesheetName.png")
+
+ if (!spritesheetFile.exists()) {
+ println("Damage effect spritesheet not found: ${spritesheetFile.absolutePath}")
+ return null
+ }
+
+ val spritesheet = BitmapFactory.decodeFile(spritesheetFile.absolutePath)
+ if (spritesheet == null) {
+ println("Failed to decode damage effect spritesheet: ${spritesheetFile.absolutePath}")
+ return null
+ }
+
+ // Extract frame from spritesheet
+ val frameBitmap = when (spritesheetName) {
+ "dmg_ef1", "dmg_ef2" -> {
+ // These are 2x2 spritesheets (4 frames)
+ val frameWidth = spritesheet.width / 2
+ val frameHeight = spritesheet.height / 2
+ val row = frameIndex / 2
+ val col = frameIndex % 2
+ val x = col * frameWidth
+ val y = row * frameHeight
+ Bitmap.createBitmap(spritesheet, x, y, frameWidth, frameHeight)
+ }
+ "dmg_ef3" -> {
+ // This is a single sprite
+ spritesheet
+ }
+ else -> {
+ println("Unknown spritesheet name: $spritesheetName")
+ return null
+ }
+ }
+
+ println("Successfully loaded damage effect frame: $spritesheetName frame $frameIndex (${frameBitmap.width}x${frameBitmap.height})")
+
+ // Cache the result
+ spriteCache[cacheKey] = frameBitmap
+
+ return frameBitmap
+
+ } catch (e: Exception) {
+ println("Error loading damage effect sprite: ${e.message}")
+ e.printStackTrace()
+ return null
+ }
+ }
+
+ /**
+ * Get all available hit sprites
+ * @return List of hit sprite names (without .png extension)
+ */
+ fun getAvailableHitSprites(): List {
+ val hitSpritesDir = getHitSpritesDir()
+
+ if (!hitSpritesDir.exists()) {
+ return emptyList()
+ }
+
+ return hitSpritesDir.listFiles { file ->
+ file.name.startsWith("hit_") && file.name.endsWith(".png")
+ }?.map { file ->
+ file.name.substringBefore(".png")
+ }?.sorted() ?: emptyList()
+ }
+
+ /**
+ * Get available damage effect spritesheet names
+ * @return List of available damage effect spritesheet names
+ */
+ fun getAvailableDamageEffectSpritesheets(): List {
+ try {
+ if (!getHitSpritesDir().exists()) {
+ return emptyList()
+ }
+
+ val dmgFiles = getHitSpritesDir().listFiles { file ->
+ file.name.startsWith("dmg_ef") && file.name.endsWith(".png")
+ } ?: emptyArray()
+
+ return dmgFiles.map { file ->
+ file.name.substringBefore(".png")
+ }.sorted()
+
+ } catch (e: Exception) {
+ println("Error getting available damage effect spritesheets: ${e.message}")
+ e.printStackTrace()
+ return emptyList()
+ }
+ }
+
+ /**
+ * Clear the sprite cache
+ */
+ fun clearCache() {
+ spriteCache.clear()
+ }
}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt
index b6bb2bc..84c68a7 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt
@@ -1,10 +1,12 @@
package com.github.nacabaro.vbhelper.battle
+import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
+import android.os.Environment
import java.io.File
-class IndividualSpriteManager {
+class IndividualSpriteManager(private val context: Context) {
private val spriteCache = mutableMapOf()
// Get the external storage directory for sprite files
@@ -63,4 +65,68 @@ class IndividualSpriteManager {
}
}
-}
\ No newline at end of file
+ /**
+ * Get all available sprite frames for a character
+ * @param characterId The character ID
+ * @return List of frame numbers (1-12) that exist for this character
+ */
+ fun getAvailableFrames(characterId: String): List {
+ val spriteBaseDir = getSpriteBaseDir()
+ val characterDir = File(spriteBaseDir, characterId)
+
+ if (!characterDir.exists()) {
+ return emptyList()
+ }
+
+ val spriteFiles = characterDir.listFiles { file ->
+ file.name.startsWith("${characterId}_") && file.name.endsWith(".png")
+ } ?: emptyArray()
+
+ return spriteFiles.mapNotNull { file ->
+ val fileName = file.name
+ val frameMatch = Regex("${characterId}_(\\d{2})\\.png").find(fileName)
+ frameMatch?.groupValues?.get(1)?.toIntOrNull()
+ }.sorted()
+ }
+
+ /**
+ * Get all available character IDs
+ * @return List of character IDs that have sprite directories
+ */
+ fun getAvailableCharacters(): List {
+ val spriteBaseDir = getSpriteBaseDir()
+
+ if (!spriteBaseDir.exists()) {
+ return emptyList()
+ }
+
+ return spriteBaseDir.listFiles { file ->
+ file.isDirectory && file.listFiles()?.any { it.name.endsWith(".png") } == true
+ }?.map { it.name }?.sorted() ?: emptyList()
+ }
+
+ /**
+ * Clear the sprite cache
+ */
+ fun clearCache() {
+ spriteCache.clear()
+ }
+
+ /**
+ * Check if a character has sprite files
+ * @param characterId The character ID to check
+ * @return true if the character has sprite files, false otherwise
+ */
+ fun hasCharacterSprites(characterId: String): Boolean {
+ val characterDir = File(getSpriteBaseDir(), characterId)
+ if (!characterDir.exists()) {
+ return false
+ }
+
+ val spriteFiles = characterDir.listFiles { file ->
+ file.name.startsWith("${characterId}_") && file.name.endsWith(".png")
+ } ?: emptyArray()
+
+ return spriteFiles.isNotEmpty()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt
index fc0ce24..b465ddc 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt
@@ -12,14 +12,10 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeoutOrNull
import com.github.nacabaro.vbhelper.battle.BattleAuthContainer
-import java.net.SocketTimeoutException
-import java.io.InterruptedIOException
-import java.util.concurrent.TimeUnit
class RetrofitHelper {
-
+
/**
* Creates an OkHttpClient with authentication interceptor for game endpoints.
* Requires a non-null, non-empty token.
@@ -32,22 +28,9 @@ class RetrofitHelper {
return OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(token))
.addInterceptor(loggingInterceptor)
- .connectTimeout(30, TimeUnit.SECONDS)
- .readTimeout(30, TimeUnit.SECONDS)
- .writeTimeout(30, TimeUnit.SECONDS)
- .callTimeout(45, TimeUnit.SECONDS)
- .retryOnConnectionFailure(true)
.build()
}
-
- private fun classifyNetworkError(t: Throwable): String {
- return when (t) {
- is SocketTimeoutException -> "Authentication server timed out"
- is InterruptedIOException -> "Request timed out"
- else -> t.message ?: "Network error"
- }
- }
-
+
/**
* Gets the session token from AuthRepository for API calls.
* Falls back to nacatech token if session token is not available (backward compatibility).
@@ -55,8 +38,7 @@ class RetrofitHelper {
private fun getAuthToken(context: Context): String? {
return try {
val authContainer = BattleAuthContainer(context)
- runBlocking(Dispatchers.IO) {
- withTimeoutOrNull(5000L) {
+ runBlocking {
// Prefer session token, fall back to nacatech token for backward compatibility
val sessionToken = authContainer.authRepository.sessionToken.first()
if (!sessionToken.isNullOrEmpty()) {
@@ -70,10 +52,6 @@ class RetrofitHelper {
}
nacatechToken
}
- } ?: run {
- println("RetrofitHelper: Timed out while reading auth token from DataStore")
- null
- }
}
} catch (e: Exception) {
println("RetrofitHelper: Error getting auth token: ${e.message}")
@@ -98,30 +76,7 @@ class RetrofitHelper {
.addConverterFactory(GsonConverterFactory.create())
.build()
}
- /**
- * Creates a Retrofit instance without authentication for public endpoints.
- */
- private fun createPublicRetrofit(): Retrofit {
- val loggingInterceptor = HttpLoggingInterceptor().apply {
- level = HttpLoggingInterceptor.Level.BODY
- }
-
- val client = OkHttpClient.Builder()
- .addInterceptor(loggingInterceptor)
- .connectTimeout(30, TimeUnit.SECONDS)
- .readTimeout(30, TimeUnit.SECONDS)
- .writeTimeout(30, TimeUnit.SECONDS)
- .callTimeout(45, TimeUnit.SECONDS)
- .retryOnConnectionFailure(true)
- .build()
-
- return Retrofit.Builder()
- .baseUrl("http://battle.io-void.com:8080/")
- .client(client)
- .addConverterFactory(GsonConverterFactory.create())
- .build()
- }
-
+
/**
* Handles HTTP error responses (401, 403, 429).
* For 401/403, clears authentication state to trigger re-authentication.
@@ -166,15 +121,16 @@ class RetrofitHelper {
}
}
- fun getOpponents(context: Context, stage: String, onComplete: (() -> Unit)? = null, callback: (OpponentsDataModel) -> Unit) {
+ fun getOpponents(context: Context, stage: String, callback: (OpponentsDataModel) -> Unit) {
//println("RetrofitHelper: Starting API call for stage: $stage")
try {
- // Opponents endpoint requires auth on the current backend.
- // Prefer authenticated Retrofit; fall back to public client only if no token is available.
- val retrofit = createAuthenticatedRetrofit(context) ?: run {
- println("RetrofitHelper: No auth token for opponents request, falling back to public Retrofit")
- createPublicRetrofit()
+ // Create an authenticated Retrofit instance
+ val retrofit = createAuthenticatedRetrofit(context)
+ if (retrofit == null) {
+ println("RetrofitHelper: Cannot create authenticated Retrofit - no token available")
+ Toast.makeText(context, "Authentication required. Please log in.", Toast.LENGTH_SHORT).show()
+ return
}
// Create an ApiService instance from the Retrofit instance.
@@ -190,11 +146,9 @@ class RetrofitHelper {
// make an asynchronous API request.
call.enqueue(object : Callback {
override fun onFailure(call: Call, t: Throwable) {
- val classified = classifyNetworkError(t)
- println("RetrofitHelper: API call failed (${t::class.java.simpleName}): $classified")
+ println("RetrofitHelper: API call failed: ${t.message}")
t.printStackTrace()
- Toast.makeText(context, classified, Toast.LENGTH_SHORT).show()
- onComplete?.invoke()
+ Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call, response: Response) {
@@ -202,19 +156,13 @@ class RetrofitHelper {
println("RetrofitHelper: Response body: ${response.body()}")
if(response.isSuccessful){
- val opponentsList = response.body()
- if (opponentsList != null) {
- callback(opponentsList)
- } else {
- println("RetrofitHelper: Successful opponents response had empty body")
- Toast.makeText(context, "Empty response from server", Toast.LENGTH_SHORT).show()
- }
- onComplete?.invoke()
+ //println("RetrofitHelper: Response successful, calling callback")
+ val opponentsList: OpponentsDataModel = response.body() as OpponentsDataModel
+ callback(opponentsList)
} else {
val errorBody = response.errorBody()?.string()
- println("RetrofitHelper: Opponents response not successful - Code: ${response.code()}, Error: $errorBody")
- handleErrorResponse(context, response, errorBody ?: "Unable to load opponents right now.")
- onComplete?.invoke()
+ println("RetrofitHelper: Response not successful - Error: $errorBody")
+ handleErrorResponse(context, response, errorBody ?: "Unknown error")
}
}
})
@@ -222,7 +170,6 @@ class RetrofitHelper {
println("RetrofitHelper: Exception in getOpponents: ${e.message}")
e.printStackTrace()
Toast.makeText(context, "Request failed: ${e.message}", Toast.LENGTH_SHORT).show()
- onComplete?.invoke()
}
}
@@ -337,10 +284,9 @@ class RetrofitHelper {
override fun onFailure(call: Call, t: Throwable) {
// This method is called when the API request fails.
- val classified = classifyNetworkError(t)
- println("RetrofitHelper: PVP API call failed (${t::class.java.simpleName}): $classified")
+ println("RetrofitHelper: PVP API call failed: ${t.message}")
t.printStackTrace()
- Toast.makeText(context, classified, Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call, response: Response) {
@@ -348,13 +294,13 @@ class RetrofitHelper {
println("RetrofitHelper: PVP API response received - Code: ${response.code()}")
if(response.isSuccessful){
- val apiResults = response.body()
- if (apiResults != null) {
- callback(apiResults)
- } else {
- println("RetrofitHelper: Successful PVP response had empty body")
- Toast.makeText(context, "Empty response from server", Toast.LENGTH_SHORT).show()
- }
+ // If the response is successful, parse the
+ // response body to a DataModel object.
+ val apiResults: PVPDataModel = response.body() as PVPDataModel
+
+ // Call the callback function with the DataModel
+ // object as a parameter.
+ callback(apiResults)
} else {
val errorBody = response.errorBody()?.string()
println("RetrofitHelper: PVP API response not successful - Code: ${response.code()}, Error: $errorBody")
@@ -369,20 +315,12 @@ class RetrofitHelper {
}
}
- fun authenticate(
- context: Context,
- token: String,
- showUserFacingErrors: Boolean = true,
- callback: (AuthenticateResponse) -> Unit,
- ) {
+ fun authenticate(context: Context, token: String, callback: (AuthenticateResponse) -> Unit) {
//println("RetrofitHelper: Starting validate API call with token: $token")
if (token.isEmpty()) {
println("RetrofitHelper: ERROR - Token is empty!")
- if (showUserFacingErrors) {
- Toast.makeText(context, "Authentication failed: Token is empty", Toast.LENGTH_SHORT).show()
- }
- callback(AuthenticateResponse(success = false, message = "Token is empty"))
+ Toast.makeText(context, "Authentication failed: Token is empty", Toast.LENGTH_SHORT).show()
return
}
@@ -393,10 +331,6 @@ class RetrofitHelper {
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
- .connectTimeout(30, TimeUnit.SECONDS)
- .readTimeout(30, TimeUnit.SECONDS)
- .writeTimeout(30, TimeUnit.SECONDS)
- .callTimeout(45, TimeUnit.SECONDS)
.build()
val retrofit: Retrofit = Retrofit.Builder()
@@ -414,11 +348,7 @@ class RetrofitHelper {
override fun onFailure(call: Call, t: Throwable) {
println("RetrofitHelper: Validate API call failed: ${t.message}")
t.printStackTrace()
- val message = classifyNetworkError(t)
- if (showUserFacingErrors) {
- Toast.makeText(context, "Authentication failed: $message", Toast.LENGTH_SHORT).show()
- }
- callback(AuthenticateResponse(success = false, message = message))
+ Toast.makeText(context, "Authentication failed: ${t.message}", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call, response: Response) {
@@ -430,25 +360,11 @@ class RetrofitHelper {
} else {
println("RetrofitHelper: Validation failed: Invalid response body")
Toast.makeText(context, "Authentication failed: Invalid response", Toast.LENGTH_SHORT).show()
- callback(AuthenticateResponse(success = false, message = "Invalid response body"))
}
} else {
val errorBody = response.errorBody()?.string()
println("RetrofitHelper: Validate response not successful - Code: ${response.code()}, Error: $errorBody")
- val userMessage = when {
- response.code() == 522 -> "Authentication server timed out. Please try again in a moment."
- response.code() >= 500 -> "Authentication server is unavailable. Please try again later."
- else -> "Authentication failed: ${response.code()}"
- }
- if (showUserFacingErrors) {
- Toast.makeText(context, userMessage, Toast.LENGTH_SHORT).show()
- }
- callback(
- AuthenticateResponse(
- success = false,
- message = "HTTP ${response.code()}: ${errorBody ?: "Authentication server error"}"
- )
- )
+ Toast.makeText(context, "Authentication failed: ${response.code()}", Toast.LENGTH_SHORT).show()
}
}
})
@@ -456,7 +372,6 @@ class RetrofitHelper {
println("RetrofitHelper: Exception in validate: ${e.message}")
e.printStackTrace()
Toast.makeText(context, "Authentication failed: ${e.message}", Toast.LENGTH_SHORT).show()
- callback(AuthenticateResponse(success = false, message = e.message ?: "Authentication error"))
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt
index e2812c3..65238e4 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt
@@ -1,13 +1,275 @@
package com.github.nacabaro.vbhelper.battle
+import android.content.Context
import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.FileInputStream
-class SpriteFileManager {
- private fun getExternalSpriteBaseDir(): File {
+class SpriteFileManager(private val context: Context) {
+
+ // Get the external storage directory where files are already located
+ fun getExternalSpriteBaseDir(): File {
val externalDir = android.os.Environment.getExternalStorageDirectory()
return File(externalDir, "VBHelper/battle_sprites")
}
-
+
+ // Get the internal storage directory for sprite files
+ private fun getInternalSpriteBaseDir(): File {
+ return File(context.filesDir, "battle_sprites")
+ }
+
+ fun copySpriteFilesToInternalStorage() {
+ try {
+ println("Starting sprite file copy process from external storage to internal storage...")
+
+ val externalDir = getExternalSpriteBaseDir()
+ val internalDir = getInternalSpriteBaseDir()
+
+ // Check if external directory exists
+ if (!externalDir.exists()) {
+ println("External sprite directory does not exist: ${externalDir.absolutePath}")
+ return
+ }
+
+ println("External sprite directory exists: ${externalDir.absolutePath}")
+ println("Copying to internal storage: ${internalDir.absolutePath}")
+
+ // Create internal directory if it doesn't exist
+ if (!internalDir.exists()) {
+ val created = internalDir.mkdirs()
+ println("Created internal sprite directory: $created")
+ }
+
+ // Copy all subdirectories from external to internal storage
+ val externalFiles = externalDir.listFiles()
+ if (externalFiles != null) {
+ println("Found ${externalFiles.size} items in external directory")
+ externalFiles.forEach { item ->
+ val targetItem = File(internalDir, item.name)
+ if (item.isDirectory) {
+ println("Copying directory: ${item.name}")
+ copyDirectory(item, targetItem)
+ } else {
+ println("Copying file: ${item.name}")
+ copyFile(item, targetItem)
+ }
+ }
+ }
+
+ println("Sprite files copied successfully to internal storage: ${internalDir.absolutePath}")
+
+ } catch (e: Exception) {
+ println("Error copying sprite files to internal storage: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ fun copySpriteFilesToExternalStorage() {
+ try {
+ println("Starting sprite file copy process to external storage...")
+
+ // Debug: List what's in the assets directory
+ val assetManager = context.assets
+ val battleSpritesFiles = assetManager.list("battle_sprites")
+ println("battle_sprites directory in assets contains: ${battleSpritesFiles?.joinToString(", ")}")
+
+ val extractedAssetsFiles = assetManager.list("battle_sprites/extracted_assets")
+ println("battle_sprites/extracted_assets directory in assets contains: ${extractedAssetsFiles?.joinToString(", ")}")
+
+ // Check specifically for extracted_atksprites in assets (now directly under battle_sprites)
+ val atkspritesInAssets = assetManager.list("battle_sprites/extracted_atksprites")
+ println("extracted_atksprites in assets contains: ${atkspritesInAssets?.size ?: 0} files")
+ if (atkspritesInAssets != null && atkspritesInAssets.isNotEmpty()) {
+ println("First few attack files in assets: ${atkspritesInAssets.take(5).joinToString(", ")}")
+ }
+
+ // Check for extracted_battlebgs in assets (now directly under battle_sprites)
+ val battlebgsInAssets = assetManager.list("battle_sprites/extracted_battlebgs")
+ println("extracted_battlebgs in assets contains: ${battlebgsInAssets?.size ?: 0} files")
+ if (battlebgsInAssets != null && battlebgsInAssets.isNotEmpty()) {
+ println("First few battle background files in assets: ${battlebgsInAssets.take(5).joinToString(", ")}")
+ }
+
+ // Try to list all possible subdirectories in battle_sprites
+ println("Checking all possible subdirectories in battle_sprites...")
+ battleSpritesFiles?.forEach { subdir ->
+ try {
+ val subdirFiles = assetManager.list("battle_sprites/$subdir")
+ println(" $subdir contains: ${subdirFiles?.size ?: 0} files")
+ if (subdirFiles != null && subdirFiles.isNotEmpty()) {
+ println(" First few files: ${subdirFiles.take(3).joinToString(", ")}")
+ }
+ } catch (e: Exception) {
+ println(" Error listing $subdir: ${e.message}")
+ }
+ }
+
+ // Create the base directory for battle_sprites in external storage
+ val battleSpritesDir = getExternalSpriteBaseDir()
+ if (!battleSpritesDir.exists()) {
+ battleSpritesDir.mkdirs()
+ println("Created battle_sprites directory in external storage: ${battleSpritesDir.absolutePath}")
+ } else {
+ println("battle_sprites directory already exists in external storage: ${battleSpritesDir.absolutePath}")
+ }
+
+ // Copy all subdirectories from battle_sprites assets to external storage
+ println("Copying all battle_sprites subdirectories to external storage...")
+ battleSpritesFiles?.forEach { subdir ->
+ val sourcePath = "battle_sprites/$subdir"
+ val targetDir = File(battleSpritesDir, subdir)
+ println("Copying $sourcePath to ${targetDir.absolutePath}")
+ copyAssetDirectory(sourcePath, targetDir)
+ }
+
+ println("Sprite files copied successfully to external storage: ${battleSpritesDir.absolutePath}")
+
+ // Verify that attack sprites were copied
+ val atkspritesDir = File(battleSpritesDir, "extracted_atksprites")
+ if (atkspritesDir.exists()) {
+ val attackFiles = atkspritesDir.listFiles()
+ println("Attack sprites directory exists with ${attackFiles?.size ?: 0} files")
+ if (attackFiles != null && attackFiles.isNotEmpty()) {
+ println("First few attack files: ${attackFiles.take(5).map { it.name }}")
+ }
+ } else {
+ println("WARNING: extracted_atksprites directory does not exist!")
+ // List what's actually in the battle_sprites directory
+ val battleSpritesContents = battleSpritesDir.listFiles()
+ println("battle_sprites directory contains: ${battleSpritesContents?.map { it.name }?.joinToString(", ")}")
+ }
+
+ // Verify that battle backgrounds were copied
+ val battlebgsDir = File(battleSpritesDir, "extracted_battlebgs")
+ if (battlebgsDir.exists()) {
+ val bgFiles = battlebgsDir.listFiles()
+ println("Battle backgrounds directory exists with ${bgFiles?.size ?: 0} files")
+ if (bgFiles != null && bgFiles.isNotEmpty()) {
+ println("First few battle background files: ${bgFiles.take(5).map { it.name }}")
+ }
+ } else {
+ println("WARNING: extracted_battlebgs directory does not exist!")
+ }
+
+ } catch (e: Exception) {
+ println("Error copying sprite files to external storage: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ private fun copyAssetDirectory(assetPath: String, targetDir: File) {
+ try {
+ val assetManager = context.assets
+ val files = assetManager.list(assetPath) ?: return
+
+ println("Copying asset directory: $assetPath (${files.size} items)")
+ println("Files found: ${files.joinToString(", ")}")
+
+ for (file in files) {
+ val assetFilePath = if (assetPath.isEmpty()) file else "$assetPath/$file"
+ val targetFile = File(targetDir, file)
+
+ // Create subdirectories if needed
+ if (targetFile.parentFile != null && !targetFile.parentFile!!.exists()) {
+ targetFile.parentFile!!.mkdirs()
+ }
+
+ // Check if it's a directory by trying to list its contents
+ try {
+ val subFiles = assetManager.list(assetFilePath)
+ if (subFiles != null && subFiles.isNotEmpty()) {
+ // It's a directory, create it and copy contents
+ println("Copying subdirectory: $assetFilePath (${subFiles.size} files)")
+ if (!targetFile.exists()) {
+ targetFile.mkdirs()
+ }
+ copyAssetDirectory(assetFilePath, targetFile)
+ } else {
+ // It's a file, copy it
+ copyAssetFile(assetFilePath, targetFile)
+ }
+ } catch (e: Exception) {
+ // If we can't list contents, it's probably a file
+ println("Treating $assetFilePath as file (could not list contents)")
+ copyAssetFile(assetFilePath, targetFile)
+ }
+ }
+
+ // Special handling for extracted_atksprites - try to copy it directly if it wasn't found
+ if (assetPath == "battle_sprites/extracted_assets") {
+ println("Special handling: Checking for extracted_atksprites directory...")
+ try {
+ val atkspritesFiles = assetManager.list("battle_sprites/extracted_assets/extracted_atksprites")
+ if (atkspritesFiles != null && atkspritesFiles.isNotEmpty()) {
+ println("Found extracted_atksprites with ${atkspritesFiles.size} files")
+ val atkspritesDir = File(targetDir, "extracted_atksprites")
+ if (!atkspritesDir.exists()) {
+ atkspritesDir.mkdirs()
+ }
+ copyAssetDirectory("battle_sprites/extracted_assets/extracted_atksprites", atkspritesDir)
+ } else {
+ println("extracted_atksprites directory not found in assets")
+ }
+ } catch (e: Exception) {
+ println("Error checking extracted_atksprites: ${e.message}")
+ }
+ }
+
+ } catch (e: Exception) {
+ println("Error copying asset directory $assetPath: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ private fun copyDirectory(sourceDir: File, targetDir: File) {
+ if (!targetDir.exists()) {
+ targetDir.mkdirs()
+ }
+
+ val files = sourceDir.listFiles()
+ if (files != null) {
+ files.forEach { file ->
+ val targetFile = File(targetDir, file.name)
+ if (file.isDirectory) {
+ copyDirectory(file, targetFile)
+ } else {
+ copyFile(file, targetFile)
+ }
+ }
+ }
+ }
+
+ private fun copyFile(sourceFile: File, targetFile: File) {
+ try {
+ val inputStream = FileInputStream(sourceFile)
+ val outputStream = FileOutputStream(targetFile)
+
+ inputStream.copyTo(outputStream)
+ inputStream.close()
+ outputStream.close()
+
+ println("Copied: ${sourceFile.name} -> ${targetFile.absolutePath}")
+ } catch (e: IOException) {
+ println("Error copying file ${sourceFile.name}: ${e.message}")
+ }
+ }
+
+ private fun copyAssetFile(assetPath: String, targetFile: File) {
+ try {
+ val inputStream = context.assets.open(assetPath)
+ val outputStream = FileOutputStream(targetFile)
+
+ inputStream.copyTo(outputStream)
+ inputStream.close()
+ outputStream.close()
+
+ println("Copied: $assetPath -> ${targetFile.absolutePath}")
+ } catch (e: IOException) {
+ println("Error copying asset file $assetPath: ${e.message}")
+ }
+ }
+
fun checkSpriteFilesExist(): Boolean {
val battleSpritesDir = getExternalSpriteBaseDir()
val extractedAssetsDir = File(battleSpritesDir, "extracted_assets")
@@ -32,4 +294,35 @@ class SpriteFileManager {
return battleSpritesExist && assetsExist && statsExist && atkspritesExist && battlebgsExist
}
-}
\ No newline at end of file
+
+ fun clearSpriteFiles() {
+ try {
+ val battleSpritesDir = getInternalSpriteBaseDir()
+
+ if (battleSpritesDir.exists()) {
+ deleteDirectory(battleSpritesDir)
+ println("Cleared battle_sprites directory from internal storage")
+ }
+
+ } catch (e: Exception) {
+ println("Error clearing sprite files: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ private fun deleteDirectory(directory: File) {
+ if (directory.exists()) {
+ val files = directory.listFiles()
+ if (files != null) {
+ for (file in files) {
+ if (file.isDirectory) {
+ deleteDirectory(file)
+ } else {
+ file.delete()
+ }
+ }
+ }
+ directory.delete()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt
index 61cbee1..5e1c141 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt
@@ -5,6 +5,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
@Composable
fun SpriteImage(
@@ -13,8 +14,9 @@ fun SpriteImage(
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit
) {
- val spriteManager = remember { IndividualSpriteManager() }
-
+ val context = LocalContext.current
+ val spriteManager = remember { IndividualSpriteManager(context) }
+
var bitmap by remember { mutableStateOf(null) }
LaunchedEffect(characterId, frameNumber) {
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/AdventureDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/AdventureDao.kt
index 8114a8b..7167b97 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/AdventureDao.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/AdventureDao.kt
@@ -2,7 +2,6 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Query
-import androidx.room.RewriteQueriesToDropUnusedColumns
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@@ -20,7 +19,6 @@ interface AdventureDao {
""")
fun getAdventureCount(): Int
- @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT
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 3fbca2a..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
@@ -13,9 +13,6 @@ interface CardDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertNewCard(card: Card): Long
- @Query("SELECT * FROM Card")
- fun getAllCards(): List
-
@Query("SELECT * FROM Card WHERE cardId = :id")
fun getCardByCardId(id: Int): List
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt
index 4382448..86d6a9a 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt
@@ -2,7 +2,6 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Query
-import androidx.room.RewriteQueriesToDropUnusedColumns
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@@ -28,12 +27,12 @@ interface CardFusionsDao {
toCharaId: Int
)
- @RewriteQueriesToDropUnusedColumns
@Query("""
SELECT
cf.toCharaId as charaId,
cf.fromCharaId as fromCharaId,
s.spriteIdle1 as spriteIdle,
+ cc.attribute as attribute,
s.width as spriteWidth,
s.height as spriteHeight,
d.discoveredOn as discoveredOn,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt
index e8f0de4..378e92f 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt
@@ -13,15 +13,9 @@ interface CharacterDao {
@Insert
suspend fun insertCharacter(vararg characterData: CardCharacter)
- @Query("SELECT * FROM CardCharacter WHERE id = :id LIMIT 1")
- fun getCharacterById(id: Long): CardCharacter?
-
@Query("SELECT * FROM CardCharacter WHERE charaIndex = :monIndex AND cardId = :dimId LIMIT 1")
fun getCharacterByMonIndex(monIndex: Int, dimId: Long): CardCharacter
- @Query("SELECT * FROM CardCharacter WHERE charaIndex = :monIndex AND cardId = :dimId LIMIT 1")
- fun getCharacterByMonIndexOrNull(monIndex: Int, dimId: Long): CardCharacter?
-
@Insert
suspend fun insertSprite(vararg sprite: Sprite)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/SpriteDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/SpriteDao.kt
index 62943a5..4d8d6e6 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/SpriteDao.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/SpriteDao.kt
@@ -11,9 +11,6 @@ interface SpriteDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertSprite(sprite: Sprite): Long
- @Query("SELECT * FROM Sprite WHERE id = :id LIMIT 1")
- fun getSpriteById(id: Long): Sprite?
-
@Query("SELECT * FROM Sprite")
suspend fun getAllSprites(): List
}
\ 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 b8bd138..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
@@ -4,7 +4,6 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
-import androidx.room.RewriteQueriesToDropUnusedColumns
import androidx.room.Upsert
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
@@ -39,7 +38,6 @@ interface UserCharacterDao {
@Upsert
fun insertSpecialMissions(vararg specialMissions: SpecialMissions)
- @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT
@@ -57,7 +55,6 @@ interface UserCharacterDao {
)
fun getTransformationHistory(monId: Long): Flow>
- @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT
@@ -74,7 +71,6 @@ interface UserCharacterDao {
)
suspend fun getTransformationHistoryForExport(monId: Long): List
- @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT
@@ -100,7 +96,6 @@ interface UserCharacterDao {
)
fun getAllCharacters(): Flow>
- @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT
@@ -133,22 +128,12 @@ interface UserCharacterDao {
@Query("SELECT * FROM BECharacterData WHERE id = :id")
fun getBeData(id: Long): Flow
- @Query("SELECT * FROM BECharacterData WHERE id = :id")
- suspend fun getBeDataOrNull(id: Long): BECharacterData?
-
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
fun getVbData(id: Long): Flow
- @Query("SELECT * FROM VBCharacterData WHERE id = :id")
- suspend fun getVbDataOrNull(id: Long): VBCharacterData?
-
- @Query("DELETE FROM BECharacterData WHERE id = :id")
- suspend fun deleteBECharacterData(id: Long)
-
@Query("SELECT * FROM SpecialMissions WHERE characterId = :id")
fun getSpecialMissions(id: Long): Flow>
- @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT
@@ -214,7 +199,6 @@ interface UserCharacterDao {
@Query("""SELECT * FROM VitalsHistory WHERE charId = :charId ORDER BY id ASC""")
suspend fun getVitalsHistory(charId: Long): List
- @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT
@@ -241,7 +225,6 @@ interface UserCharacterDao {
)
suspend fun getBECharacters(): List
- @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/database/AppDatabase.kt b/app/src/main/java/com/github/nacabaro/vbhelper/database/AppDatabase.kt
index 566f828..7aa784c 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/database/AppDatabase.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/database/AppDatabase.kt
@@ -2,8 +2,6 @@ package com.github.nacabaro.vbhelper.database
import androidx.room.Database
import androidx.room.RoomDatabase
-import androidx.room.migration.Migration
-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
@@ -15,7 +13,6 @@ import com.github.nacabaro.vbhelper.daos.ItemDao
import com.github.nacabaro.vbhelper.daos.SpecialMissionDao
import com.github.nacabaro.vbhelper.daos.SpriteDao
import com.github.nacabaro.vbhelper.daos.UserCharacterDao
-import com.github.nacabaro.vbhelper.daos.VitalWearSettingsDao
import com.github.nacabaro.vbhelper.domain.card.Background
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.card.Card
@@ -32,12 +29,10 @@ import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
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.domain.device_data.VitalWearCharacterSettings
import com.github.nacabaro.vbhelper.domain.items.Items
@Database(
- version = 6,
- exportSchema = false,
+ version = 1,
entities = [
Card::class,
CardProgress::class,
@@ -55,8 +50,7 @@ import com.github.nacabaro.vbhelper.domain.items.Items
Items::class,
Adventure::class,
Background::class,
- PossibleTransformations::class,
- VitalWearCharacterSettings::class,
+ PossibleTransformations::class
]
)
abstract class AppDatabase : RoomDatabase() {
@@ -71,92 +65,4 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun specialMissionDao(): SpecialMissionDao
abstract fun cardAdventureDao(): CardAdventureDao
abstract fun cardFusionsDao(): CardFusionsDao
- abstract fun vitalWearSettingsDao(): VitalWearSettingsDao
-
- companion object {
- val MIGRATION_1_2 = object : Migration(1, 2) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL(
- """
- CREATE TABLE IF NOT EXISTS `VitalWearCharacterSettings` (
- `characterId` INTEGER NOT NULL,
- `trainingInBackground` INTEGER NOT NULL,
- `allowedBattles` INTEGER NOT NULL,
- `accumulatedDailyInjuries` INTEGER NOT NULL,
- PRIMARY KEY(`characterId`)
- )
- """.trimIndent()
- )
-
- // Clean existing duplicates before adding the unique index.
- db.execSQL(
- """
- DELETE FROM `CardCharacter`
- WHERE `id` NOT IN (
- SELECT MIN(`id`) FROM `CardCharacter` GROUP BY `cardId`, `charaIndex`
- )
- """.trimIndent()
- )
- db.execSQL(
- """
- CREATE UNIQUE INDEX IF NOT EXISTS `index_CardCharacter_cardId_charaIndex`
- ON `CardCharacter` (`cardId`, `charaIndex`)
- """.trimIndent()
- )
- }
- }
-
- val MIGRATION_2_3 = object : Migration(2, 3) {
- override fun migrate(db: SupportSQLiteDatabase) {
- val backfillTimestamp = System.currentTimeMillis()
- db.execSQL(
- """
- INSERT INTO `TransformationHistory`(`monId`, `stageId`, `transformationDate`)
- SELECT uc.`id`, uc.`charId`, $backfillTimestamp
- FROM `UserCharacter` uc
- WHERE NOT EXISTS (
- SELECT 1
- FROM `TransformationHistory` th
- WHERE th.`monId` = uc.`id`
- )
- """.trimIndent()
- )
- }
- }
-
- val MIGRATION_3_5 = object : Migration(3, 5) {
- override fun migrate(db: SupportSQLiteDatabase) {
- // TransferSeen moved to shared_transfer.db
- db.execSQL("DROP TABLE IF EXISTS `TransferSeen`")
- db.execSQL("DROP INDEX IF EXISTS `index_TransferSeen_cardLookupKey_slotId`")
- }
- }
-
- val MIGRATION_4_5 = object : Migration(4, 5) {
- override fun migrate(db: SupportSQLiteDatabase) {
- // TransferSeen moved to shared_transfer.db
- db.execSQL("DROP TABLE IF EXISTS `TransferSeen`")
- db.execSQL("DROP INDEX IF EXISTS `index_TransferSeen_cardLookupKey_slotId`")
- }
- }
-
- val MIGRATION_5_6 = object : Migration(5, 6) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_CardCharacter_spriteId` ON `CardCharacter` (`spriteId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_CardAdventure_cardId` ON `CardAdventure` (`cardId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_CardAdventure_characterId` ON `CardAdventure` (`characterId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_CardFusions_fromCharaId` ON `CardFusions` (`fromCharaId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_CardFusions_toCharaId` ON `CardFusions` (`toCharaId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_Background_cardId` ON `Background` (`cardId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_PossibleTransformations_charaId` ON `PossibleTransformations` (`charaId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_PossibleTransformations_toCharaId` ON `PossibleTransformations` (`toCharaId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_UserCharacter_charId` ON `UserCharacter` (`charId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_SpecialMissions_characterId` ON `SpecialMissions` (`characterId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_TransformationHistory_monId` ON `TransformationHistory` (`monId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_TransformationHistory_stageId` ON `TransformationHistory` (`stageId`)")
- db.execSQL("CREATE INDEX IF NOT EXISTS `index_VitalsHistory_charId` ON `VitalsHistory` (`charId`)")
- }
- }
-
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/di/AppContainer.kt b/app/src/main/java/com/github/nacabaro/vbhelper/di/AppContainer.kt
index ff6d8fc..2d82b95 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/di/AppContainer.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/di/AppContainer.kt
@@ -1,15 +1,11 @@
package com.github.nacabaro.vbhelper.di
-import com.github.cfogrady.vitalwear.common.data.SharedTransferSeenDao
import com.github.nacabaro.vbhelper.database.AppDatabase
-import com.github.nacabaro.vbhelper.source.CardSettingsRepository
import com.github.nacabaro.vbhelper.source.CurrencyRepository
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
interface AppContainer {
val db: AppDatabase
- val transferSeenDao: SharedTransferSeenDao
val dataStoreSecretsRepository: DataStoreSecretsRepository
val currencyRepository: CurrencyRepository
- val cardSettingsRepository: CardSettingsRepository
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/di/DefaultAppContainer.kt b/app/src/main/java/com/github/nacabaro/vbhelper/di/DefaultAppContainer.kt
index c949f86..70305b2 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/di/DefaultAppContainer.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/di/DefaultAppContainer.kt
@@ -4,11 +4,8 @@ import androidx.datastore.dataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import androidx.room.Room
-import com.github.cfogrady.vitalwear.common.data.SharedDatabaseFactory
-import com.github.cfogrady.vitalwear.common.data.SharedTransferSeenDao
-import com.github.nacabaro.vbhelper.di.AppContainer
import com.github.nacabaro.vbhelper.database.AppDatabase
-import com.github.nacabaro.vbhelper.source.CardSettingsRepository
+import com.github.nacabaro.vbhelper.di.AppContainer
import com.github.nacabaro.vbhelper.source.CurrencyRepository
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
import com.github.nacabaro.vbhelper.source.SecretsSerializer
@@ -34,22 +31,11 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
"internalDb"
)
.createFromAsset("items.db")
- .addMigrations(AppDatabase.MIGRATION_1_2)
- .addMigrations(AppDatabase.MIGRATION_2_3)
- .addMigrations(AppDatabase.MIGRATION_3_5)
- .addMigrations(AppDatabase.MIGRATION_4_5)
- .addMigrations(AppDatabase.MIGRATION_5_6)
.build()
}
- override val transferSeenDao: SharedTransferSeenDao by lazy {
- SharedDatabaseFactory.getDatabase(context).transferSeenDao()
- }
-
override val dataStoreSecretsRepository = DataStoreSecretsRepository(context.secretsStore)
override val currencyRepository = CurrencyRepository(context.currencyStore)
-
- override val cardSettingsRepository = CardSettingsRepository(context.currencyStore)
}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/di/VBHelper.kt b/app/src/main/java/com/github/nacabaro/vbhelper/di/VBHelper.kt
index b6e6c07..0c66c99 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/di/VBHelper.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/di/VBHelper.kt
@@ -2,37 +2,12 @@ package com.github.nacabaro.vbhelper.di
import DefaultAppContainer
import android.app.Application
-import androidx.room.Room
-import com.github.nacabaro.vbhelper.companion.logging.TinyLogTree
-import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
-import com.github.nacabaro.vbhelper.companion.validation.CompanionValidatedDatabase
-import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
-import timber.log.Timber
class VBHelper : Application() {
lateinit var container: DefaultAppContainer
- lateinit var validatedCardDatabase: CompanionValidatedDatabase
- lateinit var validatedCardManager: ValidatedCardManager
- val companionLogService = CompanionLogService()
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer(applicationContext)
-
- Timber.plant(Timber.DebugTree())
- Timber.plant(TinyLogTree(this))
- val originalExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
- Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
- Timber.e(throwable, "VBHelper crashed on thread ${thread.name}")
- TinyLogTree.shutdown()
- originalExceptionHandler?.uncaughtException(thread, throwable)
- }
-
- validatedCardDatabase = Room.databaseBuilder(
- applicationContext,
- CompanionValidatedDatabase::class.java,
- "vbhelper_companion_tools"
- ).build()
- validatedCardManager = ValidatedCardManager(validatedCardDatabase.validatedCardDao())
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/Background.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/Background.kt
index d49c49a..23766d4 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/Background.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/Background.kt
@@ -2,11 +2,9 @@ package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.ForeignKey
-import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
- indices = [Index(value = ["cardId"])],
foreignKeys = [
ForeignKey(
entity = Card::class,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardAdventure.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardAdventure.kt
index 5fe0e55..b8f4f56 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardAdventure.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardAdventure.kt
@@ -2,14 +2,9 @@ package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.ForeignKey
-import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
- indices = [
- Index(value = ["cardId"]),
- Index(value = ["characterId"])
- ],
foreignKeys = [
ForeignKey(
entity = CardCharacter::class,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardCharacter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardCharacter.kt
index bfed547..050cc9a 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardCharacter.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardCharacter.kt
@@ -2,16 +2,11 @@ package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.ForeignKey
-import androidx.room.Index
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.domain.characters.Sprite
@Entity(
- indices = [
- Index(value = ["cardId", "charaIndex"], unique = true),
- Index(value = ["spriteId"])
- ],
foreignKeys = [
ForeignKey(
entity = Card::class,
@@ -29,8 +24,9 @@ import com.github.nacabaro.vbhelper.domain.characters.Sprite
)
/*
- * Character represents a character on a specific card slot.
- * Uniqueness is enforced per cardId + charaIndex.
+ * Character represents a character on a card. There should only be one of these per dimId
+ * and monIndex.
+ * TODO: Customs will mean this should be unique per cardName and monIndex
*/
data class CardCharacter (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt
index ecc4e41..1769eda 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt
@@ -2,15 +2,10 @@ package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.ForeignKey
-import androidx.room.Index
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter
@Entity(
- indices = [
- Index(value = ["fromCharaId"]),
- Index(value = ["toCharaId"])
- ],
foreignKeys = [
ForeignKey(
entity = CardCharacter::class,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/PossibleTransformations.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/PossibleTransformations.kt
index 5312070..2dc2e8b 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/PossibleTransformations.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/PossibleTransformations.kt
@@ -2,14 +2,9 @@ package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.ForeignKey
-import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
- indices = [
- Index(value = ["charaId"]),
- Index(value = ["toCharaId"])
- ],
foreignKeys = [
ForeignKey(
entity = CardCharacter::class,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/SpecialMissions.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/SpecialMissions.kt
index ef2e3dd..13b092d 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/SpecialMissions.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/SpecialMissions.kt
@@ -2,12 +2,10 @@ package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity
import androidx.room.ForeignKey
-import androidx.room.Index
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.vb.SpecialMission
@Entity(
- indices = [Index(value = ["characterId"])],
foreignKeys = [
ForeignKey(
entity = UserCharacter::class,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/TransformationHistory.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/TransformationHistory.kt
index 436af2b..0a560cc 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/TransformationHistory.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/TransformationHistory.kt
@@ -2,15 +2,10 @@ package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity
import androidx.room.ForeignKey
-import androidx.room.Index
import androidx.room.PrimaryKey
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
@Entity(
- indices = [
- Index(value = ["monId"]),
- Index(value = ["stageId"])
- ],
foreignKeys = [
ForeignKey(
entity = UserCharacter::class,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/UserCharacter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/UserCharacter.kt
index 3a68aa4..e34b57d 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/UserCharacter.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/UserCharacter.kt
@@ -2,14 +2,12 @@ package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity
import androidx.room.ForeignKey
-import androidx.room.Index
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.utils.DeviceType
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
@Entity(
- indices = [Index(value = ["charId"])],
foreignKeys = [
ForeignKey(
entity = CardCharacter::class,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/VitalsHistory.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/VitalsHistory.kt
index 163b44e..313e7c1 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/VitalsHistory.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/VitalsHistory.kt
@@ -2,11 +2,9 @@ package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity
import androidx.room.ForeignKey
-import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
- indices = [Index(value = ["charId"])],
foreignKeys = [
ForeignKey(
entity = UserCharacter::class,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt
index 22f43be..86b474d 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt
@@ -6,7 +6,6 @@ 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
@@ -15,7 +14,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.NavHost
-import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.github.nacabaro.vbhelper.di.VBHelper
@@ -60,21 +58,6 @@ fun AppNavigation(
) {
val navController = rememberNavController()
- LaunchedEffect(initialRoute) {
- val route = initialRoute ?: return@LaunchedEffect
- if (navController.currentBackStackEntry?.destination?.route == route) {
- return@LaunchedEffect
- }
- navController.navigate(route) {
- popUpTo(navController.graph.findStartDestination().id) {
- inclusive = false
- saveState = false
- }
- launchSingleTop = true
- restoreState = false
- }
- }
-
Scaffold(
bottomBar = {
BottomNavigationBar(navController = navController)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/BottomNavigationBar.kt b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/BottomNavigationBar.kt
index 0118833..16e6728 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/BottomNavigationBar.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/BottomNavigationBar.kt
@@ -7,7 +7,6 @@ import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavController
-import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.ui.res.stringResource
@@ -31,14 +30,17 @@ fun BottomNavigationBar(navController: NavController) {
label = { Text(text = stringResource(item.label)) },
selected = currentRoute == item.route,
onClick = {
- navController.navigate(item.route) {
- // Always route tab clicks to each tab's root screen.
- popUpTo(navController.graph.findStartDestination().id) {
- inclusive = false
- saveState = false
+ if (item == NavigationItems.Home) {
+ navController.navigate(NavigationItems.Home.route) {
+ popUpTo(0) { inclusive = false }
+ launchSingleTop = true
+ }
+ } else {
+ navController.navigate(item.route) {
+ popUpTo(navController.graph.startDestinationId) { saveState = true }
+ launchSingleTop = true
+ restoreState = true
}
- launchSingleTop = true
- restoreState = false
}
}
)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationItems.kt b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationItems.kt
index 27af488..77b9b91 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationItems.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationItems.kt
@@ -6,8 +6,8 @@ import androidx.annotation.StringRes
sealed class NavigationItems(
val route: String,
- @param:DrawableRes val icon: Int,
- @param:StringRes val label: Int
+ @DrawableRes val icon: Int,
+ @StringRes val label: Int
) {
object Scan : NavigationItems(
"Scan/{characterId}",
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt
index 179776b..dad71ac 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt
@@ -51,7 +51,6 @@ import android.content.Intent
import android.net.Uri
import android.media.MediaPlayer
import android.os.Environment
-import android.widget.Toast
//import androidx.compose.animation.core.animateFloatAsState
//import androidx.compose.animation.core.tween
import kotlinx.coroutines.delay
@@ -65,8 +64,6 @@ import com.github.nacabaro.vbhelper.battle.APIBattleCharacter
import android.util.Log
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.battle.RetrofitHelper
-import com.github.nacabaro.vbhelper.battle.extractSessionToken
-import com.github.nacabaro.vbhelper.battle.extractUserId
import com.github.nacabaro.vbhelper.battle.AttackSpriteImage
import com.github.nacabaro.vbhelper.battle.SpriteFileManager
import com.github.nacabaro.vbhelper.battle.ArenaBattleSystem
@@ -74,7 +71,6 @@ import com.github.nacabaro.vbhelper.battle.DigimonAnimationType
import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage
import com.github.nacabaro.vbhelper.battle.HitEffectOverlay
import com.github.nacabaro.vbhelper.battle.BattleAuthContainer
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.collect
import androidx.compose.foundation.lazy.LazyColumn
@@ -903,6 +899,7 @@ fun MiddleBattleView(
y = enemyVerticalOffset + 40.dp
),
contentScale = ContentScale.Fit,
+ reloadMappings = false,
animationOffset = 375L // Offset enemy animation by half the idle duration
)
@@ -989,6 +986,7 @@ fun MiddleBattleView(
y = playerVerticalOffset - 40.dp
),
contentScale = ContentScale.Fit,
+ reloadMappings = false,
animationOffset = 0L // Player animation starts immediately
)
@@ -1074,30 +1072,19 @@ fun MiddleBattleView(
// Capture userId for use in this lambda
val playerUserId = userId
- if (playerUserId == null) {
- context?.let {
- Toast.makeText(it, "Session missing user id. Please re-authenticate.", Toast.LENGTH_SHORT).show()
- }
- battleSystem.resetAttackState()
- battleSystem.enableAttackButton()
- return@Button
- }
-
+
// Get crit bar progress as float (0.0f to 100.0f)
val critBarProgressFloat = battleSystem.critBarProgress.toFloat()
// Determine player and opponent stages
- // activeCharacter.stage is DB stage (2=rookie, 3=champion, 4=ultimate, 5=mega)
- // API expects stage 0-3
val playerStage = when (activeCharacter?.stage) {
- 2 -> 0 // rookie
- 3 -> 1 // champion
- 4 -> 2 // ultimate
- 5 -> 3 // mega
+ 0 -> 0 // rookie
+ 1 -> 1 // champion
+ 2 -> 2 // ultimate
+ 3 -> 3 // mega
else -> 0
}
- // opponentCharacter.stage comes from the API and is already 0-3
val opponentStage = when (opponentCharacter?.stage) {
0 -> 0 // rookie
1 -> 1 // champion
@@ -1114,11 +1101,11 @@ fun MiddleBattleView(
RetrofitHelper().getPVPWinner(
ctx,
1,
- playerUserId,
- activeCharacter?.charaId ?: "dim011_mon01",
- playerStage,
- battleSystem.critBarProgress,
- opponentCharacter?.charaId ?: "dim011_mon01",
+ playerUserId ?: 2L,
+ activeCharacter?.name ?: "Player",
+ playerStage,
+ opponentStage,
+ opponentCharacter?.name ?: "Opponent",
opponentStage
) { apiResult ->
// Handle API response here
@@ -1322,6 +1309,7 @@ fun PlayerBattleView(
y = verticalOffset
),
contentScale = ContentScale.Fit,
+ reloadMappings = false,
animationOffset = 0L // Player animation starts immediately
)
@@ -1503,6 +1491,7 @@ fun EnemyBattleView(
y = verticalOffset
),
contentScale = ContentScale.Fit,
+ reloadMappings = false,
animationOffset = 375L // Offset enemy animation by half the idle duration
)
@@ -1652,63 +1641,8 @@ fun BattlesScreen() {
// Track processed tokens to prevent duplicate API calls
// Use rememberSaveable to persist across configuration changes (screen rotation)
var processedTokens by rememberSaveable { mutableStateOf>(emptySet()) }
- var isProcessingAuthCallback by rememberSaveable { mutableStateOf(false) }
- var authBrowserLaunchInFlight by rememberSaveable { mutableStateOf(false) }
var opponentsList by remember { mutableStateOf(ArrayList()) }
- var isLoadingOpponents by remember { mutableStateOf(false) }
- var authReadyToken by remember { mutableStateOf(null) }
-
- LaunchedEffect(Unit) {
- battleAuthContainer.authRepository.sessionToken
- .combine(battleAuthContainer.authRepository.authToken) { sessionToken, nacatechToken ->
- sessionToken?.takeIf { it.isNotBlank() }
- ?: nacatechToken?.takeIf { it.isNotBlank() }
- }
- .collect { token ->
- authReadyToken = token
- if (token.isNullOrBlank() && opponentsList.isNotEmpty()) {
- opponentsList = ArrayList()
- }
- }
- }
-
- val openBattleAuthPage: () -> Unit = openAuth@{
- if (isAuthenticated && !authReadyToken.isNullOrBlank()) {
- println("BATTLESCREEN: Skipping auth browser launch because session is already ready")
- return@openAuth
- }
- if (isProcessingAuthCallback) {
- println("BATTLESCREEN: Skipping auth browser launch while callback is being processed")
- return@openAuth
- }
- if (authBrowserLaunchInFlight) {
- println("BATTLESCREEN: Auth browser launch already in flight, skipping duplicate request")
- return@openAuth
- }
-
- val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token="
- authBrowserLaunchInFlight = true
- try {
- val customTabsIntent = androidx.browser.customtabs.CustomTabsIntent.Builder()
- .setShowTitle(true)
- .build()
- customTabsIntent.launchUrl(context, Uri.parse(authUrl))
- println("BATTLESCREEN: Opened auth URL in Custom Tab: $authUrl")
- } catch (e: Exception) {
- authBrowserLaunchInFlight = false
- println("BATTLESCREEN: Failed to open auth URL in Custom Tab: ${e.message}")
- // Fall back to external browser if Custom Tabs is unavailable
- try {
- val fallbackIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))
- context.startActivity(fallbackIntent)
- println("BATTLESCREEN: Fallback to external browser succeeded")
- } catch (e2: Exception) {
- println("BATTLESCREEN: Fallback browser also failed: ${e2.message}")
- e2.printStackTrace()
- }
- }
- }
var activeCharacter by remember { mutableStateOf(null) }
var selectedOpponent by remember { mutableStateOf(null) }
@@ -1790,86 +1724,40 @@ fun BattlesScreen() {
// Determine if player can battle based on stage (derived from activeUserCharacter)
val canBattle = activeUserCharacter?.stage?.let { it >= 2 } ?: false
- val loadOpponentsIfReady: () -> Unit = loadOpponents@{
- val currentCharacter = activeUserCharacter
- val readyToken = authReadyToken
-
+ // Load opponents automatically based on player's stage
+ // Only load if authenticated and character is ready
+ LaunchedEffect(activeUserCharacter, isAuthenticated) {
+ // Wait for authentication to complete before loading opponents
if (!isAuthenticated) {
- if (opponentsList.isNotEmpty()) {
- opponentsList = ArrayList()
- }
- return@loadOpponents
+ return@LaunchedEffect
}
-
- if (readyToken.isNullOrBlank()) {
- println("BATTLESCREEN: Skipping opponent load - auth token not persisted yet")
- return@loadOpponents
- }
-
- if (isLoadingOpponents) {
- println("BATTLESCREEN: Opponents load already in flight, skipping duplicate request")
- return@loadOpponents
- }
-
- if (currentCharacter != null && currentCharacter.stage >= 2) {
- val battleType = when (currentCharacter.stage) {
- 2 -> "rookie"
- 3 -> "champion"
- 4 -> "ultimate"
- 5 -> "mega"
- else -> null
- }
- if (battleType == null) {
- println("BATTLESCREEN: Stage ${currentCharacter.stage} has no battle type, skipping")
- return@loadOpponents
- }
- try {
- isLoadingOpponents = true
- RetrofitHelper().getOpponents(context, battleType, onComplete = {
- isLoadingOpponents = false
- }) { opponents ->
- try {
- opponentsList = ArrayList(opponents.opponentsList)
- println("BATTLESCREEN: Loaded ${opponents.opponentsList.size} opponents for stage $battleType")
- } catch (e: Exception) {
- Log.d(TAG, "Error processing opponents data: ${e.message}")
- e.printStackTrace()
- }
- }
- } catch (e: Exception) {
- isLoadingOpponents = false
- Log.d(TAG, "Error calling getOpponents: ${e.message}")
- e.printStackTrace()
- }
- } else {
- if (opponentsList.isNotEmpty()) {
- opponentsList = ArrayList()
- }
- println("BATTLESCREEN: Cannot load opponents - stage ${currentCharacter?.stage}, must be >= 2")
- }
- }
-
- // Load opponents automatically based on player's stage once auth is truly ready.
- LaunchedEffect(activeUserCharacter, isAuthenticated, authReadyToken, playerBattleType) {
- loadOpponentsIfReady()
- }
-
- // Re-fetch opponents when returning from browser (resume)
- DisposableEffect(context, isAuthenticated, activeUserCharacter, canBattle, playerBattleType, authReadyToken) {
- val activity = context as? ComponentActivity
- val lifecycleOwner = activity as? LifecycleOwner
- val observer = LifecycleEventObserver { _, event ->
- if (event == Lifecycle.Event.ON_RESUME) {
- if (isAuthenticated && !authReadyToken.isNullOrBlank()) {
- loadOpponentsIfReady()
+ val currentCharacter = activeUserCharacter
+ if (currentCharacter != null && canBattle && playerBattleType != null) {
+ try {
+ RetrofitHelper().getOpponents(context, playerBattleType!!) { opponents ->
+ try {
+ // Create a new list to trigger UI recomposition
+ opponentsList = ArrayList(opponents.opponentsList)
+ } catch (e: Exception) {
+ Log.d(TAG, "Error processing opponents data: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(TAG,"Error calling getOpponents: ${e.message}")
+ e.printStackTrace()
}
+ } else {
+ println("BATTLESCREEN: Cannot load opponents - activeUserCharacter: $currentCharacter")
+ println("BATTLESCREEN: canBattle: $canBattle")
+ println("BATTLESCREEN: playerBattleType: $playerBattleType")
+ println("BATTLESCREEN: currentCharacter != null: ${currentCharacter != null}")
+ if (currentCharacter != null) {
+ println("BATTLESCREEN: currentCharacter.stage: ${currentCharacter.stage}")
+ println("BATTLESCREEN: currentCharacter.stage >= 2: ${currentCharacter.stage >= 2}")
}
}
- lifecycleOwner?.lifecycle?.addObserver(observer)
- onDispose {
- lifecycleOwner?.lifecycle?.removeObserver(observer)
- }
}
// Helper lambda to extract token from URI and authenticate
@@ -1887,19 +1775,17 @@ fun BattlesScreen() {
if (!processedTokens.contains(token)) {
// Mark token as being processed IMMEDIATELY to prevent duplicate API calls
processedTokens = processedTokens + token
- isProcessingAuthCallback = true
- authBrowserLaunchInFlight = false
-
+
// Exchange token with battle server
RetrofitHelper().authenticate(context, token) { response ->
if (response.success) {
// Extract userId and sessionToken from response
- val extractedUserId = response.extractUserId()
- val sessionToken = response.extractSessionToken()
-
+ val extractedUserId = response.userInfo?.userId?.toLongOrNull()
+ val sessionToken = response.sessionToken
+
println("BATTLESCREEN: Authentication successful, userId: $extractedUserId, sessionToken: ${if (sessionToken != null) "present" else "missing"}")
- // Persist auth first so follow-up API calls can immediately use the token.
+ // Store both nacatech token (for re-auth) and sessionToken (for API calls)
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
battleAuthContainer.authRepository.setAuthenticated(
isAuthenticated = true,
@@ -1907,16 +1793,14 @@ fun BattlesScreen() {
sessionToken = sessionToken,
userId = extractedUserId
)
-
- withContext(Dispatchers.Main) {
- isAuthenticated = true
- isCheckingAuth = false
- userId = extractedUserId
- isProcessingAuthCallback = false
- authBrowserLaunchInFlight = false
- println("BATTLESCREEN: Authentication successful, userId: $extractedUserId")
- android.widget.Toast.makeText(context, "Authentication successful!", android.widget.Toast.LENGTH_SHORT).show()
- }
+ }
+ // Update UI state on main thread
+ kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch {
+ isAuthenticated = true
+ isCheckingAuth = false
+ userId = extractedUserId
+ println("BATTLESCREEN: Authentication successful, userId: $extractedUserId")
+ android.widget.Toast.makeText(context, "Authentication successful!", android.widget.Toast.LENGTH_SHORT).show()
}
} else {
println("BATTLESCREEN: Authentication failed: ${response.message}")
@@ -1931,16 +1815,23 @@ fun BattlesScreen() {
kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch {
isAuthenticated = false
isCheckingAuth = false
- isProcessingAuthCallback = false
- authBrowserLaunchInFlight = false
- println("BATTLESCREEN: Auth callback token was already used; avoiding automatic browser relaunch loop")
- }
+ // Small delay to ensure state is updated
+ kotlinx.coroutines.delay(100)
+ // Open auth URL to get a fresh token
+ val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token="
+ val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))
+ try {
+ context.startActivity(authIntent)
+ println("BATTLESCREEN: Opened auth URL after token expiration: $authUrl")
+ } catch (e: Exception) {
+ println("BATTLESCREEN: Failed to open auth URL: ${e.message}")
+ e.printStackTrace()
+ }
+ }
} else {
// For other errors, remove from processed set to allow retry with a new token
println("BATTLESCREEN: Authentication failed, removing token from processed set to allow retry")
processedTokens = processedTokens - token
- isProcessingAuthCallback = false
- authBrowserLaunchInFlight = false
}
// Show toast on main thread
kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch {
@@ -1948,47 +1839,12 @@ fun BattlesScreen() {
}
}
}
- } else {
- println("BATTLESCREEN: Ignoring already processed auth token")
}
} else {
println("BATTLESCREEN: No token found in URI: $uri (checked 'c' and 'token' parameters)")
}
}
-
- val consumeAuthCallbackIntent: () -> Boolean = consume@{
- val currentActivity = context as? ComponentActivity ?: return@consume false
- val incomingIntent = currentActivity.intent ?: return@consume false
- if (incomingIntent.action != Intent.ACTION_VIEW) return@consume false
-
- val uri = incomingIntent.data ?: return@consume false
- val isAppAuthCallback = uri.scheme == "vbhelper" && uri.host == "auth"
- val isLocalhostAuthCallback =
- (uri.scheme == "http" || uri.scheme == "https") &&
- (uri.host == "localhost" || uri.host == "127.0.0.1") &&
- uri.path?.startsWith("/authenticate") == true
- val hasAuthToken = !uri.getQueryParameter("c").isNullOrEmpty() ||
- !uri.getQueryParameter("token").isNullOrEmpty()
-
- if (!hasAuthToken) {
- return@consume false
- }
-
- if (!isAppAuthCallback && !isLocalhostAuthCallback) {
- return@consume false
- }
-
- handleTokenFromUri(uri)
-
- // Clear callback intent so reopening Battles does not replay stale deep links.
- currentActivity.setIntent(Intent(incomingIntent).apply {
- action = Intent.ACTION_MAIN
- data = null
- })
-
- true
- }
-
+
// Check authentication status on load
LaunchedEffect(Unit) {
try {
@@ -1996,8 +1852,7 @@ fun BattlesScreen() {
val localAuthState = authRepository.isAuthenticated.first()
val storedToken = authRepository.authToken.first()
val storedUserId = authRepository.userId.first()
- val storedSessionToken = authRepository.sessionToken.first()
-
+
// Load stored userId if available
if (storedUserId != null) {
userId = storedUserId
@@ -2012,36 +1867,41 @@ fun BattlesScreen() {
userId = storedUserId
}
- if (consumeAuthCallbackIntent()) {
- return@LaunchedEffect
+ // Only check for token in intent if it's a fresh deep link (ACTION_VIEW intent)
+ // This prevents processing stale tokens from previous sessions
+ val activity = context as? ComponentActivity
+ val intent = activity?.intent
+ if (intent?.action == Intent.ACTION_VIEW) {
+ intent.data?.let { uri ->
+ if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) {
+ handleTokenFromUri(uri)
+ return@LaunchedEffect // Don't open auth URL if we're processing a token
+ }
+ }
}
// If we have a stored token and userId, assume session is still active
// The single-use token can't be re-validated, but the server maintains a session after initial validation
// We'll only re-authenticate if API calls fail with authentication errors
- if (localAuthState && storedToken != null && storedToken.isNotEmpty() && (storedUserId != null || !storedSessionToken.isNullOrEmpty())) {
+ if (localAuthState && storedToken != null && storedToken.isNotEmpty() && storedUserId != null) {
// Session appears to be active - user is already authenticated
// No need to re-validate the single-use token, just restore the session
- println("BATTLESCREEN: Restoring active session (userId: $storedUserId, sessionTokenPresent=${!storedSessionToken.isNullOrEmpty()})")
+ println("BATTLESCREEN: Restoring active session (userId: $storedUserId)")
isAuthenticated = true
isCheckingAuth = false
userId = storedUserId
// Session is restored, no need to validate token again
- } else if (localAuthState && storedToken != null && storedToken.isNotEmpty() && storedSessionToken.isNullOrEmpty()) {
+ } else if (localAuthState && storedToken != null && storedToken.isNotEmpty()) {
// We have a token but no userId - try to validate once to get userId
// This should only happen on first login or if userId was lost
println("BATTLESCREEN: Have token but no userId, validating once to get userId...")
- RetrofitHelper().authenticate(
- context = context,
- token = storedToken,
- showUserFacingErrors = false,
- ) { response ->
+ RetrofitHelper().authenticate(context, storedToken) { response ->
// Update UI on main thread
kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch {
if (response.success) {
- val extractedUserId = response.extractUserId()
- val sessionToken = response.extractSessionToken()
-
+ val extractedUserId = response.userInfo?.userId?.toLongOrNull()
+ val sessionToken = response.sessionToken
+
// Update stored userId and sessionToken
if (extractedUserId != null) {
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
@@ -2064,22 +1924,8 @@ fun BattlesScreen() {
response.message?.contains("nonce") == true ||
response.message?.contains("invalid") == true ||
response.message?.contains("expired") == true
- val latestSessionToken = withContext(Dispatchers.IO) {
- authRepository.sessionToken.first()
- }
- val latestUserId = withContext(Dispatchers.IO) {
- authRepository.userId.first()
- }
-
+
if (isCriticalError) {
- // A parallel callback auth can complete before this fallback check returns.
- if (!latestSessionToken.isNullOrEmpty() || latestUserId != null) {
- println("BATTLESCREEN: Ignoring nonce validation failure because a valid session already exists")
- isAuthenticated = true
- isCheckingAuth = false
- userId = latestUserId ?: userId
- return@launch
- }
// Critical error - token is invalid, need to re-authenticate
println("BATTLESCREEN: Critical authentication error, clearing state and redirecting")
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
@@ -2087,7 +1933,11 @@ fun BattlesScreen() {
}
isAuthenticated = false
isCheckingAuth = false
- openBattleAuthPage()
+ // Open auth URL
+ val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token="
+ val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))
+ context.startActivity(authIntent)
+ println("BATTLESCREEN: Opened auth URL after critical validation failure: $authUrl")
} else {
// Non-critical error (e.g., network issue) - keep authenticated state
println("BATTLESCREEN: Non-critical validation error, keeping authenticated state")
@@ -2102,7 +1952,10 @@ fun BattlesScreen() {
isAuthenticated = false
isCheckingAuth = false
// If not authenticated and no fresh token in intent, open auth URL
- openBattleAuthPage()
+ val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token="
+ val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))
+ context.startActivity(authIntent)
+ println("BATTLESCREEN: Opened auth URL: $authUrl")
}
} catch (e: Exception) {
println("BATTLESCREEN: Error checking authentication status: ${e.message}")
@@ -2111,11 +1964,42 @@ fun BattlesScreen() {
}
}
- // Handle deep link callback once on initial load and consume it.
+ // Handle deep link callback to get token
+ // Check intent data on initial load - handle both vbhelper:// and http://localhost:8080/authenticate?c=
+ // Only process if it's a fresh ACTION_VIEW intent (deep link)
LaunchedEffect(Unit) {
// Small delay to ensure activity is fully initialized
kotlinx.coroutines.delay(100)
- consumeAuthCallbackIntent()
+
+ val activity = context as? ComponentActivity
+ val intent = activity?.intent
+
+ // Only process if this is a fresh deep link (ACTION_VIEW)
+ if (intent?.action == Intent.ACTION_VIEW) {
+ val uri = intent.data
+ if (uri != null) {
+
+ // Handle vbhelper://auth?token= or vbhelper://auth?c= deep link
+ if (uri.scheme == "vbhelper" && uri.host == "auth") {
+ handleTokenFromUri(uri)
+ }
+ // Handle http://localhost:8080/authenticate?c= redirect
+ else if ((uri.scheme == "http" || uri.scheme == "https") &&
+ (uri.host == "localhost" || uri.host == "127.0.0.1" || uri.host?.contains("8080") == true)) {
+ handleTokenFromUri(uri)
+ }
+ // Also check if there's a 'c' or 'token' parameter in any URL
+ else if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) {
+ handleTokenFromUri(uri)
+ } else {
+ println("BATTLESCREEN: URI found but no token parameter detected")
+ }
+ } else {
+ println("BATTLESCREEN: ACTION_VIEW intent but no URI found")
+ }
+ } else {
+ println("BATTLESCREEN: Not an ACTION_VIEW intent, skipping deep link processing")
+ }
}
// Check intent when screen becomes visible or when authentication state changes
@@ -2125,10 +2009,15 @@ fun BattlesScreen() {
val lifecycleOwner = activity as? LifecycleOwner
val observer = LifecycleEventObserver { _, event ->
- if (event == Lifecycle.Event.ON_RESUME) {
- authBrowserLaunchInFlight = false
- if (!isAuthenticated) {
- consumeAuthCallbackIntent()
+ if (event == Lifecycle.Event.ON_RESUME && !isAuthenticated) {
+ // Check intent data when activity resumes - only if it's a fresh ACTION_VIEW intent
+ val intent = activity?.intent
+ if (intent?.action == Intent.ACTION_VIEW) {
+ intent.data?.let { uri ->
+ if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) {
+ handleTokenFromUri(uri)
+ }
+ }
}
}
}
@@ -2149,16 +2038,45 @@ fun BattlesScreen() {
isAuthenticated = false
isCheckingAuth = false
// Open auth URL to get a fresh token
- openBattleAuthPage()
+ val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token="
+ val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))
+ try {
+ context.startActivity(authIntent)
+ println("BATTLESCREEN: Opened auth URL after token expiration: $authUrl")
+ } catch (e: Exception) {
+ println("BATTLESCREEN: Failed to open auth URL: ${e.message}")
+ e.printStackTrace()
+ }
}
}
}
- // Also check for callback when auth state changes.
+ // Also check intent when authentication state changes
+ // Only process if it's a fresh ACTION_VIEW intent (deep link)
LaunchedEffect(isAuthenticated) {
if (!isAuthenticated) {
kotlinx.coroutines.delay(200) // Small delay to ensure intent is available
- consumeAuthCallbackIntent()
+ val activity = context as? ComponentActivity
+ val intent = activity?.intent
+ // Only process if this is a fresh deep link (ACTION_VIEW)
+ if (intent?.action == Intent.ACTION_VIEW) {
+ intent.data?.let { uri ->
+ println("BATTLESCREEN: Re-checking ACTION_VIEW intent data - URI: $uri, scheme: ${uri.scheme}, host: ${uri.host}")
+ // Handle vbhelper://auth?token= or vbhelper://auth?c= deep link
+ if (uri.scheme == "vbhelper" && uri.host == "auth") {
+ handleTokenFromUri(uri)
+ }
+ // Handle http://localhost:8080/authenticate?c= redirect
+ else if ((uri.scheme == "http" || uri.scheme == "https") &&
+ (uri.host == "localhost" || uri.host == "127.0.0.1" || uri.host?.contains("8080") == true)) {
+ handleTokenFromUri(uri)
+ }
+ // Also check if there's a 'c' or 'token' parameter in any URL
+ else if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) {
+ handleTokenFromUri(uri)
+ }
+ }
+ }
}
}
@@ -2166,8 +2084,9 @@ fun BattlesScreen() {
// Only check if permission is granted
LaunchedEffect(hasStoragePermission) {
if (hasStoragePermission) {
- val spriteFileManager = SpriteFileManager()
- if (!spriteFileManager.checkSpriteFilesExist()) {
+ val spriteFileManager = SpriteFileManager(context)
+ if (spriteFileManager.checkSpriteFilesExist()) {
+ } else {
println("BATTLESCREEN: Sprite files not found in external storage")
}
} else {
@@ -2314,10 +2233,6 @@ fun BattlesScreen() {
color = Color.Gray,
textAlign = TextAlign.Center
)
- Spacer(modifier = Modifier.height(16.dp))
- Button(onClick = { openBattleAuthPage() }) {
- Text("Open Login")
- }
}
}
}
@@ -2352,12 +2267,6 @@ fun BattlesScreen() {
items(opponentsList) { opponent ->
Button(
onClick = {
- val currentUserId = userId
- if (currentUserId == null) {
- Toast.makeText(context, "Session missing user id. Please re-authenticate.", Toast.LENGTH_SHORT).show()
- return@Button
- }
-
activeCardId?.let { cardId ->
selectedOpponent = opponent
// Randomly select background set (0, 1, or 2)
@@ -2372,7 +2281,7 @@ fun BattlesScreen() {
else -> 0
}
- RetrofitHelper().getPVPWinner(context, 0, currentUserId, cardId, apiStage, 0, opponent.charaId, apiStage) { apiResult ->
+ RetrofitHelper().getPVPWinner(context, 0, userId ?: 2L, cardId, apiStage, 0, opponent.charaId, apiStage) { apiResult ->
// Check if there's an existing match
when {
apiResult.status.contains("Existing match found", ignoreCase = true) -> {
@@ -2473,17 +2382,14 @@ fun BattlesScreen() {
LaunchedEffect(Unit) {
// Determine player and opponent stages
- // activeCharacter.stage is DB stage (2=rookie, 3=champion, 4=ultimate, 5=mega)
- // API expects stage 0-3
val playerStage = when (activeCharacter?.stage) {
- 2 -> 0 // rookie
- 3 -> 1 // champion
- 4 -> 2 // ultimate
- 5 -> 3 // mega
+ 0 -> 0 // rookie
+ 1 -> 1 // champion
+ 2 -> 2 // ultimate
+ 3 -> 3 // mega
else -> 0
}
- // selectedOpponent.stage comes from the API and is already 0-3
val opponentStage = when (selectedOpponent?.stage) {
0 -> 0 // rookie
1 -> 1 // champion
@@ -2496,16 +2402,11 @@ fun BattlesScreen() {
RetrofitHelper().getPVPWinner(
context,
1,
- userId ?: run {
- Toast.makeText(context, "Session missing user id. Please re-authenticate.", Toast.LENGTH_SHORT).show()
- isWinnerLoaded = true
- winnerName = "Unknown"
- return@LaunchedEffect
- },
- activeCharacter?.charaId ?: "dim011_mon01",
- playerStage,
- 0,
- selectedOpponent?.charaId ?: "dim011_mon01",
+ userId ?: 2L,
+ activeCharacter?.name ?: "Player",
+ playerStage,
+ opponentStage,
+ selectedOpponent?.name ?: "Opponent",
opponentStage
) { apiResult ->
// Winner might be empty in first call, but we can check HP values
@@ -2514,7 +2415,7 @@ fun BattlesScreen() {
// Also check winner field if it's not empty
val playerWonFromWinner = activeCardId?.let { cardId ->
- val winner = apiResult.winner
+ val winner = apiResult.winner ?: ""
if (winner.isNotEmpty()) {
if (winner.contains("|")) {
// Pipe-separated format: first part is the winner
@@ -2541,21 +2442,18 @@ fun BattlesScreen() {
println("BATTLESCREEN: Battle result (first call) - winner: '${apiResult.winner}', playerHP: ${apiResult.playerHP}, opponentHP: ${apiResult.opponentHP}, playerWonFromHP: $playerWonFromHP, playerWonFromWinner: $playerWonFromWinner, final playerWon: $playerWon")
// Store winner name for display (will be updated in cleanup call if available)
- winnerName = if (apiResult.winner.isNotEmpty()) apiResult.winner else if (playerWon) "You" else "Opponent"
+ winnerName = apiResult.winner ?: if (playerWon) "You" else "Opponent"
isWinnerLoaded = true
// Then send the cleanup call - this will have the actual winner name
RetrofitHelper().getPVPWinner(
context,
2,
- userId ?: run {
- Toast.makeText(context, "Session missing user id. Please re-authenticate.", Toast.LENGTH_SHORT).show()
- return@getPVPWinner
- },
- activeCharacter?.charaId ?: "dim011_mon01",
+ userId ?: 2L,
+ activeCharacter?.name ?: "Player",
playerStage,
- 0,
- selectedOpponent?.charaId ?: "dim011_mon01",
+ opponentStage,
+ selectedOpponent?.name ?: "Opponent",
opponentStage
) { cleanupResult ->
// Update winner name from cleanup call if available
@@ -2567,7 +2465,7 @@ fun BattlesScreen() {
// Primary method: Check HP values (opponentHP <= 0 means opponent lost = player won)
// Secondary method: Check winner name (if winner doesn't match opponent, player won)
val opponentName = selectedOpponent?.name ?: ""
- val winner = cleanupResult.winner
+ val winner = cleanupResult.winner ?: ""
// Primary: HP-based determination (most reliable)
// If opponentHP <= 0, opponent is dead = player won
@@ -2704,13 +2602,8 @@ fun BattlesScreen() {
RetrofitHelper().getPVPWinner(
context,
0,
- userId ?: run {
- Toast.makeText(context, "Session missing user id. Please re-authenticate.", Toast.LENGTH_SHORT).show()
- showResumeDialog = false
- existingMatchState = null
- return@let
- },
- cardId,
+ userId ?: 2L,
+ cardId,
apiStage,
0,
clickedOpponent.charaId,
@@ -2834,13 +2727,8 @@ fun BattlesScreen() {
RetrofitHelper().getPVPWinner(
context,
0,
- userId ?: run {
- Toast.makeText(context, "Session missing user id. Please re-authenticate.", Toast.LENGTH_SHORT).show()
- showResumeDialog = false
- existingMatchState = null
- return@let
- },
- cardId,
+ userId ?: 2L,
+ cardId,
apiStage,
0,
opponent.charaId,
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 a970896..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
@@ -33,12 +33,16 @@ import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems
+import com.github.nacabaro.vbhelper.screens.homeScreens.screens.BEBEmHomeScreen
+import com.github.nacabaro.vbhelper.screens.homeScreens.screens.BEDiMHomeScreen
import com.github.nacabaro.vbhelper.screens.homeScreens.screens.VBDiMHomeScreen
import com.github.nacabaro.vbhelper.screens.itemsScreen.ObtainedItemDialog
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
@@ -72,8 +76,9 @@ fun HomeScreen(
?: flowOf(emptyList())
).collectAsState(initial = emptyList())
- val specialMissions by (
+ val vbSpecialMissions by (
activeMon
+ ?.takeIf { it.characterType == DeviceType.VBDevice }
?.let { chara ->
storageRepository.getSpecialMissions(chara.id)
}
@@ -141,20 +146,29 @@ fun HomeScreen(
height = cardIconData!!.cardIconHeight
)
- val activeCharacter = activeMon!!
- val displaySpecialMissions = specialMissions
-
- if (activeCharacter.characterType == DeviceType.BEDevice || vbData != null) {
- VBDiMHomeScreen(
- activeMon = activeCharacter,
- vbData = vbData ?: VBCharacterData(
- id = activeCharacter.id,
- generation = 0,
- totalTrophies = activeCharacter.trophies,
- ),
+ if (activeMon!!.isBemCard && beData != null) {
+ BEBEmHomeScreen(
+ activeMon = activeMon!!,
+ beData = beData!!,
transformationHistory = transformationHistory,
contentPadding = PaddingValues(0.dp),
- specialMissions = displaySpecialMissions,
+ 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
@@ -164,6 +178,27 @@ fun HomeScreen(
)
}
+ 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()
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Text(text = "Send to VitalWear")
+ }
}
}
}
@@ -213,5 +248,3 @@ fun HomeScreen(
}
}
}
-
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemElement.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemElement.kt
index 1aafa84..e08effa 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemElement.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemElement.kt
@@ -8,17 +8,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.dtos.ItemDtos
-import com.github.nacabaro.vbhelper.domain.items.ItemType
import com.github.nacabaro.vbhelper.R
@Composable
@@ -33,12 +28,6 @@ fun ItemElement(
.aspectRatio(1f)
) {
Box(modifier = Modifier.fillMaxSize()) {
- ItemCompatibilityBadge(
- itemType = item.itemType,
- modifier = Modifier
- .align(Alignment.TopEnd)
- .padding(8.dp)
- )
Icon(
painter = painterResource(id = getIconResource(item.itemIcon)),
contentDescription = null,
@@ -59,48 +48,3 @@ fun ItemElement(
}
}
}
-
-@Composable
-private fun ItemCompatibilityBadge(
- itemType: ItemType,
- modifier: Modifier = Modifier,
-) {
- val (label, containerColor, contentColor) = when (itemType) {
- ItemType.VBITEM -> Triple(
- "VB",
- MaterialTheme.colorScheme.primaryContainer,
- MaterialTheme.colorScheme.onPrimaryContainer,
- )
- ItemType.BEITEM -> Triple(
- "BE",
- MaterialTheme.colorScheme.tertiaryContainer,
- MaterialTheme.colorScheme.onTertiaryContainer,
- )
- ItemType.UNIVERSAL -> Triple(
- "VB+BE",
- MaterialTheme.colorScheme.secondaryContainer,
- MaterialTheme.colorScheme.onSecondaryContainer,
- )
- ItemType.SPECIALMISSION -> Triple(
- "MISSION",
- MaterialTheme.colorScheme.errorContainer,
- MaterialTheme.colorScheme.onErrorContainer,
- )
- }
-
- Surface(
- modifier = modifier,
- shape = RoundedCornerShape(12.dp),
- color = containerColor,
- tonalElevation = 2.dp,
- shadowElevation = 1.dp,
- ) {
- Text(
- text = label,
- color = contentColor,
- fontSize = 10.sp,
- modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
- )
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemsScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemsScreen.kt
index c233134..63ca4c5 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemsScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemsScreen.kt
@@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
-import androidx.compose.material3.PrimaryTabRow
+import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -34,7 +34,7 @@ fun ItemsScreen(
topBar = {
Column {
TopBanner(text = stringResource(R.string.items_title))
- PrimaryTabRow(
+ TabRow(
selectedTabIndex = selectedTabItem,
modifier = Modifier
) {
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemsScreenControllerImpl.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemsScreenControllerImpl.kt
index 45847f3..eb3f477 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemsScreenControllerImpl.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/itemsScreen/ItemsScreenControllerImpl.kt
@@ -7,6 +7,7 @@ import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
+import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlinx.coroutines.Dispatchers
@@ -50,12 +51,19 @@ class ItemsScreenControllerImpl (
val item = getItem(itemId)
val characterData = database.userCharacterDao().getCharacter(characterId)
var beCharacterData: BECharacterData? = null
+ var vbCharacterData: VBCharacterData? = null
if (characterData.characterType == DeviceType.BEDevice) {
beCharacterData = database
.userCharacterDao()
.getBeData(characterId)
.firstOrNull()
+
+ } else if (characterData.characterType == DeviceType.VBDevice) {
+ vbCharacterData = database
+ .userCharacterDao()
+ .getVbData(characterId)
+ .firstOrNull()
}
if (
@@ -114,7 +122,8 @@ class ItemsScreenControllerImpl (
.updateCharacter(characterData)
} else if (item.itemIcon in ItemTypes.Step8k.id .. ItemTypes.Win4.id &&
- (characterData.characterType == DeviceType.VBDevice || characterData.characterType == DeviceType.BEDevice)
+ characterData.characterType == DeviceType.VBDevice &&
+ vbCharacterData != null
) {
applySpecialMission(item.itemIcon, item.itemLength, characterId)
}
@@ -171,12 +180,12 @@ class ItemsScreenControllerImpl (
.getSpecialMissions(characterId)
.first()
- val existingMission = availableSpecialMissions.firstOrNull { it.watchId == specialMissionSlot }
- val newSpecialMission = SpecialMissions(
- id = existingMission?.id ?: 0,
- characterId = characterId,
+ var newSpecialMission = availableSpecialMissions[specialMissionSlot]
+ newSpecialMission = SpecialMissions(
+ id = newSpecialMission.id,
+ characterId = newSpecialMission.characterId,
goal = specialMissionGoal,
- watchId = specialMissionSlot,
+ watchId = newSpecialMission.watchId,
progress = 0,
status = SpecialMission.Status.AVAILABLE,
timeElapsedInMinutes = 0,
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 03de44f..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
@@ -43,7 +43,7 @@ fun ChooseConnectOption(
.padding(contentPadding)
) {
Text(
- text = "VitalWear/VBH",
+ text = "Bandai Toys = Vital Bracelet, Vital Hero, BE Bracelet",
fontSize = 14.sp,
modifier = Modifier.padding(bottom = 24.dp)
)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreen.kt
index 7ec6b52..72fc520 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreen.kt
@@ -24,7 +24,6 @@ import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import com.github.nacabaro.vbhelper.R
@@ -132,8 +131,6 @@ fun ScanScreenPreview() {
navController = rememberNavController(),
scanScreenController = object: ScanScreenController {
override val secretsFlow = MutableStateFlow(Secrets.getDefaultInstance())
- override val detectedTransportFlow: StateFlow = MutableStateFlow(DetectedTransport.UNKNOWN)
- override val transferStatusFlow: StateFlow = MutableStateFlow(null)
override fun unregisterActivityLifecycleListener(key: String) { }
override fun registerActivityLifecycleListener(
key: String,
@@ -144,12 +141,7 @@ fun ScanScreenPreview() {
override fun flushCharacter(cardId: Long) {}
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit, onMultipleCards: (List) -> Unit) {}
override fun onClickCheckCard(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit) {}
- override fun onClickWrite(
- secrets: Secrets,
- nfcCharacter: NfcCharacter,
- characterId: Long?,
- onComplete: (ScanScreenController.WriteResult) -> Unit
- ) {}
+ override fun onClickWrite(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit) {}
override fun cancelRead() {}
override fun characterFromNfc(nfcCharacter: NfcCharacter, onMultipleCards: (List, NfcCharacter) -> Unit): String { return "" }
override suspend fun characterToNfc(characterId: Long): NfcCharacter? { return null }
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt
index 23d97e1..485fab8 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt
@@ -5,27 +5,12 @@ import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
interface ScanScreenController {
val secretsFlow: Flow
- val detectedTransportFlow: StateFlow
- val transferStatusFlow: StateFlow
-
- enum class WriteResult {
- MOVE_CONFIRMED,
- COPIED,
- BLOCKED_DEVICE_FULL,
- }
-
fun onClickRead(secrets: Secrets, onComplete: ()->Unit, onMultipleCards: (List) -> Unit)
fun onClickCheckCard(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit)
- fun onClickWrite(
- secrets: Secrets,
- nfcCharacter: NfcCharacter,
- characterId: Long? = null,
- onComplete: (WriteResult) -> Unit
- )
+ fun onClickWrite(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit)
fun cancelRead()
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt
index 5f1de65..eb52382 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt
@@ -3,7 +3,6 @@ package com.github.nacabaro.vbhelper.screens.scanScreen
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
-import android.nfc.tech.IsoDep
import android.nfc.tech.NfcA
import android.os.Bundle
import android.provider.Settings
@@ -12,34 +11,21 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vbnfc.TagCommunicator
-import com.github.cfogrady.vitalwear.protos.Character
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.NfcCharacter
-import com.github.cfogrady.vbnfc.data.DeviceType as NfcDeviceTypeId
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.domain.card.Card
-import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.screens.scanScreen.converters.FromNfcConverter
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.source.getCryptographicTransformerMap
import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
-import com.github.nacabaro.vbhelper.transfer.hce.VitalWearHceReaderClient
-import com.github.nacabaro.vbhelper.utils.DeviceType
-import com.github.nacabaro.vbhelper.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import java.util.Arrays
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicLong
+import com.github.nacabaro.vbhelper.R
class ScanScreenControllerImpl(
override val secretsFlow: Flow,
@@ -47,316 +33,49 @@ class ScanScreenControllerImpl(
private val registerActivityLifecycleListener: (String, ActivityLifecycleListener)->Unit,
private val unregisterActivityLifecycleListener: (String)->Unit,
): ScanScreenController {
-
- override val detectedTransportFlow: StateFlow
- get() = _detectedTransportFlow
- override val transferStatusFlow: StateFlow
- get() = _transferStatusFlow
-
- private val _detectedTransportFlow = MutableStateFlow(DetectedTransport.UNKNOWN)
- private val _transferStatusFlow = MutableStateFlow(null)
private var lastScannedCharacter: NfcCharacter? = null
- private var lastRequestedCharacterId: Long? = null
- private var lastWriteCharacterId: Long? = null
private val nfcAdapter: NfcAdapter
- private val isHandlingTag = AtomicBoolean(false)
- private var lastTagId: ByteArray? = null
- private var lastTagHandledAtMs: Long = 0L
- private val tagStateLock = Any()
- private val readerSessionCounter = AtomicLong(0L)
- @Volatile private var activeReaderSessionId: Long = 0L
-
- companion object {
- private const val TAG_DEBOUNCE_MS = 1500L
- private const val TAG_IGNORE_AFTER_HANDLED_MS = 2000
- private val VITALWEAR_AID = byteArrayOf(
- 0xF0.toByte(), 0x56, 0x49, 0x54, 0x41, 0x4C, 0x57, 0x45, 0x41, 0x52
- )
- private const val SW_OK = 0x9000
- // Lifecycle key for the always-on NFC suppressor that prevents Android from launching
- // com.android.apps.tag/.TagViewer whenever the watch's HCE comes into range.
- private const val LIFECYCLE_KEY_NFC_SUPPRESSOR = "nfc_suppressor"
- }
init {
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(componentActivity)
if (maybeNfcAdapter == null) {
- Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_no_nfc_on_device), Toast.LENGTH_SHORT).show()
+ Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_no_nfc_on_device), Toast.LENGTH_SHORT).show()
}
nfcAdapter = maybeNfcAdapter
checkSecrets()
- registerNfcSuppressor()
}
- /**
- * Registers a lifecycle listener that keeps reader mode active (with a no-op callback)
- * whenever this Activity is in the foreground. This prevents Android's default NFC dispatch
- * system from launching com.android.apps.tag/.TagViewer when the watch's HCE service is
- * detected while the user hasn't explicitly pressed a transfer button yet.
- *
- * When the user presses Read/Write/CheckCard, [handleTag] replaces this suppressor with
- * the real transfer callback via [NfcAdapter.enableReaderMode]. After the transfer
- * completes, [enableNfcSuppressor] is called again to restore the passive suppressor.
- */
- private fun registerNfcSuppressor() {
- registerActivityLifecycleListener(
- LIFECYCLE_KEY_NFC_SUPPRESSOR,
- object : ActivityLifecycleListener {
- override fun onResume() {
- enableNfcSuppressor()
+ override fun onClickRead(secrets: Secrets, onComplete: ()->Unit, onMultipleCards: (List) -> Unit) {
+ handleTag(secrets) { tagCommunicator ->
+ try {
+ val character = tagCommunicator.receiveCharacter()
+ val resultMessage = characterFromNfc(character) { cards, nfcCharacter ->
+ lastScannedCharacter = nfcCharacter
+ onMultipleCards(cards)
}
- override fun onPause() {
- disableReaderModeSafely()
+ onComplete.invoke()
+ resultMessage
+ } catch (e: Exception) {
+ Log.e("NFC_READ", "Error reading character from NFC", e)
+ componentActivity.runOnUiThread {
+ Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic) + ": " + (e.message ?: e.javaClass.simpleName), Toast.LENGTH_LONG).show()
}
+ onComplete.invoke()
+ componentActivity.getString(R.string.scan_error_generic)
}
- )
- }
-
- /**
- * Enables reader mode with a silent no-op callback. Calling [NfcAdapter.enableReaderMode]
- * suppresses Android's default tag-dispatch system (and therefore the "new tag scanned"
- * TagViewer screen) for as long as this Activity is in the foreground. The actual transfer
- * logic is wired up separately via [handleTag] when the user presses a button.
- */
- private fun enableNfcSuppressor() {
- if (!nfcAdapter.isEnabled) return
- val options = Bundle()
- options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
- val flags = NfcAdapter.FLAG_READER_NFC_A or
- NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS or
- NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
- runCatching {
- nfcAdapter.enableReaderMode(
- componentActivity,
- { /* suppress default dispatch — user must tap a transfer button */ },
- flags,
- options
- )
- }.onFailure {
- Log.w("NFC", "Failed to enable NFC suppressor reader mode", it)
}
}
- // ---- Read (phone receives character FROM watch or bracelet) --------------------
-
- override fun onClickRead(secrets: Secrets, onComplete: () -> Unit, onMultipleCards: (List) -> Unit) {
- _detectedTransportFlow.value = DetectedTransport.UNKNOWN
- setTransferStatus(R.string.scan_transfer_waiting_tap)
- handleTag(
- secrets,
- nfcAHandler = { tagCommunicator ->
- try {
- val character = tagCommunicator.receiveCharacter()
- val resultMessage = characterFromNfc(character) { cards, nfcCharacter ->
- lastScannedCharacter = nfcCharacter
- onMultipleCards(cards)
- }
- onComplete.invoke()
- resultMessage
- } catch (e: Exception) {
- Log.e("NFC_READ", "Error reading character from NFC", e)
- componentActivity.runOnUiThread {
- Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic) + ": " + (e.message ?: e.javaClass.simpleName), Toast.LENGTH_LONG).show()
- }
- onComplete.invoke()
- componentActivity.getString(R.string.scan_error_generic)
- }
- },
- isoDepHandler = { isoDep ->
- val application = componentActivity.applicationContext as VBHelper
- val importer = VitalWearCharacterImporter(application.container.db, application.container.transferSeenDao)
- try {
- // ISO-DEP/HCE route: VitalWear characters are always imported as BE device type.
- var importResult: VitalWearCharacterImporter.ImportResult? = null
- val moved = VitalWearHceReaderClient(isoDep).moveCharacterFromWatch { character ->
- // Force imported device type to BE since source is VitalWear HCE (BE only).
- val result = importer.importCharacter(character, forcedDeviceType = DeviceType.BEDevice)
- importResult = result
- result.success
- }
- onComplete.invoke()
- if (moved) {
- importResult?.message ?: componentActivity.getString(R.string.scan_sent_character_success)
- } else {
- importResult?.message
- ?: "VitalWear import was rejected. Source character remains on the watch."
- }
- } catch (readError: Exception) {
- Log.e("NFC_READ", "HCE read failed; watch may be armed as destination", readError)
- onComplete.invoke()
- "No source character detected on watch. If the watch is waiting to receive, use VBH to Watch."
- }
- }
- )
- }
-
- // ---- Write (phone sends character TO watch or bracelet) ------------------------
-
- override fun onClickWrite(
- secrets: Secrets,
- nfcCharacter: NfcCharacter,
- characterId: Long?,
- onComplete: (ScanScreenController.WriteResult) -> Unit
- ) {
- _detectedTransportFlow.value = DetectedTransport.UNKNOWN
- setTransferStatus(R.string.scan_transfer_waiting_tap)
- handleTag(
- secrets,
- nfcAHandler = { tagCommunicator ->
- try {
- val targetHeader = runCatching { tagCommunicator.readTargetHeader() }.getOrNull()
- val normalizedDeviceType = when (targetHeader?.deviceTypeId) {
- 512u.toUShort() -> NfcDeviceTypeId.VitalSeriesDeviceType
- 768u.toUShort() -> NfcDeviceTypeId.VitalCharactersDeviceType
- 1024u.toUShort() -> NfcDeviceTypeId.VitalBraceletBEDeviceType
- else -> targetHeader?.deviceTypeId
- }
- val forcedProfile = when (normalizedDeviceType) {
- NfcDeviceTypeId.VitalBraceletBEDeviceType -> DeviceType.BEDevice
- else -> DeviceType.VBDevice
- }
- Log.i(
- "NFC_WRITE_A",
- "Target NFC-A header rawDeviceType=${targetHeader?.deviceTypeId}, normalizedDeviceType=$normalizedDeviceType, dimId=${targetHeader?.getDimId()}, forcedProfile=$forcedProfile"
- )
-
- // NFC-A route: choose export profile by target bracelet type.
- // - Vital Series / Vital Characters => VB profile
- // - Vital Bracelet BE => BE profile
- val nfcACharacter = if (characterId != null) {
- runBlocking {
- ToNfcConverter(componentActivity = componentActivity)
- .characterToNfc(characterId, forcedProfile)
- }
- } else {
- nfcCharacter
- }
-
- val initialSlotState = readNfcASlotState(tagCommunicator)
- if (initialSlotState.isFull()) {
- onComplete.invoke(ScanScreenController.WriteResult.BLOCKED_DEVICE_FULL)
- return@handleTag componentActivity.getString(R.string.scan_target_device_full)
- }
-
- val migrationCheck = verifyActiveToBackupMigration(tagCommunicator, initialSlotState)
- if (!migrationCheck.canProceed) {
- onComplete.invoke(ScanScreenController.WriteResult.COPIED)
- return@handleTag migrationCheck.message
- }
-
- // Send with VB profile (which was forced above). Real bracelets only accept VBNfcCharacter.
- return@handleTag try {
- when (nfcACharacter) {
- is VBNfcCharacter -> tagCommunicator.sendCharacter(nfcACharacter)
- is BENfcCharacter -> {
- // This should not happen since we forced VBDevice above, but fail if it does.
- Log.e("NFC_WRITE_A", "BENfcCharacter sent to NFC-A (real bracelet) — protocol error")
- throw IllegalStateException("NFC-A forced VBDevice but received BENfcCharacter")
- }
- }
- onComplete.invoke(ScanScreenController.WriteResult.MOVE_CONFIRMED)
- componentActivity.getString(R.string.scan_sent_character_success)
- } catch (deviceTypeMismatch: Exception) {
- // Device type mismatch on NFC-A is terminal for this tap.
- if (deviceTypeMismatch.message?.contains("Character doesn't match device type") == true) {
- Log.e("NFC_WRITE_A", "Device type mismatch on NFC-A with selected transfer profile", deviceTypeMismatch)
- onComplete.invoke(ScanScreenController.WriteResult.COPIED)
- "Transfer failed: bracelet rejected character profile for this tap. Retry and keep bracelet steady."
- } else {
- Log.e("NFC_WRITE_A", "NFC-A write failed", deviceTypeMismatch)
- onComplete.invoke(ScanScreenController.WriteResult.COPIED)
- throw deviceTypeMismatch
- }
- }
- } catch (writeError: Throwable) {
- Log.e("NFC_WRITE_A", "NFC-A setup failed", writeError)
- onComplete.invoke(ScanScreenController.WriteResult.COPIED)
- throw writeError
- }
- },
- isoDepHandler = { isoDep ->
- val characterId = lastRequestedCharacterId
- ?: throw IllegalStateException("No character id available for VitalWear HCE write")
- val application = componentActivity.applicationContext as VBHelper
- val hceClient = VitalWearHceReaderClient(isoDep)
- try {
- // ISO-DEP/HCE route: VitalWear always uses BE profile (enforced).
- // Create protobuf with forced BE device type for correct serialization.
- val proto = runBlocking {
- VitalWearCharacterExporter(application.container.db)
- .buildCharacterProto(characterId, forcedTransferProfile = DeviceType.BEDevice)
- }
- if (proto.characterStats.deviceType != Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_BE) {
- throw IllegalStateException("VitalWear HCE export must use BE transfer profile")
- }
- // Send with status confirmation: only mark MOVE_CONFIRMED after watch confirms import.
- val confirmedMove = hceClient.sendCharacterToWatchAndConfirm(proto)
- if (confirmedMove) {
- onComplete.invoke(ScanScreenController.WriteResult.MOVE_CONFIRMED)
- componentActivity.getString(R.string.scan_sent_character_success)
- } else {
- // Transfer acknowledging but watch import failed or did not confirm.
- onComplete.invoke(ScanScreenController.WriteResult.COPIED)
- "Transfer sent but watch did not confirm import. Source was kept in VBH."
- }
- } catch (writeError: Throwable) {
- Log.e("NFC_WRITE_HCE", "HCE write failed", writeError)
- onComplete.invoke(ScanScreenController.WriteResult.COPIED)
- throw writeError
- }
- }
- )
- }
-
-
-
- // ---- Check card (NFC-A only; ISO-DEP watches don't need a DIM prep) -----------
-
- override fun onClickCheckCard(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit) {
- _detectedTransportFlow.value = DetectedTransport.UNKNOWN
- setTransferStatus(R.string.scan_transfer_waiting_tap)
- handleTag(
- secrets,
- nfcAHandler = { tagCommunicator ->
- tagCommunicator.prepareDIMForCharacter(nfcCharacter.dimId)
- onComplete.invoke()
- componentActivity.getString(R.string.scan_sent_dim_success)
- },
- // HCE equivalent of the prep step: verify watch is armed for phone->watch transfer.
- isoDepHandler = { isoDep ->
- runCatching {
- VitalWearHceReaderClient(isoDep).verifyWatchReadyToReceive()
- }.fold(
- onSuccess = {
- onComplete.invoke()
- "Watch ready. Tap again to send character."
- },
- onFailure = { error ->
- Log.e("NFC_HCE", "Watch is not ready to receive character", error)
- "Watch is not ready. On watch: Transfer > Receive from VBH, then tap again."
- }
- )
- }
- )
- }
-
- // ---- Cancel / lifecycle -------------------------------------------------------
-
override fun cancelRead() {
- _detectedTransportFlow.value = DetectedTransport.UNKNOWN
- setTransferStatus(R.string.scan_transfer_cancelled)
- activeReaderSessionId = readerSessionCounter.incrementAndGet()
- isHandlingTag.set(false)
- synchronized(tagStateLock) {
- lastTagId = null
- lastTagHandledAtMs = 0L
+ if(nfcAdapter.isEnabled) {
+ nfcAdapter.disableReaderMode(componentActivity)
}
- // Re-arm the suppressor so the TagViewer doesn't appear while the user is still
- // on the scan screen but hasn't pressed a transfer button yet.
- enableNfcSuppressor()
}
- override fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
+ override fun registerActivityLifecycleListener(
+ key: String,
+ activityLifecycleListener: ActivityLifecycleListener
+ ) {
registerActivityLifecycleListener.invoke(key, activityLifecycleListener)
}
@@ -364,232 +83,43 @@ class ScanScreenControllerImpl(
unregisterActivityLifecycleListener.invoke(key)
}
- // ---- NFC adapter wiring -------------------------------------------------------
-
- /**
- * Arms the NFC reader for both NFC-A (bracelet) and ISO-DEP (VitalWear HCE).
- * [isoDepHandler] is optional; when null the reader only accepts NFC-A.
- */
- private fun handleTag(
- secrets: Secrets,
- nfcAHandler: (TagCommunicator) -> String,
- isoDepHandler: ((IsoDep) -> String)? = null
- ) {
+ // EXTRACTED DIRECTLY FROM EXAMPLE APP
+ private fun handleTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String) {
if (!nfcAdapter.isEnabled) {
showWirelessSettings()
- return
+ } else {
+ val options = Bundle()
+ // Work around for some broken Nfc firmware implementations that poll the card too fast
+ options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
+ nfcAdapter.enableReaderMode(componentActivity, buildOnReadTag(secrets, handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
+ options
+ )
}
- val options = Bundle()
- // Work around for some broken Nfc firmware implementations that poll the card too fast
- options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
- val flags = NfcAdapter.FLAG_READER_NFC_A or
- NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS or
- NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
- val sessionId = readerSessionCounter.incrementAndGet()
- activeReaderSessionId = sessionId
- isHandlingTag.set(false)
- synchronized(tagStateLock) {
- lastTagId = null
- lastTagHandledAtMs = 0L
- }
- disableReaderModeSafely()
- nfcAdapter.enableReaderMode(
- componentActivity,
- buildOnReadTag(secrets, nfcAHandler, isoDepHandler, sessionId),
- flags,
- options
- )
}
- private fun buildOnReadTag(
- secrets: Secrets,
- nfcAHandler: (TagCommunicator) -> String,
- isoDepHandler: ((IsoDep) -> String)?,
- sessionId: Long,
- ): (Tag) -> Unit {
- return { tag ->
- if (activeReaderSessionId == sessionId) {
- val now = System.currentTimeMillis()
- val shouldHandleTag = synchronized(tagStateLock) {
- val sameRecentTag = lastTagId != null && tag.id != null && Arrays.equals(lastTagId, tag.id) &&
- (now - lastTagHandledAtMs) < TAG_DEBOUNCE_MS
- if (sameRecentTag || !isHandlingTag.compareAndSet(false, true)) {
- false
- } else {
- lastTagId = tag.id?.clone()
- lastTagHandledAtMs = now
- true
- }
- }
- if (shouldHandleTag) {
- setTransferStatus(R.string.scan_transfer_detected_keep_tap)
-
- // Detect transport once per tap and lock to that route for this transfer.
- // Real Bandai bracelets route through NFC-A; VitalWear routes through ISO-DEP/HCE.
- val isoDep = IsoDep.get(tag)
- val hasNfcARoute = NfcA.get(tag) != null
- val hasIsoDepHandler = isoDep != null && isoDepHandler != null
- val confirmedVitalWear = if (hasIsoDepHandler) {
- runCatching { isVitalWearHceTarget(isoDep) }.getOrDefault(false)
- } else {
- false
- }
- try {
- if (hasIsoDepHandler && confirmedVitalWear) {
- val isoDepTarget = isoDep
- val isoDepAction = isoDepHandler
- try {
- _detectedTransportFlow.value = DetectedTransport.ISO_DEP
- isoDepTarget.connect()
- isoDepTarget.use {
- setTransferStatus(R.string.scan_transfer_in_progress)
- val successText = isoDepAction.invoke(isoDepTarget)
- componentActivity.runOnUiThread {
- Toast.makeText(componentActivity, successText, Toast.LENGTH_SHORT).show()
- setTransferStatus(R.string.scan_transfer_complete_remove)
- }
- }
- } catch (e: Throwable) {
- Log.e("NFC_HCE", "IsoDep transfer failed", e)
- componentActivity.runOnUiThread {
- Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic), Toast.LENGTH_SHORT).show()
- setTransferStatus(R.string.scan_transfer_failed_try_again)
- }
- }
- } else if (hasIsoDepHandler && !confirmedVitalWear && !hasNfcARoute) {
- // Hard-fail only when IsoDep is present without any NFC-A fallback.
- // This keeps HCE safety while allowing real bracelets to route via NFC-A.
- Log.w("NFC_ROUTE", "IsoDep present but VitalWear AID not confirmed and no NFC-A route; cancelling transfer")
- _detectedTransportFlow.value = DetectedTransport.UNKNOWN
- componentActivity.runOnUiThread {
- Toast.makeText(componentActivity, "VitalWear HCE not detected. Transfer cancelled.", Toast.LENGTH_SHORT).show()
- setTransferStatus(R.string.scan_transfer_failed_try_again)
- }
- } else if (hasNfcARoute) {
- if (hasIsoDepHandler && !confirmedVitalWear) {
- Log.i("NFC_ROUTE", "IsoDep present but VitalWear AID not confirmed; routing to NFC-A fallback")
- }
- if (!handleNfcATag(tag, secrets, nfcAHandler)) {
- componentActivity.runOnUiThread {
- Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_tag_not_vb), Toast.LENGTH_SHORT).show()
- setTransferStatus(R.string.scan_transfer_failed_try_again)
- }
- }
- } else {
- if (hasIsoDepHandler && !confirmedVitalWear) {
- Log.w("NFC_ROUTE", "IsoDep tag does not expose VitalWear HCE AID and has no NFC-A fallback")
- }
- _detectedTransportFlow.value = DetectedTransport.UNKNOWN
- componentActivity.runOnUiThread {
- Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_tag_not_vb), Toast.LENGTH_SHORT).show()
- setTransferStatus(R.string.scan_transfer_failed_try_again)
- }
- }
- } finally {
- finishHandledTagSession(tag, sessionId)
- }
+ // EXTRACTED DIRECTLY FROM EXAMPLE APP
+ private fun buildOnReadTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
+ return { tag->
+ val nfcData = NfcA.get(tag)
+ if (nfcData == null) {
+ componentActivity.runOnUiThread {
+ Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_tag_not_vb), Toast.LENGTH_SHORT).show()
}
}
- }
- }
-
- private fun finishHandledTagSession(tag: Tag, sessionId: Long) {
- if (activeReaderSessionId != sessionId) return
-
- // Keep Android from redispatching this same tag to other NFC apps while still in range.
- runCatching {
- nfcAdapter.ignore(
- tag,
- TAG_IGNORE_AFTER_HANDLED_MS,
- NfcAdapter.OnTagRemovedListener { },
- null
- )
- }.onFailure {
- Log.w("NFC", "Failed to ignore handled tag", it)
- }
-
- // Restore the passive suppressor so devices can remain close without Android
- // launching the TagViewer between transfers or while waiting for the next button press.
- enableNfcSuppressor()
- isHandlingTag.set(false)
- }
-
- private fun handleNfcATag(
- tag: Tag,
- secrets: Secrets,
- nfcAHandler: (TagCommunicator) -> String,
- ): Boolean {
- val nfcData = NfcA.get(tag) ?: return false
- _detectedTransportFlow.value = DetectedTransport.NFC_A
- return try {
nfcData.connect()
nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
- setTransferStatus(R.string.scan_transfer_in_progress)
- val successText = nfcAHandler(tagCommunicator)
+ val successText = handlerFunc(tagCommunicator)
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, successText, Toast.LENGTH_SHORT).show()
- setTransferStatus(R.string.scan_transfer_complete_remove)
}
}
- true
- } catch (e: Throwable) {
- val staleTagSession = e is SecurityException && (e.message?.contains("out of date", ignoreCase = true) == true)
- if (staleTagSession) {
- Log.w("NFC_A", "Ignoring stale NFC-A tag session; waiting for a fresh tap", e)
- componentActivity.runOnUiThread {
- Toast.makeText(componentActivity, "Tag session expired. Please tap again.", Toast.LENGTH_SHORT).show()
- setTransferStatus(R.string.scan_transfer_waiting_tap)
- }
- return false
- }
- Log.e("NFC_A", "NfcA transfer failed", e)
- componentActivity.runOnUiThread {
- Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic), Toast.LENGTH_SHORT).show()
- setTransferStatus(R.string.scan_transfer_failed_try_again)
- }
- true
}
}
- private fun setTransferStatus(messageRes: Int) {
- _transferStatusFlow.value = componentActivity.getString(messageRes)
- }
-
- private fun disableReaderModeSafely() {
- if (!nfcAdapter.isEnabled) return
- runCatching { nfcAdapter.disableReaderMode(componentActivity) }
- }
-
- private fun isVitalWearHceTarget(isoDep: IsoDep): Boolean {
- return runCatching {
- val originalTimeout = isoDep.timeout
- try {
- isoDep.timeout = 500
- if (!isoDep.isConnected) {
- isoDep.connect()
- }
- val selectApdu = byteArrayOf(0x00, 0xA4.toByte(), 0x04, 0x00, VITALWEAR_AID.size.toByte()) + VITALWEAR_AID
- val response = isoDep.transceive(selectApdu)
- statusWord(response) == SW_OK
- } finally {
- isoDep.timeout = originalTimeout
- runCatching { if (isoDep.isConnected) isoDep.close() }
- }
- }.getOrDefault(false)
- }
-
- private fun statusWord(response: ByteArray): Int {
- if (response.size < 2) return -1
- return ((response[response.size - 2].toInt() and 0xFF) shl 8) or
- (response[response.size - 1].toInt() and 0xFF)
- }
-
- // ---- Misc ---------------------------------------------------------------------
-
private fun checkSecrets() {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
- if (secretsFlow.stateIn(componentActivity.lifecycleScope).value.isMissingSecrets()) {
+ if(secretsFlow.stateIn(componentActivity.lifecycleScope).value.isMissingSecrets()) {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_missing_secrets), Toast.LENGTH_SHORT).show()
}
@@ -597,153 +127,74 @@ class ScanScreenControllerImpl(
}
}
+ override fun onClickWrite(
+ secrets: Secrets,
+ nfcCharacter: NfcCharacter,
+ onComplete: () -> Unit
+ ) {
+ handleTag(secrets) { tagCommunicator ->
+ try {
+ if (nfcCharacter is VBNfcCharacter) {
+ Log.d("SendCharacter", "VBNfcCharacter")
+ val castNfcCharacter: VBNfcCharacter = nfcCharacter
+ tagCommunicator.sendCharacter(castNfcCharacter)
+ } else if (nfcCharacter is BENfcCharacter) {
+ Log.d("SendCharacter", "BENfcCharacter")
+ val castNfcCharacter: BENfcCharacter = nfcCharacter
+ tagCommunicator.sendCharacter(castNfcCharacter)
+ }
+ onComplete.invoke()
+ componentActivity.getString(R.string.scan_sent_character_success)
+ } catch (e: Throwable) {
+ Log.e("TAG", e.stackTraceToString())
+ componentActivity.getString(R.string.scan_error_generic)
+ }
+ }
+ }
+
+ override fun onClickCheckCard(
+ secrets: Secrets,
+ nfcCharacter: NfcCharacter,
+ onComplete: () -> Unit
+ ) {
+ handleTag(secrets) { tagCommunicator ->
+ tagCommunicator.prepareDIMForCharacter(nfcCharacter.dimId)
+ onComplete.invoke()
+ componentActivity.getString(R.string.scan_sent_dim_success)
+ }
+ }
+
+ // EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun showWirelessSettings() {
- Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_nfc_must_be_enabled), Toast.LENGTH_SHORT).show()
+ Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_nfc_must_be_enabled), Toast.LENGTH_SHORT).show()
componentActivity.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
- /**
- * Best-effort, non-destructive slot-capacity check for NFC-A devices.
- */
- private fun readNfcASlotState(tagCommunicator: TagCommunicator): NfcASlotState {
- val communicatorClass = tagCommunicator.javaClass
-
- val countMethod = communicatorClass.methods.firstOrNull {
- it.parameterCount == 0 &&
- Number::class.java.isAssignableFrom(it.returnType) &&
- it.name.contains("count", ignoreCase = true) &&
- it.name.contains("character", ignoreCase = true)
- }
- val count = if (countMethod != null) {
- runCatching { (countMethod.invoke(tagCommunicator) as Number).toInt() }.getOrNull()
- } else null
-
- val activeMethod = communicatorClass.methods.firstOrNull {
- it.parameterCount == 0 && it.returnType == java.lang.Boolean.TYPE &&
- (it.name.contains("active", ignoreCase = true) || it.name.contains("current", ignoreCase = true)) &&
- it.name.contains("character", ignoreCase = true)
- }
- val backupMethod = communicatorClass.methods.firstOrNull {
- it.parameterCount == 0 && it.returnType == java.lang.Boolean.TYPE &&
- it.name.contains("backup", ignoreCase = true) &&
- it.name.contains("character", ignoreCase = true)
- }
-
- val activePresent = if (activeMethod != null) {
- runCatching { activeMethod.invoke(tagCommunicator) as Boolean }.getOrNull()
- } else null
- val backupPresent = if (backupMethod != null) {
- runCatching { backupMethod.invoke(tagCommunicator) as Boolean }.getOrNull()
- } else null
-
- if (count == null && activePresent == null && backupPresent == null) {
- Log.w("NFC_A", "Unable to introspect NFC-A slot occupancy; defaulting to non-blocking write path")
- }
-
- return NfcASlotState(count = count, activePresent = activePresent, backupPresent = backupPresent)
- }
-
- /**
- * Attempts to mirror official toy behavior by moving the active character to backup.
- * If the library already does this internally, this is a harmless no-op.
- */
- private fun verifyActiveToBackupMigration(
- tagCommunicator: TagCommunicator,
- beforeState: NfcASlotState,
- ): SlotMigrationCheck {
- val migrationResult = moveActiveToBackupIfSupported(tagCommunicator)
- if (migrationResult.attempted && !migrationResult.success) {
- return SlotMigrationCheck(
- canProceed = false,
- message = "Transfer blocked. Could not move active character to backup safely."
- )
- }
-
- // If we could not introspect occupancy at all, keep legacy behavior and proceed.
- if (!beforeState.hasOccupancySignal()) {
- return SlotMigrationCheck(canProceed = true, message = "")
- }
-
- val afterState = readNfcASlotState(tagCommunicator)
- val migrationVerified = afterState.backupPresent == true ||
- (beforeState.count != null && afterState.count != null && afterState.count >= beforeState.count)
- if (!migrationVerified) {
- // Some real bracelets do not expose stable occupancy signals through the current
- // reflection-based probing. In that case, prefer compatibility over false blocks.
- Log.w("NFC_A", "Could not verify active->backup migration from slot signals; proceeding with write")
- return SlotMigrationCheck(canProceed = true, message = "")
- }
-
- return SlotMigrationCheck(canProceed = true, message = "")
- }
-
- private fun moveActiveToBackupIfSupported(tagCommunicator: TagCommunicator): MigrationInvokeResult {
- val communicatorClass = tagCommunicator.javaClass
- val candidate = communicatorClass.methods.firstOrNull {
- it.parameterCount == 0 &&
- (
- (it.name.contains("move", ignoreCase = true) && it.name.contains("backup", ignoreCase = true)) ||
- (it.name.contains("shift", ignoreCase = true) && it.name.contains("backup", ignoreCase = true)) ||
- (it.name.contains("promote", ignoreCase = true) && it.name.contains("backup", ignoreCase = true))
- )
- }
- if (candidate == null) {
- return MigrationInvokeResult(attempted = false, success = true)
- }
-
- return runCatching { candidate.invoke(tagCommunicator) }
- .fold(
- onSuccess = { MigrationInvokeResult(attempted = true, success = true) },
- onFailure = {
- Log.w("NFC_A", "Failed to move active character to backup", it)
- MigrationInvokeResult(attempted = true, success = false)
- }
- )
- }
-
- private data class NfcASlotState(
- val count: Int?,
- val activePresent: Boolean?,
- val backupPresent: Boolean?,
- ) {
- fun hasOccupancySignal(): Boolean {
- return count != null || activePresent != null || backupPresent != null
- }
-
- fun isFull(): Boolean {
- if (count != null) {
- return count >= 2
- }
- if (activePresent != null && backupPresent != null) {
- return activePresent && backupPresent
- }
- return false
- }
- }
-
- private data class SlotMigrationCheck(
- val canProceed: Boolean,
- val message: String,
- )
-
- private data class MigrationInvokeResult(
- val attempted: Boolean,
- val success: Boolean,
- )
-
- override fun characterFromNfc(nfcCharacter: NfcCharacter, onMultipleCards: (List, NfcCharacter) -> Unit): String {
- return FromNfcConverter(componentActivity = componentActivity).addCharacter(nfcCharacter, onMultipleCards)
+ override fun characterFromNfc(
+ nfcCharacter: NfcCharacter,
+ onMultipleCards: (List, NfcCharacter) -> Unit
+ ): String {
+ val nfcConverter = FromNfcConverter(
+ componentActivity = componentActivity
+ )
+ return nfcConverter.addCharacter(nfcCharacter, onMultipleCards)
}
override suspend fun characterToNfc(characterId: Long): NfcCharacter {
- lastRequestedCharacterId = characterId
- val character = ToNfcConverter(componentActivity = componentActivity).characterToNfc(characterId)
+ val nfcGenerator = ToNfcConverter(
+ componentActivity = componentActivity
+ )
+
+ val character = nfcGenerator.characterToNfc(characterId)
Log.d("CharacterType", character.toString())
return character
}
override fun flushCharacter(cardId: Long) {
- val nfcConverter = FromNfcConverter(componentActivity = componentActivity)
+ val nfcConverter = FromNfcConverter(
+ componentActivity = componentActivity
+ )
+
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
if (lastScannedCharacter != null) {
nfcConverter.addCharacterUsingCard(lastScannedCharacter!!, cardId)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/FromNfcConverter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/FromNfcConverter.kt
index d23f6be..dcc85a3 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/FromNfcConverter.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/FromNfcConverter.kt
@@ -3,7 +3,6 @@ package com.github.nacabaro.vbhelper.screens.scanScreen.converters
import androidx.activity.ComponentActivity
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.NfcCharacter
-import com.github.cfogrady.vbnfc.vb.SpecialMission
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.card.Card
@@ -20,8 +19,7 @@ class FromNfcConverter (
) {
private val application = componentActivity.applicationContext as VBHelper
private val database = application.container.db
- private val transferSeenDao = application.container.transferSeenDao
-
+
fun addCharacterUsingCard(
nfcCharacter: NfcCharacter,
@@ -120,16 +118,11 @@ class FromNfcConverter (
.userCharacterDao()
.insertCharacterData(characterData)
- val seenTimestamp = System.currentTimeMillis()
- markSeen(cardData, nfcCharacter.charIndex.toInt(), seenTimestamp)
-
if (nfcCharacter is BENfcCharacter) {
addBeCharacterToDatabase(
characterId = characterId,
nfcCharacter = nfcCharacter
)
- // Keep a VB profile alongside BE stats for later VB-target exports.
- addVbCharacterProfileFromBe(characterId, nfcCharacter)
} else if (nfcCharacter is VBNfcCharacter) {
addVbCharacterToDatabase(
characterId = characterId,
@@ -236,8 +229,8 @@ class FromNfcConverter (
itemType = nfcCharacter.itemType.toInt(),
itemMultiplier = nfcCharacter.itemMultiplier.toInt(),
itemRemainingTime = nfcCharacter.itemRemainingTime.toInt(),
- otp0 = nfcCharacter.getOtp0().joinToString("") { "%02x".format(it) },
- otp1 = nfcCharacter.getOtp1().joinToString("") { "%02x".format(it) },
+ otp0 = "", //nfcCharacter.value!!.otp0.toString(),
+ otp1 = "", //nfcCharacter.value!!.otp1.toString(),
minorVersion = nfcCharacter.characterCreationFirmwareVersion.minorVersion.toInt(),
majorVersion = nfcCharacter.characterCreationFirmwareVersion.majorVersion.toInt(),
)
@@ -247,42 +240,6 @@ class FromNfcConverter (
.insertBECharacterData(extraCharacterData)
}
- private fun addVbCharacterProfileFromBe(
- characterId: Long,
- nfcCharacter: BENfcCharacter,
- ) {
- val historyCount = nfcCharacter.transformationHistory.count { it.toCharIndex.toInt() != 255 }
- val vbData = VBCharacterData(
- id = characterId,
- generation = (historyCount - 1).coerceAtLeast(0),
- totalTrophies = nfcCharacter.trophies.toInt(),
- )
- database.userCharacterDao().insertVBCharacterData(vbData)
-
- // Populate empty mission slots for BE characters for UI consistency
- addEmptyMissionSlotsForBe(characterId)
- }
-
- private fun addEmptyMissionSlotsForBe(characterId: Long) {
- // Create 4 empty mission slots (one for each watch position 0-3)
- val emptyMissions = (0..3).map { watchId ->
- SpecialMissions(
- characterId = characterId,
- watchId = watchId,
- missionType = SpecialMission.Type.NONE,
- status = SpecialMission.Status.UNAVAILABLE,
- goal = 0,
- progress = 0,
- timeElapsedInMinutes = 0,
- timeLimitInMinutes = 0,
- )
- }
-
- database
- .userCharacterDao()
- .insertSpecialMissions(*emptyMissions.toTypedArray())
- }
-
private fun addVitalsHistoryToDatabase(
@@ -335,14 +292,12 @@ class FromNfcConverter (
database
.dexDao()
- .insertCharacter(item.toCharIndex.toInt(), dimData.id, date)
- transferSeenDao.markSeen(dimData.name, item.toCharIndex.toInt(), date)
+ .insertCharacter(
+ item.toCharIndex.toInt(),
+ dimData.id,
+ date
+ )
}
}
}
-
- private fun markSeen(cardData: Card, slotId: Int, timestamp: Long) {
- database.dexDao().insertCharacter(slotId, cardData.id, timestamp)
- transferSeenDao.markSeen(cardData.name, slotId, timestamp)
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverter.kt
index bb105dd..143fef8 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverter.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverter.kt
@@ -11,42 +11,22 @@ import com.github.cfogrady.vbnfc.vb.SpecialMission
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.VBHelper
-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.utils.DeviceType
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.firstOrNull
import java.util.Date
class ToNfcConverter(
private val componentActivity: ComponentActivity
) {
- companion object {
- private const val MIN_NFC_TRANSFORMATION_YEAR = 2021
- private const val MAX_NFC_TRANSFORMATION_YEAR = 2035
-
- internal fun shouldEncodeAsBem(
- forcedProfile: DeviceType?,
- storedDeviceType: DeviceType,
- ): Boolean {
- return when (forcedProfile) {
- DeviceType.BEDevice -> true
- DeviceType.VBDevice -> false
- else -> storedDeviceType == DeviceType.BEDevice
- }
- }
- }
-
private val application: VBHelper = componentActivity.applicationContext as VBHelper
private val database: AppDatabase = application.container.db
suspend fun characterToNfc(
- characterId: Long,
- forcedProfile: DeviceType? = null,
+ characterId: Long
): NfcCharacter {
val app = componentActivity.applicationContext as VBHelper
val database = app.container.db
@@ -59,13 +39,7 @@ class ToNfcConverter(
.characterDao()
.getCharacterInfo(userCharacter.charId)
- // Forced profile overrides stored device type for transfer semantics:
- // - NFC-A route forces VBDevice (real bracelets only understand VB)
- // - HCE route forces BEDevice (VitalWear only sends/accepts BE)
- // Otherwise, use the character's own stored type.
- val shouldEncodeAsBem = shouldEncodeAsBem(forcedProfile, userCharacter.characterType)
-
- return if (shouldEncodeAsBem)
+ return if (userCharacter.characterType == DeviceType.BEDevice)
nfcToBENfc(characterId, characterInfo, userCharacter)
else
nfcToVBNfc(characterId, characterInfo, userCharacter)
@@ -81,12 +55,7 @@ class ToNfcConverter(
val vbData = database
.userCharacterDao()
.getVbData(characterId)
- .firstOrNull()
- ?: VBCharacterData(
- id = characterId,
- generation = 0,
- totalTrophies = userCharacter.trophies
- )
+ .first()
val paddedTransformationArray = generateTransformationHistory(characterId, 9)
@@ -207,47 +176,11 @@ class ToNfcConverter(
): BENfcCharacter {
val beData = database
.userCharacterDao()
- .getBeDataOrNull(characterId)
- ?: BECharacterData(
- id = characterId,
- trainingHp = 0,
- trainingAp = 0,
- trainingBp = 0,
- remainingTrainingTimeInMinutes = 0,
- itemEffectMentalStateValue = 0,
- itemEffectMentalStateMinutesRemaining = 0,
- itemEffectActivityLevelValue = 0,
- itemEffectActivityLevelMinutesRemaining = 0,
- itemEffectVitalPointsChangeValue = 0,
- itemEffectVitalPointsChangeMinutesRemaining = 0,
- abilityRarity = NfcCharacter.AbilityRarity.None,
- abilityType = 0,
- abilityBranch = 0,
- abilityReset = 0,
- rank = 0,
- itemType = 0,
- itemMultiplier = 0,
- itemRemainingTime = 0,
- otp0 = "",
- otp1 = "",
- minorVersion = 0,
- majorVersion = 0,
- )
+ .getBeData(characterId)
+ .first()
val paddedTransformationArray = generateTransformationHistory(characterId)
- // Convert hex-encoded OTP strings back to ByteArrays; use empty arrays if not stored
- val otp0ByteArray = if (beData.otp0.isNotEmpty()) {
- beData.otp0.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
- } else {
- ByteArray(8) // Default to 8-byte zero array if not available
- }
- val otp1ByteArray = if (beData.otp1.isNotEmpty()) {
- beData.otp1.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
- } else {
- ByteArray(8) // Default to 8-byte zero array if not available
- }
-
val nfcData = BENfcCharacter(
dimId = characterInfo.cardId.toUShort(),
charIndex = characterInfo.charId.toUShort(),
@@ -288,8 +221,8 @@ class ToNfcConverter(
itemType = beData.itemType.toByte(),
itemMultiplier = beData.itemMultiplier.toByte(),
itemRemainingTime = beData.itemRemainingTime.toByte(),
- otp0 = otp0ByteArray,
- otp1 = otp1ByteArray,
+ otp0 = byteArrayOf(8),
+ otp1 = byteArrayOf(8),
characterCreationFirmwareVersion = FirmwareVersion(
minorVersion = beData.minorVersion.toByte(),
majorVersion = beData.majorVersion.toByte()
@@ -321,15 +254,11 @@ class ToNfcConverter(
"Day: ${calendar.get(Calendar.DAY_OF_MONTH)}"
)
- val rawYear = calendar.get(Calendar.YEAR)
- val normalizedYear = rawYear.coerceIn(MIN_NFC_TRANSFORMATION_YEAR, MAX_NFC_TRANSFORMATION_YEAR)
- if (normalizedYear != rawYear) {
- Log.w("TransformationHistory", "Normalizing out-of-range transformation year $rawYear to $normalizedYear")
- }
-
NfcCharacter.Transformation(
toCharIndex = it.monIndex.toUByte(),
- year = normalizedYear.toUShort(),
+ year = calendar
+ .get(Calendar.YEAR)
+ .toUShort(),
month = (calendar
.get(Calendar.MONTH) + 1)
.toUByte(),
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/ActionScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/ActionScreen.kt
index 8f2a1f5..5aaa7e7 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/ActionScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/ActionScreen.kt
@@ -1,54 +1,47 @@
package com.github.nacabaro.vbhelper.screens.scanScreen.screens
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
-import com.github.nacabaro.vbhelper.screens.scanScreen.TransferHaptics
-import com.github.nacabaro.vbhelper.utils.ImageBitmapData
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.github.nacabaro.vbhelper.components.TopBanner
+import com.github.nacabaro.vbhelper.R
@Composable
fun ActionScreen(
topBannerText: String,
- detectedTransportMessage: String?,
- transferStatusMessage: String?,
- characterPreview: ImageBitmapData? = null,
onClickCancel: () -> Unit,
- isTransferring: Boolean = true,
) {
- val context = LocalContext.current
- val transferHaptics = remember(context) { TransferHaptics(context) }
-
- LaunchedEffect(transferStatusMessage) {
- if (transferStatusMessage != null && transferStatusMessage.contains("progress", ignoreCase = true)) {
- transferHaptics.onTransferProgress()
+ Scaffold (
+ topBar = {
+ TopBanner(
+ text = topBannerText,
+ onBackClick = onClickCancel
+ )
+ }
+ ) { innerPadding ->
+ Column (
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .padding(innerPadding)
+ .fillMaxSize()
+ ) {
+ Text(stringResource(R.string.action_place_near_reader))
+ Button(
+ onClick = onClickCancel,
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Text(stringResource(R.string.action_cancel))
+ }
}
}
-
- if (transferStatusMessage?.contains("complete", ignoreCase = true) == true) {
- TransferCompleteScreen(
- topBannerText = topBannerText,
- resultMessage = transferStatusMessage,
- onClickOk = onClickCancel,
- isSuccess = !transferStatusMessage.contains("fail", ignoreCase = true)
- )
- } else if (isTransferring) {
- TransferAnimationScreen(
- topBannerText = topBannerText,
- detectedTransportMessage = detectedTransportMessage,
- transferStatusMessage = transferStatusMessage,
- characterPreview = characterPreview,
- onClickCancel = onClickCancel,
- isTransferring = true
- )
- } else {
- TransferAnimationScreen(
- topBannerText = topBannerText,
- detectedTransportMessage = detectedTransportMessage,
- transferStatusMessage = transferStatusMessage,
- characterPreview = characterPreview,
- onClickCancel = onClickCancel,
- isTransferring = false
- )
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/ReadingScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/ReadingScreen.kt
index 3b08d90..633be72 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/ReadingScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/ReadingScreen.kt
@@ -1,26 +1,20 @@
package com.github.nacabaro.vbhelper.screens.scanScreen.screens
-import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.screens.cardScreen.ChooseCard
-import com.github.nacabaro.vbhelper.screens.scanScreen.DetectedTransport
import com.github.nacabaro.vbhelper.screens.scanScreen.SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenController
-import com.github.nacabaro.vbhelper.screens.scanScreen.TransferHaptics
import com.github.nacabaro.vbhelper.R
-private const val TAG = "ReadingScreen"
@Composable
fun ReadingScreen(
@@ -28,17 +22,7 @@ fun ReadingScreen(
onCancel: () -> Unit,
onComplete: () -> Unit
) {
- val context = LocalContext.current
- val transferHaptics = remember(context) { TransferHaptics(context) }
val secrets by scanScreenController.secretsFlow.collectAsState(null)
- val transferStatus by scanScreenController.transferStatusFlow.collectAsState(null)
- val detectedTransport by scanScreenController.detectedTransportFlow.collectAsState()
-
- val transportMessage = when (detectedTransport) {
- DetectedTransport.NFC_A -> stringResource(R.string.scan_transport_bandai_bracelet)
- DetectedTransport.ISO_DEP -> stringResource(R.string.scan_transport_vitalwear_hce)
- DetectedTransport.UNKNOWN -> null
- }
var cardsRead by remember { mutableStateOf?>(null) }
@@ -46,80 +30,55 @@ fun ReadingScreen(
var isDoneReadingCharacter by remember { mutableStateOf(false) }
var cardSelectScreen by remember { mutableStateOf(false) }
- LaunchedEffect(readingScreen) {
- if (readingScreen) {
- transferHaptics.onTransferStart()
- }
- }
-
- fun startReadIfNeeded() {
- val availableSecrets = secrets
- if (availableSecrets == null) {
- Log.d(TAG, "startReadIfNeeded: skipped (no secrets)")
- return
- }
- if (!readingScreen || isDoneReadingCharacter || cardSelectScreen) {
- Log.d(
- TAG,
- "startReadIfNeeded: skipped (readingScreen=$readingScreen, done=$isDoneReadingCharacter, cardSelect=$cardSelectScreen)"
- )
- return
- }
-
- Log.d(TAG, "startReadIfNeeded: arming onClickRead")
- scanScreenController.onClickRead(
- secrets = availableSecrets,
- onComplete = {
- Log.d(TAG, "onClickRead.onComplete: marking read complete")
- transferHaptics.onTransferComplete()
- readingScreen = false
- isDoneReadingCharacter = true
- },
- onMultipleCards = { cards ->
- Log.d(TAG, "onClickRead.onMultipleCards: showing card select (count=${cards.size})")
- transferHaptics.onTransferProgress()
- cardsRead = cards
- readingScreen = false
- cardSelectScreen = true
- isDoneReadingCharacter = true
- }
- )
- }
-
DisposableEffect(readingScreen) {
if(readingScreen) {
- Log.d(TAG, "DisposableEffect: readingScreen=true, registering lifecycle listener")
scanScreenController.registerActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER,
object: ActivityLifecycleListener {
override fun onPause() {
- Log.d(TAG, "lifecycle.onPause: cancelRead")
scanScreenController.cancelRead()
}
override fun onResume() {
- Log.d(TAG, "lifecycle.onResume: attempting startReadIfNeeded")
- startReadIfNeeded()
+ scanScreenController.onClickRead(
+ secrets = secrets!!,
+ onComplete = {
+ isDoneReadingCharacter = true
+ },
+ onMultipleCards = { cards ->
+ cardsRead = cards
+ readingScreen = false
+ cardSelectScreen = true
+ isDoneReadingCharacter = true
+ }
+ )
}
}
)
- startReadIfNeeded()
+ scanScreenController.onClickRead(
+ secrets = secrets!!,
+ onComplete = {
+ isDoneReadingCharacter = true
+ },
+ onMultipleCards = { cards ->
+ cardsRead = cards
+ readingScreen = false
+ cardSelectScreen = true
+ isDoneReadingCharacter = true
+ }
+ )
}
onDispose {
if(readingScreen) {
- Log.d(TAG, "DisposableEffect.onDispose: unregister listener + cancelRead")
scanScreenController.unregisterActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER
)
scanScreenController.cancelRead()
- } else {
- Log.d(TAG, "DisposableEffect.onDispose: readingScreen=false, nothing to cancel")
}
}
}
if (isDoneReadingCharacter && !cardSelectScreen) {
- Log.d(TAG, "state gate: done read without cardSelect, calling onComplete")
readingScreen = false
onComplete()
}
@@ -127,33 +86,24 @@ fun ReadingScreen(
if (!readingScreen) {
ReadCharacterScreen(
onClickConfirm = {
- Log.d(TAG, "ReadCharacterScreen.onClickConfirm: entering reading screen")
readingScreen = true
},
onClickCancel = {
- Log.d(TAG, "ReadCharacterScreen.onClickCancel: user cancelled")
onCancel()
}
)
}
if (readingScreen) {
- ActionScreen(
- topBannerText = stringResource(R.string.reading_character_title),
- detectedTransportMessage = transportMessage,
- transferStatusMessage = transferStatus,
- onClickCancel = {
- Log.d(TAG, "ActionScreen.onCancel: cancelRead + onCancel")
+ ActionScreen(topBannerText = stringResource(R.string.reading_character_title),) {
readingScreen = false
scanScreenController.cancelRead()
onCancel()
- }
- )
+ }
} else if (cardSelectScreen) {
ChooseCard(
cards = cardsRead!!,
onCardSelected = { card ->
- Log.d(TAG, "ChooseCard.onCardSelected: selected cardId=${card.id}")
cardSelectScreen = false
scanScreenController.flushCharacter(card.id)
}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/WritingScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/WritingScreen.kt
index f4a8141..8542bd0 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/WritingScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/WritingScreen.kt
@@ -7,21 +7,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.produceState
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.di.VBHelper
-import com.github.nacabaro.vbhelper.screens.scanScreen.DetectedTransport
import com.github.nacabaro.vbhelper.screens.scanScreen.SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenController
-import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenController.WriteResult
-import com.github.nacabaro.vbhelper.screens.scanScreen.TransferHaptics
import com.github.nacabaro.vbhelper.source.StorageRepository
-import com.github.nacabaro.vbhelper.utils.BitmapData
-import com.github.nacabaro.vbhelper.utils.ImageBitmapData
-import com.github.nacabaro.vbhelper.utils.getImageBitmap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import androidx.compose.ui.res.stringResource
@@ -36,53 +29,18 @@ fun WritingScreen(
onCancel: () -> Unit,
) {
val secrets by scanScreenController.secretsFlow.collectAsState(null)
- val transferStatus by scanScreenController.transferStatusFlow.collectAsState(null)
- val detectedTransport by scanScreenController.detectedTransportFlow.collectAsState()
-
- val transportMessage = when (detectedTransport) {
- DetectedTransport.NFC_A -> stringResource(R.string.scan_transport_bandai_bracelet)
- DetectedTransport.ISO_DEP -> stringResource(R.string.scan_transport_vitalwear_hce)
- DetectedTransport.UNKNOWN -> null
- }
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
- val context = LocalContext.current
-
- val characterPreview by produceState(initialValue = null, key1 = characterId) {
- value = withContext(Dispatchers.IO) {
- runCatching {
- storageRepository.getSingleCharacter(characterId)
- }.getOrNull()?.let { character ->
- if (character.spriteWidth > 0 && character.spriteHeight > 0 && character.spriteIdle.isNotEmpty()) {
- BitmapData(
- bitmap = character.spriteIdle,
- width = character.spriteWidth,
- height = character.spriteHeight
- ).getImageBitmap(
- context = context,
- multiplier = 4,
- obscure = false
- )
- } else {
- null
- }
- }
- }
- }
var writing by remember { mutableStateOf(false) }
var writingScreen by remember { mutableStateOf(false) }
var writingConfirmScreen by remember { mutableStateOf(false) }
var isDoneSendingCard by remember { mutableStateOf(false) }
var isDoneWritingCharacter by remember { mutableStateOf(false) }
- var writeResult by remember { mutableStateOf(null) }
DisposableEffect(writing) {
if (writing) {
- val transferHaptics = TransferHaptics(application)
- transferHaptics.onTransferStart()
-
scanScreenController.registerActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER,
object : ActivityLifecycleListener {
@@ -94,13 +52,10 @@ fun WritingScreen(
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter) {
isDoneSendingCard = true
- transferHaptics.onTransferProgress()
}
} else if (!isDoneWritingCharacter) {
- scanScreenController.onClickWrite(secrets!!, nfcCharacter, characterId) { result ->
- writeResult = result
+ scanScreenController.onClickWrite(secrets!!, nfcCharacter) {
isDoneWritingCharacter = true
- transferHaptics.onTransferProgress()
}
}
}
@@ -111,13 +66,10 @@ fun WritingScreen(
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter) {
isDoneSendingCard = true
- transferHaptics.onTransferProgress()
}
} else if (!isDoneWritingCharacter) {
- scanScreenController.onClickWrite(secrets!!, nfcCharacter, characterId) { result ->
- writeResult = result
+ scanScreenController.onClickWrite(secrets!!, nfcCharacter) {
isDoneWritingCharacter = true
- transferHaptics.onTransferProgress()
}
}
}
@@ -147,16 +99,10 @@ fun WritingScreen(
)
} else if (!isDoneSendingCard) {
writing = true
- ActionScreen(
- topBannerText = stringResource(R.string.sending_card_title),
- detectedTransportMessage = transportMessage,
- transferStatusMessage = transferStatus,
- characterPreview = characterPreview,
- onClickCancel = {
+ ActionScreen( topBannerText = stringResource(R.string.sending_card_title)) {
scanScreenController.cancelRead()
onCancel()
- }
- )
+ }
} else if (!writingConfirmScreen) {
writing = false
WriteCharacterScreen (
@@ -171,17 +117,11 @@ fun WritingScreen(
)
} else if (!isDoneWritingCharacter) {
writing = true
- ActionScreen(
- topBannerText = stringResource(R.string.writing_character_action_title),
- detectedTransportMessage = transportMessage,
- transferStatusMessage = transferStatus,
- characterPreview = characterPreview,
- onClickCancel = {
+ ActionScreen(topBannerText = stringResource(R.string.writing_character_action_title)) {
isDoneSendingCard = false
scanScreenController.cancelRead()
onCancel()
- }
- )
+ }
}
var completedWriting by remember { mutableStateOf(false) }
@@ -189,11 +129,8 @@ fun WritingScreen(
LaunchedEffect(isDoneSendingCard, isDoneWritingCharacter) {
withContext(Dispatchers.IO) {
if (isDoneSendingCard && isDoneWritingCharacter) {
- // Only delete source character if transfer was confirmed as move.
- // COPIED means transfer was sent but not confirmed, so keep source.
- if (writeResult == WriteResult.MOVE_CONFIRMED) {
- storageRepository.deleteCharacter(characterId)
- }
+ storageRepository
+ .deleteCharacter(characterId)
completedWriting = true
}
}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt
index 0b4bfc9..6a71765 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt
@@ -5,7 +5,6 @@ import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -13,12 +12,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -72,7 +67,6 @@ fun SettingsScreen(
settingsScreenController.onClickImportCard()
}
-
SettingsSection(title = stringResource(R.string.settings_section_about))
SettingsEntry(
title = stringResource(R.string.settings_credits_title),
@@ -104,38 +98,6 @@ fun SettingsScreen(
) {
settingsScreenController.onClickImportDatabase()
}
-
- SettingsSection(title = stringResource(R.string.settings_section_companion_tools))
- SettingsEntry(
- title = stringResource(R.string.settings_companion_import_card_image_title),
- description = stringResource(R.string.settings_companion_import_card_image_desc)
- ) {
- settingsScreenController.onClickCompanionImportCardImage()
- }
- SettingsEntry(
- title = stringResource(R.string.settings_companion_import_firmware_title),
- description = stringResource(R.string.settings_companion_import_firmware_desc)
- ) {
- settingsScreenController.onClickCompanionImportFirmware()
- }
- SettingsEntry(
- title = stringResource(R.string.settings_companion_send_watch_logs_title),
- description = stringResource(R.string.settings_companion_send_watch_logs_desc)
- ) {
- settingsScreenController.onClickCompanionSendWatchLogs()
- }
- SettingsEntry(
- title = stringResource(R.string.settings_companion_send_phone_logs_title),
- description = stringResource(R.string.settings_companion_send_phone_logs_desc)
- ) {
- settingsScreenController.onClickCompanionSendPhoneLogs()
- }
- SettingsSwitchEntry(
- title = stringResource(R.string.settings_enable_dim_to_bem_title),
- description = stringResource(R.string.settings_enable_dim_to_bem_desc),
- isEnabled = settingsScreenController,
- onToggle = { settingsScreenController.onToggleDimToBemConversion(it) }
- )
}
}
}
@@ -161,40 +123,6 @@ fun SettingsEntry(
}
}
-@Composable
-fun SettingsSwitchEntry(
- title: String,
- description: String,
- isEnabled: SettingsScreenControllerImpl,
- onToggle: (Boolean) -> Unit
-) {
- val dimToBemEnabled by isEnabled.dimToBemConversionEnabled.collectAsState(false)
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(
- modifier = Modifier
- .weight(1f)
- .padding(end = 16.dp)
- ) {
- Text(text = title)
- Text(
- text = description,
- fontSize = 12.sp,
- color = MaterialTheme.colorScheme.outline
- )
- }
- Switch(
- checked = dimToBemEnabled,
- onCheckedChange = onToggle
- )
- }
-}
-
@Composable
fun SettingsSection(
title: String
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt
index 45f8565..ea0fb19 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt
@@ -5,8 +5,4 @@ interface SettingsScreenController {
fun onClickImportDatabase()
fun onClickImportApk()
fun onClickImportCard()
- fun onClickCompanionImportCardImage()
- fun onClickCompanionImportFirmware()
- fun onClickCompanionSendWatchLogs()
- fun onClickCompanionSendPhoneLogs()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt
index dc065af..dd78c14 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt
@@ -1,6 +1,5 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen
-import android.content.Intent
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import android.net.Uri
@@ -8,13 +7,8 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
-import com.github.nacabaro.vbhelper.companion.card.CompanionImportCardActivity
-import com.github.nacabaro.vbhelper.companion.firmware.CompanionFirmwareImportActivity
-import com.github.nacabaro.vbhelper.companion.logs.CompanionWatchLogsActivity
-import com.github.nacabaro.vbhelper.companion.logging.TinyLogTree
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.VBHelper
-import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.screens.settingsScreen.controllers.CardImportController
import com.github.nacabaro.vbhelper.screens.settingsScreen.controllers.DatabaseManagementController
import com.github.nacabaro.vbhelper.source.ApkSecretsImporter
@@ -22,7 +16,6 @@ import com.github.nacabaro.vbhelper.source.SecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsRepository
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.first
class SettingsScreenControllerImpl(
@@ -36,14 +29,11 @@ class SettingsScreenControllerImpl(
private val application = context.applicationContext as VBHelper
private val secretsRepository: SecretsRepository = application.container.dataStoreSecretsRepository
private val database: AppDatabase = application.container.db
- private val cardSettingsRepository = application.container.cardSettingsRepository
private val databaseManagementController = DatabaseManagementController(
componentActivity = context,
application = application
)
- val dimToBemConversionEnabled = cardSettingsRepository.enableDimToBemConversion
-
init {
filePickerLauncher = context.registerForActivityResult(
ActivityResultContracts.CreateDocument("application/octet-stream")
@@ -111,41 +101,13 @@ class SettingsScreenControllerImpl(
filePickerCard.launch(arrayOf("*/*"))
}
- override fun onClickCompanionImportCardImage() {
- context.startActivity(Intent(context, CompanionImportCardActivity::class.java))
- }
-
- override fun onClickCompanionImportFirmware() {
- context.startActivity(Intent(context, CompanionFirmwareImportActivity::class.java))
- }
-
- override fun onClickCompanionSendWatchLogs() {
- context.startActivity(Intent(context, CompanionWatchLogsActivity::class.java))
- }
-
- override fun onClickCompanionSendPhoneLogs() {
- val file = TinyLogTree.getMostRecentLogFile(context.applicationContext)
- if (file == null) {
- Toast.makeText(context, context.getString(R.string.companion_logs_no_files), Toast.LENGTH_SHORT).show()
- return
- }
- application.companionLogService.sendLogFile(context.applicationContext, file, context)
- }
-
- fun onToggleDimToBemConversion(enabled: Boolean) {
- context.lifecycleScope.launch(Dispatchers.IO) {
- cardSettingsRepository.setEnableDimToBemConversion(enabled)
- }
- }
-
private fun importCard(uri: Uri) {
context.lifecycleScope.launch(Dispatchers.IO) {
val contentResolver = context.contentResolver
val inputStream = contentResolver.openInputStream(uri)
inputStream.use { fileReader ->
- val dimToBemEnabled = cardSettingsRepository.enableDimToBemConversion.first()
- val cardImportController = CardImportController(database, dimToBemEnabled)
+ val cardImportController = CardImportController(database)
cardImportController.importCard(fileReader)
}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt
index 5af2606..1626405 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt
@@ -13,8 +13,7 @@ import com.github.nacabaro.vbhelper.domain.characters.Sprite
import java.io.InputStream
class CardImportController(
- private val database: AppDatabase,
- private val enableDimToBemConversion: Boolean = false
+ private val database: AppDatabase
) {
suspend fun importCard(
fileReader: InputStream?
@@ -118,16 +117,6 @@ class CardImportController(
.spriteDao()
.insertSprite(domainSprite)
- // Apply DIM-to-BEM conversion if enabled and card is a DIM card (not BEM)
- var baseHp = characters[index].hp
- var baseBp = characters[index].dp
- var baseAp = characters[index].ap
-
- if (enableDimToBemConversion && card is DimCard) {
- baseHp = convertDimStatToBem(baseHp, characters[index].stage, Stat.HP)
- baseBp = convertDimStatToBem(baseBp, characters[index].stage, Stat.BP)
- baseAp = convertDimStatToBem(baseAp, characters[index].stage, Stat.AP)
- }
domainCharacters.add(
CardCharacter(
@@ -137,9 +126,9 @@ class CardImportController(
nameSprite = card.spriteData.sprites[spriteCounter].pixelData,
stage = characters[index].stage,
attribute = NfcCharacter.Attribute.entries[characters[index].attribute],
- baseHp = baseHp,
- baseBp = baseBp,
- baseAp = baseAp,
+ baseHp = characters[index].hp,
+ baseBp = characters[index].dp,
+ baseAp = characters[index].ap,
nameWidth = card.spriteData.sprites[spriteCounter].spriteDimensions.width,
nameHeight = card.spriteData.sprites[spriteCounter].spriteDimensions.height
)
@@ -161,40 +150,6 @@ class CardImportController(
.insertCharacter(*domainCharacters.toTypedArray())
}
- private enum class Stat { HP, BP, AP }
-
- private fun convertDimStatToBem(dimStat: Int, phase: Int, statType: Stat): Int {
- // DIM-to-BEM conversion factors based on phase and stat type
- // These are approximations based on the average conversion ratios
- val conversionFactor = when (phase) {
- 1 -> 0.0f
- 2 -> 0.0f
- 3 -> when (statType) {
- Stat.HP -> 4657.5f
- Stat.BP -> 4657.5f
- Stat.AP -> 1022.5f
- }
- 4 -> when (statType) {
- Stat.HP -> 5171.528f
- Stat.BP -> 5028.528f
- Stat.AP -> 1222.9167f
- }
- 5 -> when (statType) {
- Stat.HP -> 5664.4f
- Stat.BP -> 5036.4f
- Stat.AP -> 1505.7333f
- }
- 6 -> when (statType) {
- Stat.HP -> 5972.2974f
- Stat.BP -> 5256.2974f
- Stat.AP -> 1976.7568f
- }
- else -> 0.0f
- }
-
- return (dimStat * conversionFactor).toInt()
- }
-
private suspend fun importAdventureMissions(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt
index 0f9e34e..f3fb931 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt
@@ -1,14 +1,11 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen.controllers
-import androidx.room.Room
-import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
-import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.VBHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -21,19 +18,6 @@ class DatabaseManagementController(
val application: VBHelper
) {
private val roomDbName = "internalDb"
- private val requiredLegacyVersions = setOf(1, 2, 3, 4, 5)
- private val requiredCoreTables = setOf(
- "UserCharacter",
- "Character",
- "Card",
- "CardCharacter",
- "TransformationHistory",
- )
-
- private data class ValidationResult(
- val isValid: Boolean,
- val reason: String? = null,
- )
fun exportDatabase( destinationUri: Uri) {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
@@ -75,19 +59,6 @@ class DatabaseManagementController(
return@launch
}
- val validationResult = validateImportBackup(sourceUri)
- if (!validationResult.isValid) {
- componentActivity.runOnUiThread {
- Toast.makeText(
- componentActivity,
- validationResult.reason
- ?: "Import blocked: backup is incompatible with this VBH-VW version.",
- Toast.LENGTH_LONG
- ).show()
- }
- return@launch
- }
-
application.container.db.close()
val dbPath = componentActivity.getDatabasePath(roomDbName)
@@ -121,118 +92,6 @@ class DatabaseManagementController(
}
}
- private fun validateImportBackup(sourceUri: Uri): ValidationResult {
- val tempDbName = "import_validation_temp.db"
- val tempDbPath = componentActivity.getDatabasePath(tempDbName)
- val tempShm = File(tempDbPath.parentFile, "$tempDbName-shm")
- val tempWal = File(tempDbPath.parentFile, "$tempDbName-wal")
-
- try {
- if (tempDbPath.exists()) tempDbPath.delete()
- if (tempShm.exists()) tempShm.delete()
- if (tempWal.exists()) tempWal.delete()
-
- componentActivity.contentResolver.openInputStream(sourceUri)?.use { inputStream ->
- tempDbPath.outputStream().use { outputStream ->
- copyFile(inputStream, outputStream)
- }
- } ?: return ValidationResult(false, "Import blocked: unable to read selected backup file.")
-
- // Fast static checks before attempting full Room open/migration.
- val legacyVersion = readPragmaUserVersion(tempDbPath)
- if (legacyVersion !in requiredLegacyVersions) {
- return ValidationResult(
- false,
- "Import blocked: unsupported DB version ($legacyVersion). Use a VBH-VW export from a compatible app version."
- )
- }
-
- val tableNames = readTableNames(tempDbPath)
- if (!tableNames.contains("room_master_table")) {
- return ValidationResult(
- false,
- "Import blocked: backup is not a Room VBH-VW database (room metadata missing)."
- )
- }
-
- val missingTables = requiredCoreTables.filterNot { tableNames.contains(it) }
- if (missingTables.isNotEmpty()) {
- return ValidationResult(
- false,
- "Import blocked: backup is missing required tables (${missingTables.joinToString(", ")})."
- )
- }
-
- // Hard validation: open with the app's exact Room schema + migrations.
- val probeDb = Room.databaseBuilder(
- componentActivity,
- AppDatabase::class.java,
- tempDbName
- )
- .addMigrations(AppDatabase.MIGRATION_1_2)
- .addMigrations(AppDatabase.MIGRATION_2_3)
- .addMigrations(AppDatabase.MIGRATION_3_5)
- .addMigrations(AppDatabase.MIGRATION_4_5)
- .addMigrations(AppDatabase.MIGRATION_5_6)
- .build()
-
- try {
- probeDb.openHelper.writableDatabase.query("SELECT 1").close()
- } catch (e: Exception) {
- return ValidationResult(
- false,
- "Import blocked: backup cannot be opened/migrated by this VBH-VW build."
- )
- } finally {
- probeDb.close()
- }
-
- return ValidationResult(true)
- } finally {
- if (tempDbPath.exists()) tempDbPath.delete()
- if (tempShm.exists()) tempShm.delete()
- if (tempWal.exists()) tempWal.delete()
- }
- }
-
- private fun readPragmaUserVersion(dbFile: File): Int {
- val sqliteDb = SQLiteDatabase.openDatabase(
- dbFile.absolutePath,
- null,
- SQLiteDatabase.OPEN_READONLY
- )
- try {
- sqliteDb.rawQuery("PRAGMA user_version", null).use { cursor ->
- if (cursor.moveToFirst()) {
- return cursor.getInt(0)
- }
- }
- return -1
- } finally {
- sqliteDb.close()
- }
- }
-
- private fun readTableNames(dbFile: File): Set {
- val sqliteDb = SQLiteDatabase.openDatabase(
- dbFile.absolutePath,
- null,
- SQLiteDatabase.OPEN_READONLY
- )
- try {
- val tableNames = mutableSetOf()
- sqliteDb.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null)
- .use { cursor ->
- while (cursor.moveToNext()) {
- tableNames.add(cursor.getString(0))
- }
- }
- return tableNames
- } finally {
- sqliteDb.close()
- }
- }
-
private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
val buffer = ByteArray(1024)
var bytesRead: Int
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt
index c379a27..53bc737 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt
@@ -44,18 +44,12 @@ class AuthRepository(
preferences[IS_AUTHENTICATED] = isAuthenticated
if (nacatechToken != null) {
preferences[AUTH_TOKEN] = nacatechToken
- } else {
- preferences.remove(AUTH_TOKEN)
}
if (sessionToken != null) {
preferences[SESSION_TOKEN] = sessionToken
- } else {
- preferences.remove(SESSION_TOKEN)
}
if (userId != null) {
preferences[USER_ID] = userId
- } else {
- preferences.remove(USER_ID)
}
}
}
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
index 45078e0..bc3ad95 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporter.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporter.kt
@@ -1,199 +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.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.utils.DeviceType
-import com.google.protobuf.ByteString
import kotlinx.coroutines.flow.firstOrNull
-
-internal const val DEFAULT_VITALWEAR_TRAINING_TIME_SECONDS = 100L * 60L * 60L
-
-internal fun resolveTrainingSeconds(
- deviceType: DeviceType,
- beData: BECharacterData?,
-): Long {
- val remainingTrainingTimeInMinutes = beData?.remainingTrainingTimeInMinutes
- if (remainingTrainingTimeInMinutes != null) {
- return remainingTrainingTimeInMinutes.toLong() * 60L
- }
-
- // VBHelper only stores the explicit training timer on BE payloads. When exporting non-BE
- // characters to VitalWear, preserve their ability to train by seeding the same default
- // window VitalWear uses for freshly created partners instead of sending an immediate zero.
- return if (deviceType == DeviceType.BEDevice) {
- 0L
- } else {
- DEFAULT_VITALWEAR_TRAINING_TIME_SECONDS
- }
-}
+import kotlinx.coroutines.runBlocking
+import java.io.File
class VitalWearCharacterExporter(
+ private val context: Context,
private val database: AppDatabase
) {
- /**
- * Builds a Character proto from the stored character data.
- * Used by the HCE ISO-DEP transfer path.
- *
- * @param characterId ID of the character to export
- * @param forcedTransferProfile If specified (e.g., DeviceType.BEDevice for HCE), overrides stored device type
- * and ensures stats are serialized for the target ecosystem.
- * Used to lock VitalWear (HCE) transfers to BE profile.
- */
- suspend fun buildCharacterProto(
- characterId: Long,
- forcedTransferProfile: DeviceType? = null,
- ): Character {
- val characterWithSprites = database.userCharacterDao().getCharacterWithSprites(characterId)
- val userCharacter = database.userCharacterDao().getCharacter(characterId)
- val cardCharacter = database.characterDao().getCharacterById(userCharacter.charId)
- ?: error("Card character not found for user character $characterId")
- val sprite = database.spriteDao().getSpriteById(cardCharacter.spriteId)
- ?: error("Sprite not found for user character $characterId")
- val characterInfo = database.characterDao().getCharacterInfo(userCharacter.charId)
- val vwSettings = database.vitalWearSettingsDao().getByCharacterId(characterId)
- val card = database.cardDao().getCardByCharacterIdSync(characterId)
- ?: error("Card not found for character $characterId")
- val cardProgress = database.cardProgressDao().getCardProgressSync(card.id) ?: 0
- val beData = database.userCharacterDao().getBeDataOrNull(characterId)
- val vbData = database.userCharacterDao().getVbDataOrNull(characterId)
- val normalizedTransformationCountdownMinutes = normalizeTransformationCountdownMinutes(
- transformationCountdownMinutes = userCharacter.transformationCountdown,
- hasPossibleTransformations = hasPossibleTransformations(userCharacter.charId),
- )
-
- // Use forced profile if specified (e.g., BE for HCE route), otherwise use stored type.
- // For HCE/VitalWear routes, forcedTransferProfile = BE ensures BE stats are serialized.
- val transferDeviceType = forcedTransferProfile ?: userCharacter.characterType
-
- val settingsBuilder = Character.Settings.newBuilder()
- .setTrainingInBackground(vwSettings?.trainingInBackground ?: false)
- .setAllowedBattles(resolveAllowedBattles(vwSettings?.allowedBattles))
-
- // VitalWear chooses DIM-vs-BE runtime path from card type + assumedFranchise.
- // For forced BE HCE exports of DIM cards, provide a stable assumed franchise hint.
- if (forcedTransferProfile == DeviceType.BEDevice && !card.isBEm) {
- settingsBuilder.setAssumedFranchise(0)
- }
-
- return Character.newBuilder()
- .setCardId(card.cardId)
- .setCardName(card.name)
- .setCharacterStats(
- Character.CharacterStats.newBuilder()
- .setSlotId(characterInfo.charId)
- .setVitals(characterWithSprites.vitalPoints)
- .setTrainingTimeRemainingInSeconds(resolveTrainingSeconds(transferDeviceType, beData))
- .setTimeUntilNextTransformation(normalizedTransformationCountdownMinutes.toLong() * 60L)
- .setTrainedBp(resolveTrainedBp(beData))
- .setTrainedHp(resolveTrainedHp(beData))
- .setTrainedAp(resolveTrainedAp(beData))
- .setTrainedPp(characterWithSprites.trophies)
- .setInjured(characterWithSprites.injuryStatus.name.lowercase().contains("inj"))
- .setAccumulatedDailyInjuries(vwSettings?.accumulatedDailyInjuries ?: 0)
- .setTotalBattles(characterWithSprites.totalBattlesWon + characterWithSprites.totalBattlesLost)
- .setCurrentPhaseBattles(characterWithSprites.currentPhaseBattlesWon + characterWithSprites.currentPhaseBattlesLost)
- .setTotalWins(characterWithSprites.totalBattlesWon)
- .setCurrentPhaseWins(characterWithSprites.currentPhaseBattlesWon)
- .setMood(characterWithSprites.mood)
- .setDeviceType(transferDeviceType.toTransferDeviceType())
- .setAgeInDays(userCharacter.ageInDays.coerceAtLeast(0))
- .setActivityLevel(userCharacter.activityLevel.coerceAtLeast(0))
- .setHeartRateCurrent(userCharacter.heartRateCurrent.coerceAtLeast(0))
- .setGeneration(vbData?.generation ?: 0)
- .setTotalTrophies(vbData?.totalTrophies ?: userCharacter.trophies.coerceAtLeast(0))
- .setNextAdventureMissionStage(characterInfo.currentStage.coerceAtLeast(0))
- .setItemEffectMentalStateValue(beData?.itemEffectMentalStateValue ?: 0)
- .setItemEffectMentalStateMinutesRemaining(beData?.itemEffectMentalStateMinutesRemaining ?: 0)
- .setItemEffectActivityLevelValue(beData?.itemEffectActivityLevelValue ?: 0)
- .setItemEffectActivityLevelMinutesRemaining(beData?.itemEffectActivityLevelMinutesRemaining ?: 0)
- .setItemEffectVitalPointsChangeValue(beData?.itemEffectVitalPointsChangeValue ?: 0)
- .setItemEffectVitalPointsChangeMinutesRemaining(beData?.itemEffectVitalPointsChangeMinutesRemaining ?: 0)
- .setAbilityRarity(beData?.abilityRarity?.ordinal ?: 0)
- .setAbilityType(beData?.abilityType ?: 0)
- .setAbilityBranch(beData?.abilityBranch ?: 0)
- .setAbilityReset(beData?.abilityReset ?: 0)
- .setRank(beData?.rank ?: 0)
- .setItemType(beData?.itemType ?: 0)
- .setItemMultiplier(beData?.itemMultiplier ?: 0)
- .setItemRemainingTime(beData?.itemRemainingTime ?: 0)
- .setFirmwareMinorVersion(beData?.minorVersion ?: 0)
- .setFirmwareMajorVersion(beData?.majorVersion ?: 0)
- .build()
- )
- .setSettings(settingsBuilder.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)
+ 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()
- }
- )
- .setTransferCard(
- Character.TransferCard.newBuilder()
- .setCardId(card.cardId)
- .setCardName(card.name)
- .setIsBem(card.isBEm)
- .setStageCount(card.stageCount)
- .setFranchise(0)
- .build()
- )
- .setTransferSpecies(
- Character.TransferSpecies.newBuilder()
- .setSlotId(characterInfo.charId)
- .setStage(cardCharacter.stage)
- .setAttribute(cardCharacter.attribute.ordinal)
- .setBaseHp(cardCharacter.baseHp)
- .setBaseBp(cardCharacter.baseBp)
- .setBaseAp(cardCharacter.baseAp)
- .setNameSprite(ByteString.copyFrom(cardCharacter.nameSprite))
- .setNameSpriteWidth(cardCharacter.nameWidth)
- .setNameSpriteHeight(cardCharacter.nameHeight)
- .setIdle1(ByteString.copyFrom(sprite.spriteIdle1))
- .setIdle2(ByteString.copyFrom(sprite.spriteIdle2))
- .setWalk1(ByteString.copyFrom(sprite.spriteWalk1))
- .setWalk2(ByteString.copyFrom(sprite.spriteWalk2))
- .setRun1(ByteString.copyFrom(sprite.spriteRun1))
- .setRun2(ByteString.copyFrom(sprite.spriteRun2))
- .setTrain1(ByteString.copyFrom(sprite.spriteTrain1))
- .setTrain2(ByteString.copyFrom(sprite.spriteTrain2))
- .setWin(ByteString.copyFrom(sprite.spriteHappy))
- .setDown(ByteString.copyFrom(sprite.spriteSleep))
- .setAttack(ByteString.copyFrom(sprite.spriteAttack))
- .setDodge(ByteString.copyFrom(sprite.spriteDodge))
- .setSplash(ByteString.copyFrom(sprite.spriteIdle1))
- .setSpriteWidth(sprite.width)
- .setSpriteHeight(sprite.height)
- .build()
- )
- .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 resolveTrainedBp(beData: com.github.nacabaro.vbhelper.domain.device_data.BECharacterData?): Int {
- return beData?.trainingBp ?: 0
+ 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 resolveTrainedHp(beData: com.github.nacabaro.vbhelper.domain.device_data.BECharacterData?): Int {
- return beData?.trainingHp ?: 0
+ private fun resolveTrainedBp(characterId: Long, deviceType: DeviceType): Int {
+ return if (deviceType == DeviceType.BEDevice) {
+ database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingBp ?: 0
+ } else 0
}
- private fun resolveTrainedAp(beData: com.github.nacabaro.vbhelper.domain.device_data.BECharacterData?): Int {
- return beData?.trainingAp ?: 0
+ private fun resolveTrainedHp(characterId: Long, deviceType: DeviceType): Int {
+ return if (deviceType == DeviceType.BEDevice) {
+ database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingHp ?: 0
+ } else 0
}
- private suspend fun hasPossibleTransformations(cardCharacterId: Long): Boolean {
- return database.characterDao().getEvolutionRequirementsForCard(cardCharacterId).firstOrNull()?.isNotEmpty() == true
+ private fun resolveTrainedAp(characterId: Long, deviceType: DeviceType): Int {
+ return if (deviceType == DeviceType.BEDevice) {
+ database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingAp ?: 0
+ } else 0
}
-
- private fun resolveAllowedBattles(rawValue: Int?): Character.Settings.AllowedBattles {
- if (rawValue == null) return Character.Settings.AllowedBattles.CARD_ONLY
- return Character.Settings.AllowedBattles.forNumber(rawValue)
- ?: Character.Settings.AllowedBattles.CARD_ONLY
+ 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
index 704b0f9..6ec3b65 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt
@@ -1,25 +1,15 @@
package com.github.nacabaro.vbhelper.source
import com.github.cfogrady.vbnfc.data.NfcCharacter
-import com.github.cfogrady.vitalwear.common.data.SharedTransferSeenDao
import com.github.cfogrady.vitalwear.protos.Character
import com.github.nacabaro.vbhelper.database.AppDatabase
-import com.github.nacabaro.vbhelper.domain.card.Card
-import com.github.nacabaro.vbhelper.domain.card.CardCharacter
-import com.github.nacabaro.vbhelper.domain.card.CardProgress
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.domain.characters.Sprite
import com.github.nacabaro.vbhelper.utils.DeviceType
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.runBlocking
import kotlin.math.max
class VitalWearCharacterImporter(
- private val database: AppDatabase,
- private val transferSeenDao: SharedTransferSeenDao,
+ private val database: AppDatabase
) {
data class ImportResult(
val success: Boolean,
@@ -28,16 +18,15 @@ class VitalWearCharacterImporter(
fun importCharacter(character: Character): ImportResult {
val importedCard = resolveCard(character)
- ?: createPlaceholderCard(character)
?: return ImportResult(
success = false,
message = "Matching card not found in VBHelper. Import that card first."
)
val slotId = character.characterStats.slotId
- val cardCharacter = database.characterDao().getCharacterByMonIndexOrNull(slotId, importedCard.id)
- ?: createPlaceholderCharacter(importedCard, character)
- ?: return ImportResult(
+ 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}."
)
@@ -48,110 +37,60 @@ class VitalWearCharacterImporter(
val totalWins = character.characterStats.totalWins.coerceAtMost(totalBattles)
val currentPhaseBattles = character.characterStats.currentPhaseBattles
val currentPhaseWins = character.characterStats.currentPhaseWins.coerceAtMost(currentPhaseBattles)
- val normalizedTransformationCountdown = normalizeTransformationCountdownMinutes(
- transformationCountdownMinutes = secondsToMinutes(character.characterStats.timeUntilNextTransformation),
- hasPossibleTransformations = hasPossibleTransformations(cardCharacter.id),
- )
- val hasBePayloadStats = character.characterStats.trainedHp > 0 ||
- character.characterStats.trainedAp > 0 ||
- character.characterStats.trainedBp > 0 ||
- character.characterStats.trainingTimeRemainingInSeconds > 0L
- val fallbackIsBeCharacter = importedCard.isBEm || hasBePayloadStats
- val deviceType = resolveDeviceType(
- transferDeviceType = character.characterStats.deviceType,
- fallbackIsBeCharacter = fallbackIsBeCharacter,
- )
- val isBeCharacter = deviceType == DeviceType.BEDevice
val userCharacterId = database.userCharacterDao().insertCharacterData(
UserCharacter(
charId = cardCharacter.id,
- ageInDays = character.characterStats.ageInDays.takeIf { it > 0 }
- ?: max(character.transformationHistoryCount - 1, 0),
+ ageInDays = max(character.transformationHistoryCount - 1, 0),
mood = character.characterStats.mood,
vitalPoints = character.characterStats.vitals,
- transformationCountdown = normalizedTransformationCountdown,
+ 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 = character.characterStats.activityLevel.coerceAtLeast(0),
- heartRateCurrent = character.characterStats.heartRateCurrent.coerceAtLeast(0),
- characterType = deviceType,
+ activityLevel = 0,
+ heartRateCurrent = 0,
+ characterType = DeviceType.BEDevice,
isActive = true
)
)
- runBlocking {
- database.vitalWearSettingsDao().upsert(
- VitalWearCharacterSettings(
- characterId = userCharacterId,
- trainingInBackground = character.settings.trainingInBackground,
- allowedBattles = character.settings.allowedBattles.number,
- accumulatedDailyInjuries = character.characterStats.accumulatedDailyInjuries,
- )
+ 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 vbProfile = VBCharacterData(
- id = userCharacterId,
- generation = if (character.characterStats.generation > 0) {
- character.characterStats.generation
- } else {
- max(character.transformationHistoryCount - 1, 0)
- },
- totalTrophies = if (character.characterStats.totalTrophies > 0) {
- character.characterStats.totalTrophies
- } else {
- character.characterStats.trainedPp.coerceAtLeast(0)
- }
)
- database.userCharacterDao().insertVBCharacterData(vbProfile)
-
- if (isBeCharacter) {
- val abilityRarity = resolveAbilityRarity(character.characterStats.abilityRarity)
- database.userCharacterDao().insertBECharacterData(
- BECharacterData(
- id = userCharacterId,
- trainingHp = character.characterStats.trainedHp,
- trainingAp = character.characterStats.trainedAp,
- trainingBp = character.characterStats.trainedBp,
- remainingTrainingTimeInMinutes = secondsToMinutes(character.characterStats.trainingTimeRemainingInSeconds),
- itemEffectMentalStateValue = character.characterStats.itemEffectMentalStateValue,
- itemEffectMentalStateMinutesRemaining = character.characterStats.itemEffectMentalStateMinutesRemaining,
- itemEffectActivityLevelValue = character.characterStats.itemEffectActivityLevelValue,
- itemEffectActivityLevelMinutesRemaining = character.characterStats.itemEffectActivityLevelMinutesRemaining,
- itemEffectVitalPointsChangeValue = character.characterStats.itemEffectVitalPointsChangeValue,
- itemEffectVitalPointsChangeMinutesRemaining = character.characterStats.itemEffectVitalPointsChangeMinutesRemaining,
- abilityRarity = abilityRarity,
- abilityType = character.characterStats.abilityType,
- abilityBranch = character.characterStats.abilityBranch,
- abilityReset = character.characterStats.abilityReset,
- rank = character.characterStats.rank,
- itemType = character.characterStats.itemType,
- itemMultiplier = character.characterStats.itemMultiplier,
- itemRemainingTime = character.characterStats.itemRemainingTime,
- otp0 = "",
- otp1 = "",
- minorVersion = character.characterStats.firmwareMinorVersion,
- majorVersion = character.characterStats.firmwareMajorVersion
- )
- )
- }
val now = System.currentTimeMillis()
- markSeen(importedCard.name, slotId, importedCard.id, now)
+ database.dexDao().insertCharacter(slotId, importedCard.id, now)
- var insertedTransformationCount = 0
for (transformation in character.transformationHistoryList) {
- val transformationCard = resolveRelatedCard(
- incomingCardName = transformation.cardName,
- incomingCardId = character.cardId,
- matchedRootCard = importedCard,
- incomingRootCardName = character.cardName,
- )
+ val transformationCard = resolveCard(transformation.cardName, character.cardId)
if (transformationCard != null) {
database.userCharacterDao().insertTransformation(
userCharacterId,
@@ -159,28 +98,12 @@ class VitalWearCharacterImporter(
transformationCard.id,
now
)
- insertedTransformationCount++
- markSeen(transformationCard.name, transformation.slotId, transformationCard.id, now)
+ database.dexDao().insertCharacter(transformation.slotId, transformationCard.id, now)
}
}
- if (insertedTransformationCount == 0) {
- // Keep HomeScreen renderable for freshly imported characters with empty history.
- database.userCharacterDao().insertTransformation(
- userCharacterId,
- slotId,
- importedCard.id,
- now
- )
- }
-
for ((cardName, maxAdventureCompleted) in character.maxAdventureCompletedByCardMap) {
- val adventureCard = resolveRelatedCard(
- incomingCardName = cardName,
- incomingCardId = null,
- matchedRootCard = importedCard,
- incomingRootCardName = character.cardName,
- ) ?: continue
+ val adventureCard = resolveCard(cardName, null) ?: continue
val currentStage = (maxAdventureCompleted + 1).coerceAtLeast(0)
database.cardProgressDao().updateCardProgress(
currentStage = currentStage,
@@ -189,189 +112,6 @@ class VitalWearCharacterImporter(
)
}
- val nextAdventureMissionStage = character.characterStats.nextAdventureMissionStage.coerceAtLeast(0)
- if (nextAdventureMissionStage > 0) {
- database.cardProgressDao().updateCardProgress(
- currentStage = nextAdventureMissionStage,
- cardId = importedCard.id,
- unlocked = nextAdventureMissionStage > importedCard.stageCount,
- )
- }
-
- return ImportResult(
- success = true,
- message = "Imported ${importedCard.name} slot $slotId from VitalWear."
- )
- }
-
- /**
- * Import a VitalWear character with a forced device type.
- * Used by HCE (VitalWear) read path to ensure imports are marked as BE.
- *
- * @param character Protobuf character from VitalWear HCE
- * @param forcedDeviceType Always use this device type for the imported character (e.g., BEDevice for HCE)
- */
- fun importCharacter(character: Character, forcedDeviceType: DeviceType): ImportResult {
- val importedCard = resolveCard(character)
- ?: createPlaceholderCard(character)
- ?: return ImportResult(
- success = false,
- message = "Matching card not found in VBHelper. Import that card first."
- )
-
- val slotId = character.characterStats.slotId
- val cardCharacter = database.characterDao().getCharacterByMonIndexOrNull(slotId, importedCard.id)
- ?: createPlaceholderCharacter(importedCard, character)
- ?: 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 normalizedTransformationCountdown = normalizeTransformationCountdownMinutes(
- transformationCountdownMinutes = secondsToMinutes(character.characterStats.timeUntilNextTransformation),
- hasPossibleTransformations = hasPossibleTransformations(cardCharacter.id),
- )
-
- val userCharacterId = database.userCharacterDao().insertCharacterData(
- UserCharacter(
- charId = cardCharacter.id,
- ageInDays = character.characterStats.ageInDays.takeIf { it > 0 }
- ?: max(character.transformationHistoryCount - 1, 0),
- mood = character.characterStats.mood,
- vitalPoints = character.characterStats.vitals,
- transformationCountdown = normalizedTransformationCountdown,
- injuryStatus = resolveInjuryStatus(character.characterStats.injured),
- trophies = character.characterStats.trainedPp,
- currentPhaseBattlesWon = currentPhaseWins,
- currentPhaseBattlesLost = (currentPhaseBattles - currentPhaseWins).coerceAtLeast(0),
- totalBattlesWon = totalWins,
- totalBattlesLost = (totalBattles - totalWins).coerceAtLeast(0),
- activityLevel = character.characterStats.activityLevel.coerceAtLeast(0),
- heartRateCurrent = character.characterStats.heartRateCurrent.coerceAtLeast(0),
- characterType = forcedDeviceType,
- isActive = true
- )
- )
-
- runBlocking {
- database.vitalWearSettingsDao().upsert(
- VitalWearCharacterSettings(
- characterId = userCharacterId,
- trainingInBackground = character.settings.trainingInBackground,
- allowedBattles = character.settings.allowedBattles.number,
- accumulatedDailyInjuries = character.characterStats.accumulatedDailyInjuries,
- )
- )
- }
-
- val vbProfile = VBCharacterData(
- id = userCharacterId,
- generation = if (character.characterStats.generation > 0) {
- character.characterStats.generation
- } else {
- max(character.transformationHistoryCount - 1, 0)
- },
- totalTrophies = if (character.characterStats.totalTrophies > 0) {
- character.characterStats.totalTrophies
- } else {
- character.characterStats.trainedPp.coerceAtLeast(0)
- }
- )
- database.userCharacterDao().insertVBCharacterData(vbProfile)
-
- if (forcedDeviceType == DeviceType.BEDevice) {
- val abilityRarity = resolveAbilityRarity(character.characterStats.abilityRarity)
- database.userCharacterDao().insertBECharacterData(
- BECharacterData(
- id = userCharacterId,
- trainingHp = character.characterStats.trainedHp,
- trainingAp = character.characterStats.trainedAp,
- trainingBp = character.characterStats.trainedBp,
- remainingTrainingTimeInMinutes = secondsToMinutes(character.characterStats.trainingTimeRemainingInSeconds),
- itemEffectMentalStateValue = character.characterStats.itemEffectMentalStateValue,
- itemEffectMentalStateMinutesRemaining = character.characterStats.itemEffectMentalStateMinutesRemaining,
- itemEffectActivityLevelValue = character.characterStats.itemEffectActivityLevelValue,
- itemEffectActivityLevelMinutesRemaining = character.characterStats.itemEffectActivityLevelMinutesRemaining,
- itemEffectVitalPointsChangeValue = character.characterStats.itemEffectVitalPointsChangeValue,
- itemEffectVitalPointsChangeMinutesRemaining = character.characterStats.itemEffectVitalPointsChangeMinutesRemaining,
- abilityRarity = abilityRarity,
- abilityType = character.characterStats.abilityType,
- abilityBranch = character.characterStats.abilityBranch,
- abilityReset = character.characterStats.abilityReset,
- rank = character.characterStats.rank,
- itemType = character.characterStats.itemType,
- itemMultiplier = character.characterStats.itemMultiplier,
- itemRemainingTime = character.characterStats.itemRemainingTime,
- otp0 = "",
- otp1 = "",
- minorVersion = character.characterStats.firmwareMinorVersion,
- majorVersion = character.characterStats.firmwareMajorVersion
- )
- )
- }
-
- val now = System.currentTimeMillis()
- markSeen(importedCard.name, slotId, importedCard.id, now)
-
- var insertedTransformationCount = 0
- for (transformation in character.transformationHistoryList) {
- val transformationCard = resolveRelatedCard(
- incomingCardName = transformation.cardName,
- incomingCardId = character.cardId,
- matchedRootCard = importedCard,
- incomingRootCardName = character.cardName,
- )
- if (transformationCard != null) {
- database.userCharacterDao().insertTransformation(
- userCharacterId,
- transformation.slotId,
- transformationCard.id,
- now
- )
- insertedTransformationCount++
- markSeen(transformationCard.name, transformation.slotId, transformationCard.id, now)
- }
- }
-
- if (insertedTransformationCount == 0) {
- database.userCharacterDao().insertTransformation(
- userCharacterId,
- slotId,
- importedCard.id,
- now
- )
- }
-
- for ((cardName, maxAdventureCompleted) in character.maxAdventureCompletedByCardMap) {
- val adventureCard = resolveRelatedCard(
- incomingCardName = cardName,
- incomingCardId = null,
- matchedRootCard = importedCard,
- incomingRootCardName = character.cardName,
- ) ?: continue
- val currentStage = (maxAdventureCompleted + 1).coerceAtLeast(0)
- database.cardProgressDao().updateCardProgress(
- currentStage = currentStage,
- cardId = adventureCard.id,
- unlocked = currentStage > adventureCard.stageCount
- )
- }
-
- val nextAdventureMissionStage = character.characterStats.nextAdventureMissionStage.coerceAtLeast(0)
- if (nextAdventureMissionStage > 0) {
- database.cardProgressDao().updateCardProgress(
- currentStage = nextAdventureMissionStage,
- cardId = importedCard.id,
- unlocked = nextAdventureMissionStage > importedCard.stageCount,
- )
- }
-
return ImportResult(
success = true,
message = "Imported ${importedCard.name} slot $slotId from VitalWear."
@@ -380,172 +120,26 @@ class VitalWearCharacterImporter(
private fun resolveCard(character: Character) = resolveCard(character.cardName, character.cardId)
- private fun resolveCard(cardName: String?, cardId: Int?): Card? {
- return selectImportedCard(
- candidates = database.cardDao().getAllCards(),
- incomingCardName = cardName,
- incomingCardId = 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 }
+ }
- private fun resolveRelatedCard(
- incomingCardName: String?,
- incomingCardId: Int?,
- matchedRootCard: Card,
- incomingRootCardName: String,
- ): Card? {
- if (incomingCardName.isNullOrBlank()) {
- return if (incomingCardId != null && incomingCardId > 0 && matchedRootCard.cardId == incomingCardId) {
- matchedRootCard
- } else {
- null
+ if (cardId != null) {
+ val matches = database.cardDao().getCardByCardId(cardId)
+ if (matches.size == 1) {
+ return matches.first()
}
}
- if (cardNamesMatch(incomingCardName, incomingRootCardName)) {
- return matchedRootCard
- }
-
- return resolveCard(incomingCardName, incomingCardId)
- }
-
- private fun createPlaceholderCard(character: Character): Card? {
- if (!character.hasTransferCard() || !character.hasTransferSpecies()) {
- return null
- }
-
- val transferCard = character.transferCard
- selectImportedCard(
- candidates = database.cardDao().getAllCards(),
- incomingCardName = transferCard.cardName,
- incomingCardId = transferCard.cardId,
- )?.let { return it }
-
- val transferSpecies = character.transferSpecies
- val baseName = transferCard.cardName
- .takeIf { it.isNotBlank() }
- ?: character.cardName.takeIf { it.isNotBlank() }
- ?: "Transferred Card ${transferCard.cardId}"
- val placeholderName = buildPlaceholderCardName(baseName, transferCard.cardId)
- val logoBytes = when {
- !transferSpecies.nameSprite.isEmpty -> transferSpecies.nameSprite.toByteArray()
- !transferSpecies.idle1.isEmpty -> transferSpecies.idle1.toByteArray()
- else -> byteArrayOf()
- }
- val logoWidth = when {
- transferSpecies.nameSpriteWidth > 0 -> transferSpecies.nameSpriteWidth
- transferSpecies.spriteWidth > 0 -> transferSpecies.spriteWidth
- else -> 0
- }
- val logoHeight = when {
- transferSpecies.nameSpriteHeight > 0 -> transferSpecies.nameSpriteHeight
- transferSpecies.spriteHeight > 0 -> transferSpecies.spriteHeight
- else -> 0
- }
-
- val insertedId = runBlocking {
- database.cardDao().insertNewCard(
- Card(
- cardId = transferCard.cardId,
- logo = logoBytes,
- logoWidth = logoWidth,
- logoHeight = logoHeight,
- name = placeholderName,
- stageCount = transferCard.stageCount.coerceAtLeast(0),
- isBEm = transferCard.isBem,
- )
- )
- }
- val resolvedCard = when {
- insertedId > 0L -> database.cardDao().getCardById(insertedId)
- else -> database.cardDao().getCardByName(placeholderName)
- } ?: return null
-
- if (database.cardProgressDao().getCardProgressSync(resolvedCard.id) == null) {
- database.cardProgressDao().insertCardProgress(
- CardProgress(
- cardId = resolvedCard.id,
- currentStage = 1,
- unlocked = false,
- )
- )
- }
-
- return resolvedCard
- }
-
- private fun createPlaceholderCharacter(card: Card, character: Character): CardCharacter? {
- if (!character.hasTransferSpecies()) {
- return null
- }
-
- val transferSpecies = character.transferSpecies
- database.characterDao().getCharacterByMonIndexOrNull(transferSpecies.slotId, card.id)?.let { return it }
-
- val spriteId = database.spriteDao().insertSprite(
- Sprite(
- spriteIdle1 = transferSpecies.idle1.toByteArray(),
- spriteIdle2 = transferSpecies.idle2.toByteArray(),
- spriteWalk1 = transferSpecies.walk1.toByteArray(),
- spriteWalk2 = transferSpecies.walk2.toByteArray(),
- spriteRun1 = transferSpecies.run1.toByteArray(),
- spriteRun2 = transferSpecies.run2.toByteArray(),
- spriteTrain1 = transferSpecies.train1.toByteArray(),
- spriteTrain2 = transferSpecies.train2.toByteArray(),
- spriteHappy = transferSpecies.win.toByteArray(),
- spriteSleep = transferSpecies.down.toByteArray(),
- spriteAttack = transferSpecies.attack.toByteArray(),
- spriteDodge = transferSpecies.dodge.toByteArray(),
- width = transferSpecies.spriteWidth,
- height = transferSpecies.spriteHeight,
- )
- )
-
- val attribute = enumValues().getOrElse(transferSpecies.attribute) {
- enumValues().first()
- }
-
- runBlocking {
- database.characterDao().insertCharacter(
- CardCharacter(
- cardId = card.id,
- spriteId = spriteId,
- charaIndex = transferSpecies.slotId,
- stage = transferSpecies.stage,
- attribute = attribute,
- baseHp = transferSpecies.baseHp,
- baseBp = transferSpecies.baseBp,
- baseAp = transferSpecies.baseAp,
- nameSprite = transferSpecies.nameSprite.toByteArray(),
- nameWidth = transferSpecies.nameSpriteWidth,
- nameHeight = transferSpecies.nameSpriteHeight,
- )
- )
- }
-
- return database.characterDao().getCharacterByMonIndexOrNull(transferSpecies.slotId, card.id)
- }
-
- private fun buildPlaceholderCardName(baseName: String, cardId: Int): String {
- val trimmedBaseName = baseName.trim().ifBlank { "Transferred Card" }
- database.cardDao().getCardByName(trimmedBaseName)?.let {
- return if (cardId > 0) "$trimmedBaseName [Transfer $cardId]" else "$trimmedBaseName [Transfer]"
- }
- return trimmedBaseName
+ return null
}
private fun secondsToMinutes(seconds: Long): Int {
if (seconds <= 0L) {
return 0
}
- // Preserve non-zero timers from HCE payloads instead of flooring 1..59s to 0.
- return ((seconds + 59L) / 60L).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
- }
-
- private fun hasPossibleTransformations(cardCharacterId: Long): Boolean {
- return runBlocking {
- database.characterDao().getEvolutionRequirementsForCard(cardCharacterId).firstOrNull()?.isNotEmpty() == true
- }
+ return (seconds / 60L).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
}
private fun resolveInjuryStatus(injured: Boolean): NfcCharacter.InjuryStatus {
@@ -566,66 +160,4 @@ class VitalWearCharacterImporter(
private fun resolveDefaultAbilityRarity(): NfcCharacter.AbilityRarity {
return enumValues().first()
}
-
- private fun resolveAbilityRarity(rawValue: Int): NfcCharacter.AbilityRarity {
- val rarities = enumValues()
- return rarities.getOrElse(rawValue) { resolveDefaultAbilityRarity() }
- }
-
- private fun markSeen(cardName: String, slotId: Int, cardId: Long, timestamp: Long) {
- database.dexDao().insertCharacter(slotId, cardId, timestamp)
- transferSeenDao.markSeen(cardName, slotId, timestamp)
- }
}
-
-internal fun selectImportedCard(
- candidates: List,
- incomingCardName: String?,
- incomingCardId: Int?,
-): Card? {
- val requestedName = incomingCardName?.takeIf { it.isNotBlank() }
- val idMatches = if (incomingCardId != null && incomingCardId > 0) {
- candidates.filter { it.cardId == incomingCardId }
- } else {
- emptyList()
- }
-
- requestedName?.let { requestedNameValue ->
- candidates.firstOrNull {
- it.name == requestedNameValue && (incomingCardId == null || incomingCardId <= 0 || it.cardId == incomingCardId)
- }?.let { return it }
-
- val ignoreCaseMatches = candidates.filter {
- it.name.equals(requestedNameValue, ignoreCase = true) &&
- (incomingCardId == null || incomingCardId <= 0 || it.cardId == incomingCardId)
- }
- if (ignoreCaseMatches.size == 1) {
- return ignoreCaseMatches.single()
- }
- }
-
- if (idMatches.size == 1) {
- return idMatches.single()
- }
-
- requestedName?.let { requestedNameValue ->
- val normalizedMatches = candidates.filter {
- cardNamesMatch(it.name, requestedNameValue) &&
- (incomingCardId == null || incomingCardId <= 0 || it.cardId == incomingCardId)
- }
- if (normalizedMatches.size == 1) {
- return normalizedMatches.single()
- }
- }
-
- return null
-}
-
-internal fun cardNamesMatch(left: String, right: String): Boolean {
- return left.equals(right, ignoreCase = true) || left.toNormalizedCardLookupKey() == right.toNormalizedCardLookupKey()
-}
-
-private fun String.toNormalizedCardLookupKey(): String {
- return lowercase().filter { it.isLetterOrDigit() }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/utils/BitmapData.kt b/app/src/main/java/com/github/nacabaro/vbhelper/utils/BitmapData.kt
index a169743..eae66e2 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/utils/BitmapData.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/utils/BitmapData.kt
@@ -42,7 +42,7 @@ fun BitmapData.getImageBitmap(
}
fun BitmapData.getBitmap(): Bitmap {
- return Bitmap.createBitmap(createARGBIntArray(), this.width, this.height, Bitmap.Config.ARGB_8888)
+ return Bitmap.createBitmap(createARGBIntArray(), this.width, this.height, Bitmap.Config.HARDWARE)
}
object ARGBMasks {
@@ -64,7 +64,7 @@ fun BitmapData.getObscuredBitmap(): Bitmap {
argbPixels[i] = BLACK
}
}
- return Bitmap.createBitmap(argbPixels, this.width, this.height, Bitmap.Config.ARGB_8888)
+ return Bitmap.createBitmap(argbPixels, this.width, this.height, Bitmap.Config.HARDWARE)
}
diff --git a/app/src/main/java/com/github/nacabhelper/screens/BattlesScreen.kt b/app/src/main/java/com/github/nacabhelper/screens/BattlesScreen.kt
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/src/main/java/com/github/nacabhelper/screens/BattlesScreen.kt
@@ -0,0 +1 @@
+
\ 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
index 973ab02..263b4d8 100644
--- a/app/src/main/proto/com/github/cfogrady/vitalwear/protos/Character.proto
+++ b/app/src/main/proto/com/github/cfogrady/vitalwear/protos/Character.proto
@@ -7,48 +7,7 @@ message Character {
string card_name = 1;
int32 card_id = 2;
- message TransferCard {
- int32 card_id = 1;
- string card_name = 2;
- bool is_bem = 3;
- int32 stage_count = 4;
- int32 franchise = 5;
- }
-
- message TransferSpecies {
- int32 slot_id = 1;
- int32 stage = 2;
- int32 attribute = 3;
- int32 base_hp = 4;
- int32 base_bp = 5;
- int32 base_ap = 6;
- bytes name_sprite = 7;
- int32 name_sprite_width = 8;
- int32 name_sprite_height = 9;
- bytes idle1 = 10;
- bytes idle2 = 11;
- bytes walk1 = 12;
- bytes walk2 = 13;
- bytes run1 = 14;
- bytes run2 = 15;
- bytes train1 = 16;
- bytes train2 = 17;
- bytes win = 18;
- bytes down = 19;
- bytes attack = 20;
- bytes dodge = 21;
- bytes splash = 22;
- int32 sprite_width = 23;
- int32 sprite_height = 24;
- }
-
message CharacterStats {
- enum TransferDeviceType {
- TRANSFER_DEVICE_TYPE_UNSPECIFIED = 0;
- TRANSFER_DEVICE_TYPE_VB = 1;
- TRANSFER_DEVICE_TYPE_BE = 2;
- }
-
int32 slot_id = 1;
int32 vitals = 2;
int64 training_time_remaining_in_seconds = 3;
@@ -64,29 +23,6 @@ message Character {
int32 total_wins = 13;
int32 current_phase_wins = 14;
int32 mood = 15;
- TransferDeviceType device_type = 16;
- int32 age_in_days = 17;
- int32 activity_level = 18;
- int32 heart_rate_current = 19;
- int32 generation = 20;
- int32 total_trophies = 21;
- int32 next_adventure_mission_stage = 22;
- int32 item_effect_mental_state_value = 23;
- int32 item_effect_mental_state_minutes_remaining = 24;
- int32 item_effect_activity_level_value = 25;
- int32 item_effect_activity_level_minutes_remaining = 26;
- int32 item_effect_vital_points_change_value = 27;
- int32 item_effect_vital_points_change_minutes_remaining = 28;
- int32 ability_rarity = 29;
- int32 ability_type = 30;
- int32 ability_branch = 31;
- int32 ability_reset = 32;
- int32 rank = 33;
- int32 item_type = 34;
- int32 item_multiplier = 35;
- int32 item_remaining_time = 36;
- int32 firmware_minor_version = 37;
- int32 firmware_major_version = 38;
}
CharacterStats character_stats = 3;
@@ -112,8 +48,4 @@ message Character {
repeated TransformationEvent transformation_history = 5;
map max_adventure_completed_by_card = 6;
-
- optional TransferCard transfer_card = 7;
-
- optional TransferSpecies transfer_species = 8;
}
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 8283b4d..fdc0104 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -56,14 +56,6 @@
Ops!
DIM enviado com sucesso!
O NFC precisa estar ativado.
- O dispositivo de destino está cheio (ativo + backup). Remova um personagem antes de enviar.
- Transferência ignorada. O envio só é permitido quando o brinquedo tem um personagem ativo e o slot de backup está vazio.
- Pronto. Encoste e mantenha o celular e o relógio juntos até concluir.
- Conexão detectada. Mantenha os dispositivos juntos...
- Transferindo agora. Não afaste o celular ainda.
- Transferência concluída. Agora você pode afastar o celular.
- Falha na transferência. Reposicione os dispositivos e tente novamente.
- Transferência cancelada.
Configurações
@@ -178,7 +170,7 @@
Nome do personagem
HP: %1$d, BP: %2$d, AP: %3$d
Stg: %1$s, Atr: %2$s
- ________________
+ \?\?\?\?\?\?\?\?\?\?\?\?\?\?\?\?
Stg: -, Atr: -
HP: -, BP: -, AP: -
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 90d71c0..e5221ec 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -60,25 +60,14 @@
Whoops
Sent DIM successfully!
NFC must be enabled
- Target device is full (active + backup). Remove one character before sending.
- Transfer skipped. Send is allowed only when the toy has an active character and an empty backup slot.
- Ready. Tap and hold phone/watch together until complete.
- Connection detected. Keep devices together...
- Transferring now. Do not remove phone yet.
- Transfer complete. You can remove the phone now.
- Transfer failed. Reposition devices and try again.
- Transfer cancelled.
- Detected target: Bandai Vital Bracelet (NFC-A)
- Detected target: VitalWear (HCE)
Settings
NFC Communication
- VBH-VW card management
+ DiM/BEm management
About and credits
Data management
- VitalWear companion tools
Import APK
@@ -87,12 +76,7 @@
Import card
- Import DIM/BEM cards into VBHelper\'s Dex.
-
-
- Scale DIM cards to BEM stats
-
- Automatically upscale DIM card stats to BEM level when importing
+ Import DiM/BEm
Credits
@@ -111,27 +95,6 @@
Import application database
- Import card to VitalWear
- Import Dim/Bem to Vitalwear
- Import firmware to VitalWear
- Import firmware to Watch (10b for now)
- Send watch logs
- Request logs from a connected VitalWear watch and share them
- Send phone logs
- Share the latest VBHelper phone log file
- WARNING: Logs have personal health information such as heart rate measurements and steps per day.
- No log files found
- Failed to send log request to watch
- Error receiving log file
- No connected watch found
- Firmware sent successfully
- Imported Successfully
- Connect to VB
- Insert Card in VB and Reconnect
- Card Validated Successfully
- Select firmware file
- Character transfer is disabled in companion. Use watch Transfer instead.
-
Credits
Reverse engineering
Application development
@@ -216,7 +179,7 @@
Character name
HP: %1$d, BP: %2$d, AP: %3$d
Stg: %1$s, Atr: %2$s
- ________________
+ \?\?\?\?\?\?\?\?\?\?\?\?\?\?\?\?
Stg: -, Atr: -
HP: -, BP: -, AP: -
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
index b4b063a..8a3c842 100644
--- a/app/src/main/res/xml/provider_paths.xml
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -1,8 +1,5 @@
-
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5f078f3..7e4875e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -16,13 +16,7 @@ protobufGradlePlugin = "0.9.6"
protobufJavalite = "4.33.4"
roomRuntime = "2.8.4"
vbNfcReader = "0.2.0-SNAPSHOT"
-dimReader = "2.0.0"
-playServicesWearable = "19.0.0"
-kotlinxCoroutinesPlayServices = "1.4.3-native-mt"
-timber = "5.0.1"
-tinyLog = "2.4.1"
-slf4jTimber = "1.0.0"
-slf4jApi = "2.0.16"
+dimReader = "2.1.0"
[libraries]
androidx-compose-material = { module = "androidx.compose.material:material" }
@@ -52,13 +46,6 @@ protobuf-gradle-plugin = { module = "com.google.protobuf:protobuf-gradle-plugin"
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufJavalite" }
vb-nfc-reader = { module = "com.github.cfogrady:vb-nfc-reader", version.ref = "vbNfcReader" }
dim-reader = { module = "com.github.cfogrady:vb-dim-reader", version.ref = "dimReader" }
-play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" }
-kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesPlayServices" }
-timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
-tiny-log = { module = "org.tinylog:tinylog-api", version.ref = "tinyLog" }
-tiny-log-impl = { module = "org.tinylog:tinylog-impl", version.ref = "tinyLog" }
-slf4j-timber = { module = "at.favre.lib:slf4j-timber", version.ref = "slf4jTimber" }
-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4jApi" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5e8316b..917b611 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -17,16 +17,8 @@ dependencyResolutionManagement {
mavenLocal()
google()
mavenCentral()
- maven { url = uri("https://jitpack.io") }
}
}
rootProject.name = "VBHelper"
include(":app")
-
-
-includeBuild("../vb-nfc-reader") {
- dependencySubstitution {
- substitute(module("com.github.cfogrady:vb-nfc-reader")).using(project(":"))
- }
-}