Compare commits

...
13 Commits
Author SHA1 Message Date
nacabaro e5ccff16d0 Pulidora industrial
- Removed all the warnings during building
- Upgraded all the libraries
- Upgraded grandle and the SDK version
- Fix japanese strings
- Added vibration permission
2026-06-29 16:15:12 +00:00
nacabaroandGitHub 4b866aad8a Merge pull request #57 from b13st/fix/special-missions-nan-crash
Fix special missions crash and NaN% stat display
2026-06-24 23:05:51 +02:00
nacabaroandGitHub 33ad2728b8 Merge pull request #56 from b13st/fix/hce-character-cloning
Fix/hce character cloning
2026-06-24 23:04:53 +02:00
b13st 05f04fdc49 Fix LaunchedEffect key so characterToNfc runs when active character loads
When launching the scan screen from the home button, characterId is
loaded asynchronously from the active character. The previous key
(storageRepository) never re-triggered the effect, so characterToNfc
(which sets pendingExportCharacterId) was skipped if characterId was
null on first composition, making the HCE delete unreachable.
2026-06-24 15:40:19 +02:00
b13st 07315fe11b Delete character from app after successful HCE transfer to VitalWear watch
When sending a character to the VitalWear watch via HCE, the transfer
is atomic (single NFC interaction). The character was being left in the
app after transfer, allowing it to be cloned indefinitely.

Delete the character from the database immediately after sendCharacterToWatch
succeeds, matching the behavior of physical bracelet transfers which delete
the character once both write phases complete.
2026-06-24 15:29:58 +02:00
b13st 2e72b543f6 Fix special missions crash and NaN% stat display
- Fix crash when tapping a completed special mission: onClickCollect
  was ignoring the missionId passed by SpecialMissionsEntry and using
  selectedSpecialMissionId (-1 by default), causing Room to query id=-1
  on a non-nullable Flow which throws when no row exists.
- Fix NaN% in current phase win rate: the guard was checking
  totalBattlesLost==0 but dividing by currentPhaseBattles, producing
  0/0=NaN when total>0 but current phase has no battles yet.
- Guard against empty items list in clearSpecialMission to avoid
  NoSuchElementException from random() on an empty collection.
2026-06-24 15:24:54 +02:00
nacabaroandGitHub 81bfec5ac5 Merge pull request #55 from b13st/fix/vitalwear-hce-transfer
Fix VitalWear HCE transfer in both directions
2026-06-23 20:30:46 +00:00
nacabaroandGitHub 8ec98b9d39 Merge pull request #54 from shiki8826/main
Thank you very much
2026-06-23 20:30:04 +00:00
b13st e549fb4469 Fix VitalWear HCE transfer in both directions
- Detect ISO-DEP tags (VitalWear watch) vs NFC-A (physical bracelets)
- Watch->Phone: use VitalWearHceReaderClient + VitalWearCharacterImporter
- Phone->Watch: use VitalWearHceReaderClient + VitalWearCharacterExporter
- Add AppDatabase to ScanScreenControllerImpl for HCE import/export
- Extract buildCharacterProto() from VitalWearCharacterExporter
- Add JitPack repository to settings.gradle.kts
2026-06-23 13:44:26 +02:00
shiki8826 8d62437aae 日本語翻訳の修正及び追加項目の翻訳 2026-06-19 20:46:23 +09:00
nacabaro 7a8815694d Remove test file, i'm not figuring this out tonight 2026-06-18 22:44:14 +00:00
nacabaro 464f2e48a7 Add basic migration to support the vitalwear thing 2026-06-18 22:37:51 +00:00
nacabaro f641e8aaa0 Compile fix before merging 2026-06-18 22:17:52 +00:00
43 changed files with 1005 additions and 697 deletions
+14 -21
View File
@@ -2,22 +2,21 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp") version "2.3.0"
id("com.google.protobuf")
alias(libs.plugins.vksp)
alias(libs.plugins.vprotobuf)
}
android {
namespace = "com.github.nacabaro.vbhelper"
compileSdk = 36
compileSdk = 37
defaultConfig {
applicationId = "com.github.nacabaro.vbhelper"
minSdk = 28
targetSdk = 36
targetSdk = 37
versionCode = 1
versionName = "Alpha 0.6.3"
versionName = "Alpha 0.6.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -56,9 +55,6 @@ protobuf {
artifact = "com.google.protobuf:protoc:4.27.0"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().forEach { task ->
task.builtins {
@@ -90,6 +86,11 @@ 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.tinylog.api)
implementation(libs.tinylog.impl)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
@@ -104,18 +105,10 @@ dependencies {
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation("androidx.navigation:navigation-compose:2.7.0")
implementation("com.google.android.material:material:1.2.0")
implementation(libs.protobuf.javalite)
implementation("androidx.compose.material:material")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.gson)
// HTTP request logging
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation(libs.okhttp.logging.interceptor)
}
@@ -1,439 +0,0 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
import android.nfc.IsoDep
import android.nfc.NfcA
import android.nfc.Tag
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import org.junit.Test
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever
import org.mockito.kotlin.any
import org.mockito.kotlin.verify
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
/**
* Instrumented tests for NFC-A (Bandai toy) transfer functionality.
*
* These tests verify:
* - Transport detection (NFC-A vs ISO-DEP)
* - Read operations (Watch to VBH)
* - Write operations (VBH to Watch)
* - Slot state detection
* - Error handling
*
* Run with:
* ./gradlew connectedAndroidTest --tests ScanNfcaIntegrationTest
*/
@RunWith(AndroidJUnit4::class)
class ScanNfcaIntegrationTest {
@Mock
private lateinit var mockTag: Tag
@Mock
private lateinit var mockNfcA: NfcA
@Mock
private lateinit var mockIsoDep: IsoDep
@Mock
private lateinit var mockTagCommunicator: TagCommunicator
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
}
// ==========================================
// TEST GROUP 1: Transport Detection
// ==========================================
/**
* TEST 1.1: Verify NFC-A is correctly detected
*
* When: Tag with NFC-A support is detected
* Then: Transport should be NFC_A
*/
@Test
fun testNfcATransportDetection() {
// Arrange
whenever(NfcA.get(mockTag)).thenReturn(mockNfcA)
whenever(IsoDep.get(mockTag)).thenReturn(null)
// Act
val nfcA = NfcA.get(mockTag)
val isoDep = IsoDep.get(mockTag)
// Assert
assertNotNull(nfcA, "NfcA should be detected")
assertNull(isoDep, "IsoDep should not be present")
}
/**
* TEST 1.2: Verify ISO-DEP vs NFC-A detection priority
*
* When: Both NFC-A and ISO-DEP present (shouldn't happen, but test anyway)
* Then: ISO-DEP should take priority if it's VitalWear
*/
@Test
fun testIsoDepPriorityOverNfcA() {
// Arrange
whenever(NfcA.get(mockTag)).thenReturn(mockNfcA)
whenever(IsoDep.get(mockTag)).thenReturn(mockIsoDep)
// Act
val isoDep = IsoDep.get(mockTag)
val nfcA = NfcA.get(mockTag)
// Assert
// In actual code, ISO-DEP is checked first
assertNotNull(isoDep, "ISO-DEP should be detected first")
assertNotNull(nfcA, "NFC-A should also be present")
}
// ==========================================
// TEST GROUP 2: Slot State Detection
// ==========================================
/**
* TEST 2.1: Verify device not full detection
*
* When: Device has active=true, backup=false
* Then: Should allow write operation
*/
@Test
fun testSlotStateAllowsWriteWhenNotFull() {
// Arrange: Create mock state - device not full
val slotState = NfcASlotState(
count = 2,
activePresent = true,
backupPresent = false
)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(false, isFull, "Device should not be full")
}
/**
* TEST 2.2: Verify device full detection
*
* When: Device has active=true, backup=true
* Then: Should block write operation
*/
@Test
fun testSlotStateBlocksWriteWhenFull() {
// Arrange: Create mock state - device full
val slotState = NfcASlotState(
count = 2,
activePresent = true,
backupPresent = true
)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(true, isFull, "Device should be full")
}
/**
* TEST 2.3: Verify count-based full detection
*
* When: Device reports count >= 2
* Then: Should be considered full
*/
@Test
fun testSlotStateFullByCount() {
// Arrange
val slotStateFull = NfcASlotState(count = 2, activePresent = null, backupPresent = null)
val slotStateNotFull = NfcASlotState(count = 1, activePresent = null, backupPresent = null)
// Act
val isFullCount2 = slotStateFull.isFull()
val isFullCount1 = slotStateNotFull.isFull()
// Assert
assertEquals(true, isFullCount2, "Count >= 2 should be full")
assertEquals(false, isFullCount1, "Count < 2 should not be full")
}
/**
* TEST 2.4: Verify fallback when introspection fails
*
* When: Cannot introspect slot state (all null)
* Then: Should default to non-blocking path
*/
@Test
fun testSlotStateFallbackWhenUnknown() {
// Arrange: No slot info available
val slotState = NfcASlotState(count = null, activePresent = null, backupPresent = null)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(false, isFull, "Unknown state should not block (default to false)")
}
// ==========================================
// TEST GROUP 3: Character Conversion
// ==========================================
/**
* TEST 3.1: Verify NFC character to DB character conversion
*
* When: VBNfcCharacter is read from toy
* Then: Should convert to database format correctly
*/
@Test
fun testNfcCharacterConversionVB() {
// Arrange
val vbNfcCharacter = VBNfcCharacter()
// In real test, would populate with actual data
// Act
// This would call FromNfcConverter.addCharacter()
// For now, just verify the object exists
assertNotNull(vbNfcCharacter, "VBNfcCharacter should be created")
}
// ==========================================
// TEST GROUP 4: Write Operation Validation
// ==========================================
/**
* TEST 4.1: Verify write is blocked when device full
*
* Scenario:
* - User selects character to write
* - Device has 2 active+backup (full)
* - Click "VBH to Watch"
*
* Expected:
* - WriteResult.BLOCKED_DEVICE_FULL
* - No transfer occurs
*/
@Test
fun testWriteBlockedWhenDeviceFull() {
// Arrange
val slotState = NfcASlotState(
count = 2,
activePresent = true,
backupPresent = true
)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(true, isFull, "Should block when full")
// In real test, would verify WriteResult.BLOCKED_DEVICE_FULL returned
}
/**
* TEST 4.2: Verify write allowed when slot available
*
* Scenario:
* - User selects character to write
* - Device has active only (backup empty)
* - Click "VBH to Watch"
*
* Expected:
* - WriteResult.MOVE_CONFIRMED
* - Active→Backup migration performed
* - Character transferred
*/
@Test
fun testWriteAllowedWhenSlotAvailable() {
// Arrange
val slotState = NfcASlotState(
count = 2,
activePresent = true,
backupPresent = false
)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(false, isFull, "Should allow when slot available")
}
/**
* TEST 4.3: Verify write rejected when no active character
*
* Scenario:
* - Device only has backup (no active)
* - Click "VBH to Watch"
*
* Expected:
* - WriteResult.COPIED
* - Transfer skipped
*/
@Test
fun testWriteSkippedWhenNoActiveCharacter() {
// Arrange: Only backup, no active
val slotState = NfcASlotState(
count = 1,
activePresent = false,
backupPresent = true
)
// Act
val isValidForWrite = slotState.activePresent == true && slotState.backupPresent == false
// Assert
assertEquals(false, isValidForWrite, "Should reject write when no active")
}
// ==========================================
// TEST GROUP 5: Debouncing
// ==========================================
/**
* TEST 5.1: Verify rapid re-taps are debounced
*
* Scenario:
* - User taps toy for read
* - User immediately taps again (< 1.5 sec)
*
* Expected:
* - Only first tap processes
* - Second tap ignored
* - Database contains only 1 character
*/
@Test
fun testRapidTapDebouncing() {
// Arrange
val TAG_DEBOUNCE_MS = 1500L
val now = System.currentTimeMillis()
val firstTapTime = now
val secondTapTime = now + 500 // 500ms later (within debounce)
// Act
val timeDifference = secondTapTime - firstTapTime
val isWithinDebounce = timeDifference < TAG_DEBOUNCE_MS
// Assert
assertEquals(true, isWithinDebounce, "Second tap should be debounced")
}
/**
* TEST 5.2: Verify tap after debounce window is processed
*
* Scenario:
* - User taps toy for read
* - User taps again after 2 seconds (> 1.5 sec)
*
* Expected:
* - First tap processes
* - Second tap also processes
* - Database contains 2 characters
*/
@Test
fun testTapAllowedAfterDebounceWindow() {
// Arrange
val TAG_DEBOUNCE_MS = 1500L
val now = System.currentTimeMillis()
val firstTapTime = now
val secondTapTime = now + 2000 // 2 seconds later (outside debounce)
// Act
val timeDifference = secondTapTime - firstTapTime
val isWithinDebounce = timeDifference < TAG_DEBOUNCE_MS
// Assert
assertEquals(false, isWithinDebounce, "Second tap should be processed")
}
// ==========================================
// TEST GROUP 6: Error Handling
// ==========================================
/**
* TEST 6.1: Verify graceful handling of corrupt character data
*
* Scenario:
* - Toy has corrupted character data
* - User clicks "Watch to VBH"
* - Tap toy
*
* Expected:
* - Exception caught
* - Toast shown: "Whoops"
* - App doesn't crash
* - User can retry
*/
@Test
fun testCorruptCharacterHandling() {
// This is more of an integration test
// Would require mocking TagCommunicator.receiveCharacter() to throw
// Pseudocode:
// whenever(mockTagCommunicator.receiveCharacter())
// .thenThrow(IOException("Corrupt data"))
// In real test:
// Call onClickRead()
// Verify: Toast shown, no crash, app recovers
}
/**
* TEST 6.2: Verify NFC connection timeout handling
*
* Scenario:
* - User starts read/write
* - Remove toy during transfer
*
* Expected:
* - Connection timeout
* - Toast: "Whoops"
* - Reader mode disabled
* - App recovers
*/
@Test
fun testNfcConnectionTimeoutHandling() {
// Pseudocode:
// whenever(mockNfcA.connect())
// .thenThrow(IOException("NFC connection lost"))
// In real test:
// Verify timeout is caught, UI updates, no crash
}
// ==========================================
// Helper Classes (from ScanScreenControllerImpl)
// ==========================================
/**
* Mirrors the NfcASlotState data class from ScanScreenControllerImpl.kt
*/
private data class NfcASlotState(
val count: Int?,
val activePresent: Boolean?,
val backupPresent: Boolean?,
) {
fun isFull(): Boolean {
if (count != null) {
return count >= 2
}
if (activePresent != null && backupPresent != null) {
return activePresent && backupPresent
}
return false
}
}
}
+5 -3
View File
@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
@@ -34,7 +35,6 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.VBHelper">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -51,8 +51,10 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="localhost" android:port="8080" android:pathPrefix="/authenticate" />
<data android:scheme="http" android:host="127.0.0.1" android:port="8080" android:pathPrefix="/authenticate" />
<data android:scheme="http" />
<data android:host="localhost" android:port="8080" />
<data android:host="127.0.0.1" android:port="8080" />
<data android:pathPrefix="/authenticate" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
@@ -49,7 +49,8 @@ class MainActivity : ComponentActivity() {
application.container.dataStoreSecretsRepository.secretsFlow,
this,
this::registerActivityLifecycleListener,
this::unregisterActivityLifecycleListener
this::unregisterActivityLifecycleListener,
application.container.db,
)
val settingsScreenController = SettingsScreenControllerImpl(this)
val itemsScreenController = ItemsScreenControllerImpl(this)
@@ -2,11 +2,13 @@ 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
@Dao
@RewriteQueriesToDropUnusedColumns
interface AdventureDao {
@Query("""
INSERT INTO Adventure (characterId, originalDuration, finishesAdventure)
@@ -13,6 +13,9 @@ interface CardDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertNewCard(card: Card): Long
@Query("SELECT * FROM Card")
fun getAllCards(): List<Card>
@Query("SELECT * FROM Card WHERE cardId = :id")
fun getCardByCardId(id: Int): List<Card>
@@ -2,11 +2,13 @@ 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
@Dao
@RewriteQueriesToDropUnusedColumns
interface CardFusionsDao {
@Query("""
INSERT INTO
@@ -5,6 +5,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Upsert
import androidx.room.RewriteQueriesToDropUnusedColumns
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
@@ -16,6 +17,7 @@ import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@Dao
@RewriteQueriesToDropUnusedColumns
interface UserCharacterDao {
@Insert
fun insertCharacterData(characterData: UserCharacter): Long
@@ -131,6 +133,9 @@ interface UserCharacterDao {
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
fun getVbData(id: Long): Flow<VBCharacterData>
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
suspend fun getVbDataOrNull(id: Long): VBCharacterData?
@Query("SELECT * FROM SpecialMissions WHERE characterId = :id")
fun getSpecialMissions(id: Long): Flow<List<SpecialMissions>>
@@ -2,6 +2,8 @@ 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
@@ -13,6 +15,10 @@ 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.daos.CharacterTransferPolicyDao
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardDao
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardEntity
import com.github.nacabaro.vbhelper.domain.card.Background
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.card.Card
@@ -29,10 +35,13 @@ 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.device_data.CharacterTransferPolicy
import com.github.nacabaro.vbhelper.domain.items.Items
@Database(
version = 1,
version = 3,
exportSchema = false,
entities = [
Card::class,
CardProgress::class,
@@ -50,7 +59,10 @@ import com.github.nacabaro.vbhelper.domain.items.Items
Items::class,
Adventure::class,
Background::class,
PossibleTransformations::class
PossibleTransformations::class,
ValidatedCardEntity::class,
VitalWearCharacterSettings::class,
CharacterTransferPolicy::class
]
)
abstract class AppDatabase : RoomDatabase() {
@@ -65,4 +77,35 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun specialMissionDao(): SpecialMissionDao
abstract fun cardAdventureDao(): CardAdventureDao
abstract fun cardFusionsDao(): CardFusionsDao
abstract fun validatedCardDao(): ValidatedCardDao
abstract fun vitalWearSettingsDao(): VitalWearSettingsDao
abstract fun characterTransferPolicyDao(): CharacterTransferPolicyDao
companion object {
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// Create new tables
db.execSQL("CREATE TABLE IF NOT EXISTS `Items` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `itemIcon` INTEGER NOT NULL, `itemLength` INTEGER NOT NULL, `price` INTEGER NOT NULL, `quantity` INTEGER NOT NULL, `itemType` TEXT NOT NULL, PRIMARY KEY(`id`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `SpecialMissions` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `characterId` INTEGER NOT NULL, `goal` INTEGER NOT NULL, `watchId` INTEGER NOT NULL, `progress` INTEGER NOT NULL, `status` TEXT NOT NULL, `timeElapsedInMinutes` INTEGER NOT NULL, `timeLimitInMinutes` INTEGER NOT NULL, `missionType` TEXT NOT NULL, FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `VBCharacterData` (`id` INTEGER NOT NULL, `generation` INTEGER NOT NULL, `totalTrophies` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `TransformationHistory` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monId` INTEGER NOT NULL, `stageId` INTEGER NOT NULL, `transformationDate` INTEGER NOT NULL, FOREIGN KEY(`monId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stageId`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `VitalsHistory` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `charId` INTEGER NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `vitalPoints` INTEGER NOT NULL, FOREIGN KEY(`charId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `Dex` (`id` INTEGER NOT NULL, `discoveredOn` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `Adventure` (`characterId` INTEGER NOT NULL, `originalDuration` INTEGER NOT NULL, `finishesAdventure` INTEGER NOT NULL, PRIMARY KEY(`characterId`), FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `Background` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cardId` INTEGER NOT NULL, `background` BLOB NOT NULL, `backgroundWidth` INTEGER NOT NULL, `backgroundHeight` INTEGER NOT NULL, FOREIGN KEY(`cardId`) REFERENCES `Card`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `PossibleTransformations` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `charaId` INTEGER NOT NULL, `requiredVitals` INTEGER NOT NULL, `requiredTrophies` INTEGER NOT NULL, `requiredBattles` INTEGER NOT NULL, `requiredWinRate` INTEGER NOT NULL, `changeTimerHours` INTEGER NOT NULL, `requiredAdventureLevelCompleted` INTEGER NOT NULL, `toCharaId` INTEGER, FOREIGN KEY(`charaId`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`toCharaId`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `validated` (`cardId` INTEGER NOT NULL, PRIMARY KEY(`cardId`))")
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`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `CharacterTransferPolicy` (`characterId` INTEGER NOT NULL, `nativeDeviceType` TEXT NOT NULL, `preferredHceExportFormat` TEXT NOT NULL, `preferredNfcaExportFormat` TEXT NOT NULL, `lastObservedImportFormat` TEXT, `lastTransferTransport` TEXT, `lastTransferTarget` TEXT, `preserveVbRoundTrip` INTEGER NOT NULL, `preserveBeRoundTrip` INTEGER NOT NULL, PRIMARY KEY(`characterId`), FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
// Add columns to existing tables
try {
db.execSQL("ALTER TABLE `UserCharacter` ADD COLUMN `characterType` TEXT NOT NULL DEFAULT 'BEDevice'")
} catch (e: Exception) {}
try {
db.execSQL("ALTER TABLE `Card` ADD COLUMN `isBEm` INTEGER NOT NULL DEFAULT 0")
} catch (e: Exception) {}
}
}
}
}
@@ -3,9 +3,13 @@ package com.github.nacabaro.vbhelper.di
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.source.CurrencyRepository
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
interface AppContainer {
val db: AppDatabase
val dataStoreSecretsRepository: DataStoreSecretsRepository
val currencyRepository: CurrencyRepository
val validatedCardManager: ValidatedCardManager
val companionLogService: CompanionLogService
}
@@ -8,6 +8,8 @@ import com.github.nacabaro.vbhelper.database.AppDatabase
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.companion.validation.ValidatedCardManager
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
import com.github.nacabaro.vbhelper.source.SecretsSerializer
import com.github.nacabaro.vbhelper.source.proto.Secrets
@@ -30,6 +32,7 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
klass = AppDatabase::class.java,
"internalDb"
)
.addMigrations(AppDatabase.MIGRATION_1_2)
.createFromAsset("items.db")
.build()
}
@@ -37,5 +40,11 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
override val dataStoreSecretsRepository = DataStoreSecretsRepository(context.secretsStore)
override val currencyRepository = CurrencyRepository(context.currencyStore)
override val validatedCardManager by lazy {
ValidatedCardManager(db.validatedCardDao())
}
override val companionLogService = CompanionLogService()
}
@@ -2,10 +2,18 @@ package com.github.nacabaro.vbhelper.di
import DefaultAppContainer
import android.app.Application
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
class VBHelper : Application() {
lateinit var container: DefaultAppContainer
val validatedCardManager: ValidatedCardManager
get() = container.validatedCardManager
val companionLogService: CompanionLogService
get() = container.companionLogService
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer(applicationContext)
@@ -1,6 +1,7 @@
package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.Index
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@@ -12,7 +13,8 @@ import androidx.room.PrimaryKey
childColumns = ["cardId"],
onDelete = ForeignKey.CASCADE
)
]
],
indices = [Index(value = ["cardId"])]
)
data class Background (
@PrimaryKey(autoGenerate = true) val id: Long,
@@ -20,4 +22,28 @@ data class Background (
val background: ByteArray,
val backgroundWidth: Int,
val backgroundHeight: Int
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Background
if (id != other.id) return false
if (cardId != other.cardId) return false
if (!background.contentEquals(other.background)) return false
if (backgroundWidth != other.backgroundWidth) return false
if (backgroundHeight != other.backgroundHeight) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + cardId.hashCode()
result = 31 * result + background.contentHashCode()
result = 31 * result + backgroundWidth
result = 31 * result + backgroundHeight
return result
}
}
@@ -14,4 +14,34 @@ data class Card(
val name: String,
val stageCount: Int,
val isBEm: Boolean
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Card
if (id != other.id) return false
if (cardId != other.cardId) return false
if (!logo.contentEquals(other.logo)) return false
if (logoWidth != other.logoWidth) return false
if (logoHeight != other.logoHeight) return false
if (name != other.name) return false
if (stageCount != other.stageCount) return false
if (isBEm != other.isBEm) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + cardId
result = 31 * result + logo.contentHashCode()
result = 31 * result + logoWidth
result = 31 * result + logoHeight
result = 31 * result + name.hashCode()
result = 31 * result + stageCount
result = 31 * result + isBEm.hashCode()
return result
}
}
@@ -1,6 +1,7 @@
package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.Index
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@@ -18,6 +19,10 @@ import androidx.room.PrimaryKey
childColumns = ["cardId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index(value = ["characterId"]),
Index(value = ["cardId"])
]
)
data class CardAdventure(
@@ -1,6 +1,7 @@
package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.Index
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter
@@ -20,6 +21,10 @@ import com.github.nacabaro.vbhelper.domain.characters.Sprite
childColumns = ["spriteId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index(value = ["cardId"]),
Index(value = ["spriteId"])
]
)
@@ -41,4 +46,42 @@ data class CardCharacter (
val nameSprite: ByteArray,
val nameWidth: Int,
val nameHeight: Int
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CardCharacter
if (id != other.id) return false
if (cardId != other.cardId) return false
if (spriteId != other.spriteId) return false
if (charaIndex != other.charaIndex) return false
if (stage != other.stage) return false
if (attribute != other.attribute) return false
if (baseHp != other.baseHp) return false
if (baseBp != other.baseBp) return false
if (baseAp != other.baseAp) return false
if (!nameSprite.contentEquals(other.nameSprite)) return false
if (nameWidth != other.nameWidth) return false
if (nameHeight != other.nameHeight) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + cardId.hashCode()
result = 31 * result + spriteId.hashCode()
result = 31 * result + charaIndex
result = 31 * result + stage
result = 31 * result + attribute.hashCode()
result = 31 * result + baseHp
result = 31 * result + baseBp
result = 31 * result + baseAp
result = 31 * result + nameSprite.contentHashCode()
result = 31 * result + nameWidth
result = 31 * result + nameHeight
return result
}
}
@@ -1,6 +1,7 @@
package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.Index
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter
@@ -19,6 +20,10 @@ import com.github.cfogrady.vbnfc.data.NfcCharacter
childColumns = ["toCharaId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index(value = ["fromCharaId"]),
Index(value = ["toCharaId"])
]
)
data class CardFusions(
@@ -1,6 +1,7 @@
package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.Index
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@@ -18,6 +19,10 @@ import androidx.room.PrimaryKey
childColumns = ["toCharaId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index(value = ["charaId"]),
Index(value = ["toCharaId"])
]
)
data class PossibleTransformations (
@@ -20,4 +20,48 @@ data class Sprite(
val spriteDodge: ByteArray,
val width: Int,
val height: Int
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Sprite
if (id != other.id) return false
if (!spriteIdle1.contentEquals(other.spriteIdle1)) return false
if (!spriteIdle2.contentEquals(other.spriteIdle2)) return false
if (!spriteWalk1.contentEquals(other.spriteWalk1)) return false
if (!spriteWalk2.contentEquals(other.spriteWalk2)) return false
if (!spriteRun1.contentEquals(other.spriteRun1)) return false
if (!spriteRun2.contentEquals(other.spriteRun2)) return false
if (!spriteTrain1.contentEquals(other.spriteTrain1)) return false
if (!spriteTrain2.contentEquals(other.spriteTrain2)) return false
if (!spriteHappy.contentEquals(other.spriteHappy)) return false
if (!spriteSleep.contentEquals(other.spriteSleep)) return false
if (!spriteAttack.contentEquals(other.spriteAttack)) return false
if (!spriteDodge.contentEquals(other.spriteDodge)) return false
if (width != other.width) return false
if (height != other.height) return false
return true
}
override fun hashCode(): Int {
var result = id
result = 31 * result + spriteIdle1.contentHashCode()
result = 31 * result + spriteIdle2.contentHashCode()
result = 31 * result + spriteWalk1.contentHashCode()
result = 31 * result + spriteWalk2.contentHashCode()
result = 31 * result + spriteRun1.contentHashCode()
result = 31 * result + spriteRun2.contentHashCode()
result = 31 * result + spriteTrain1.contentHashCode()
result = 31 * result + spriteTrain2.contentHashCode()
result = 31 * result + spriteHappy.contentHashCode()
result = 31 * result + spriteSleep.contentHashCode()
result = 31 * result + spriteAttack.contentHashCode()
result = 31 * result + spriteDodge.contentHashCode()
result = 31 * result + width
result = 31 * result + height
return result
}
}
@@ -1,6 +1,7 @@
package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity
import androidx.room.Index
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.vb.SpecialMission
@@ -13,7 +14,8 @@ import com.github.cfogrady.vbnfc.vb.SpecialMission
childColumns = ["characterId"],
onDelete = ForeignKey.CASCADE
)
]
],
indices = [Index(value = ["characterId"])]
)
data class SpecialMissions (
@PrimaryKey(autoGenerate = true) var id: Long = 0,
@@ -1,6 +1,7 @@
package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity
import androidx.room.Index
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
@@ -19,6 +20,10 @@ import com.github.nacabaro.vbhelper.domain.card.CardCharacter
childColumns = ["stageId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index(value = ["monId"]),
Index(value = ["stageId"])
]
)
data class TransformationHistory (
@@ -1,6 +1,7 @@
package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity
import androidx.room.Index
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter
@@ -15,7 +16,8 @@ import com.github.nacabaro.vbhelper.domain.card.CardCharacter
childColumns = ["charId"],
onDelete = ForeignKey.CASCADE
)
]
],
indices = [Index(value = ["charId"])]
)
/**
* UserCharacter represents and instance of a character. The charId should map to a Character
@@ -1,6 +1,7 @@
package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity
import androidx.room.Index
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@@ -12,7 +13,8 @@ import androidx.room.PrimaryKey
childColumns = ["charId"],
onDelete = ForeignKey.CASCADE
)
]
],
indices = [Index(value = ["charId"])]
)
data class VitalsHistory (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@@ -9,7 +9,35 @@ object CardDtos {
val logoHeight: Int,
val totalCharacters: Int,
val obtainedCharacters: Int,
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CardProgress
if (cardId != other.cardId) return false
if (cardName != other.cardName) return false
if (!cardLogo.contentEquals(other.cardLogo)) return false
if (logoWidth != other.logoWidth) return false
if (logoHeight != other.logoHeight) return false
if (totalCharacters != other.totalCharacters) return false
if (obtainedCharacters != other.obtainedCharacters) return false
return true
}
override fun hashCode(): Int {
var result = cardId.hashCode()
result = 31 * result + cardName.hashCode()
result = 31 * result + cardLogo.contentHashCode()
result = 31 * result + logoWidth
result = 31 * result + logoHeight
result = 31 * result + totalCharacters
result = 31 * result + obtainedCharacters
return result
}
}
data class CardAdventureWithSprites (
val characterName: ByteArray,
@@ -23,11 +51,67 @@ object CardDtos {
val characterDp: Int,
val characterHp: Int,
val steps: Int,
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CardAdventureWithSprites
if (!characterName.contentEquals(other.characterName)) return false
if (characterNameWidth != other.characterNameWidth) return false
if (characterNameHeight != other.characterNameHeight) return false
if (!characterIdleSprite.contentEquals(other.characterIdleSprite)) return false
if (characterIdleSpriteWidth != other.characterIdleSpriteWidth) return false
if (characterIdleSpriteHeight != other.characterIdleSpriteHeight) return false
if (characterAp != other.characterAp) return false
if (characterBp != other.characterBp) return false
if (characterDp != other.characterDp) return false
if (characterHp != other.characterHp) return false
if (steps != other.steps) return false
return true
}
override fun hashCode(): Int {
var result = characterName.contentHashCode()
result = 31 * result + characterNameWidth
result = 31 * result + characterNameHeight
result = 31 * result + characterIdleSprite.contentHashCode()
result = 31 * result + characterIdleSpriteWidth
result = 31 * result + characterIdleSpriteHeight
result = 31 * result + characterAp
result = 31 * result + (characterBp ?: 0)
result = 31 * result + characterDp
result = 31 * result + characterHp
result = 31 * result + steps
return result
}
}
data class CardIcon (
val cardIcon: ByteArray,
val cardIconWidth: Int,
val cardIconHeight: Int
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CardIcon
if (!cardIcon.contentEquals(other.cardIcon)) return false
if (cardIconWidth != other.cardIconWidth) return false
if (cardIconHeight != other.cardIconHeight) return false
return true
}
override fun hashCode(): Int {
var result = cardIcon.contentHashCode()
result = 31 * result + cardIconWidth
result = 31 * result + cardIconHeight
return result
}
}
}
@@ -33,7 +33,75 @@ object CharacterDtos {
val isBemCard: Boolean,
val isInAdventure: Boolean,
val active: Boolean
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CharacterWithSprites
if (id != other.id) return false
if (charId != other.charId) return false
if (stage != other.stage) return false
if (attribute != other.attribute) return false
if (ageInDays != other.ageInDays) return false
if (mood != other.mood) return false
if (vitalPoints != other.vitalPoints) return false
if (transformationCountdown != other.transformationCountdown) return false
if (injuryStatus != other.injuryStatus) return false
if (trophies != other.trophies) return false
if (currentPhaseBattlesWon != other.currentPhaseBattlesWon) return false
if (currentPhaseBattlesLost != other.currentPhaseBattlesLost) return false
if (totalBattlesWon != other.totalBattlesWon) return false
if (totalBattlesLost != other.totalBattlesLost) return false
if (activityLevel != other.activityLevel) return false
if (heartRateCurrent != other.heartRateCurrent) return false
if (characterType != other.characterType) return false
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
if (!spriteIdle2.contentEquals(other.spriteIdle2)) return false
if (spriteWidth != other.spriteWidth) return false
if (spriteHeight != other.spriteHeight) return false
if (!nameSprite.contentEquals(other.nameSprite)) return false
if (nameSpriteWidth != other.nameSpriteWidth) return false
if (nameSpriteHeight != other.nameSpriteHeight) return false
if (isBemCard != other.isBemCard) return false
if (isInAdventure != other.isInAdventure) return false
if (active != other.active) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + charId.hashCode()
result = 31 * result + stage
result = 31 * result + attribute.hashCode()
result = 31 * result + ageInDays
result = 31 * result + mood
result = 31 * result + vitalPoints
result = 31 * result + transformationCountdown
result = 31 * result + injuryStatus.hashCode()
result = 31 * result + trophies
result = 31 * result + currentPhaseBattlesWon
result = 31 * result + currentPhaseBattlesLost
result = 31 * result + totalBattlesWon
result = 31 * result + totalBattlesLost
result = 31 * result + activityLevel
result = 31 * result + heartRateCurrent
result = 31 * result + characterType.hashCode()
result = 31 * result + spriteIdle.contentHashCode()
result = 31 * result + spriteIdle2.contentHashCode()
result = 31 * result + spriteWidth
result = 31 * result + spriteHeight
result = 31 * result + nameSprite.contentHashCode()
result = 31 * result + nameSpriteWidth
result = 31 * result + nameSpriteHeight
result = 31 * result + isBemCard.hashCode()
result = 31 * result + isInAdventure.hashCode()
result = 31 * result + active.hashCode()
return result
}
}
data class CardCharacterInfo(
val cardId: Long,
@@ -50,7 +118,33 @@ object CharacterDtos {
val spriteHeight: Int,
val monIndex: Int,
val transformationDate: Long
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TransformationHistory
if (id != other.id) return false
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
if (spriteWidth != other.spriteWidth) return false
if (spriteHeight != other.spriteHeight) return false
if (monIndex != other.monIndex) return false
if (transformationDate != other.transformationDate) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + spriteIdle.contentHashCode()
result = 31 * result + spriteWidth
result = 31 * result + spriteHeight
result = 31 * result + monIndex
result = 31 * result + transformationDate.hashCode()
return result
}
}
data class TransformationHistoryExport(
val stageId: Long,
@@ -73,14 +167,76 @@ object CharacterDtos {
val baseAp: Int,
val stage: Int,
val attribute: NfcCharacter.Attribute,
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CardCharaProgress
if (id != other.id) return false
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
if (spriteWidth != other.spriteWidth) return false
if (spriteHeight != other.spriteHeight) return false
if (!nameSprite.contentEquals(other.nameSprite)) return false
if (nameSpriteWidth != other.nameSpriteWidth) return false
if (nameSpriteHeight != other.nameSpriteHeight) return false
if (discoveredOn != other.discoveredOn) return false
if (baseHp != other.baseHp) return false
if (baseBp != other.baseBp) return false
if (baseAp != other.baseAp) return false
if (stage != other.stage) return false
if (attribute != other.attribute) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + spriteIdle.contentHashCode()
result = 31 * result + spriteWidth
result = 31 * result + spriteHeight
result = 31 * result + nameSprite.contentHashCode()
result = 31 * result + nameSpriteWidth
result = 31 * result + nameSpriteHeight
result = 31 * result + (discoveredOn?.hashCode() ?: 0)
result = 31 * result + baseHp
result = 31 * result + baseBp
result = 31 * result + baseAp
result = 31 * result + stage
result = 31 * result + attribute.hashCode()
return result
}
}
data class CardProgress(
val id: Long,
val spriteIdle: ByteArray,
val spriteWidth: Int,
val spriteHeight: Int,
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CardProgress
if (id != other.id) return false
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
if (spriteWidth != other.spriteWidth) return false
if (spriteHeight != other.spriteHeight) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + spriteIdle.contentHashCode()
result = 31 * result + spriteWidth
result = 31 * result + spriteHeight
return result
}
}
data class AdventureCharacterWithSprites(
var id: Long = 0,
@@ -106,7 +262,67 @@ object CharacterDtos {
val isBemCard: Boolean,
val finishesAdventure: Long,
val originalTimeInMinutes: Long
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AdventureCharacterWithSprites
if (id != other.id) return false
if (charId != other.charId) return false
if (stage != other.stage) return false
if (attribute != other.attribute) return false
if (ageInDays != other.ageInDays) return false
if (mood != other.mood) return false
if (vitalPoints != other.vitalPoints) return false
if (transformationCountdown != other.transformationCountdown) return false
if (injuryStatus != other.injuryStatus) return false
if (trophies != other.trophies) return false
if (currentPhaseBattlesWon != other.currentPhaseBattlesWon) return false
if (currentPhaseBattlesLost != other.currentPhaseBattlesLost) return false
if (totalBattlesWon != other.totalBattlesWon) return false
if (totalBattlesLost != other.totalBattlesLost) return false
if (activityLevel != other.activityLevel) return false
if (heartRateCurrent != other.heartRateCurrent) return false
if (characterType != other.characterType) return false
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
if (spriteWidth != other.spriteWidth) return false
if (spriteHeight != other.spriteHeight) return false
if (isBemCard != other.isBemCard) return false
if (finishesAdventure != other.finishesAdventure) return false
if (originalTimeInMinutes != other.originalTimeInMinutes) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + charId.hashCode()
result = 31 * result + stage
result = 31 * result + attribute.hashCode()
result = 31 * result + ageInDays
result = 31 * result + mood
result = 31 * result + vitalPoints
result = 31 * result + transformationCountdown
result = 31 * result + injuryStatus.hashCode()
result = 31 * result + trophies
result = 31 * result + currentPhaseBattlesWon
result = 31 * result + currentPhaseBattlesLost
result = 31 * result + totalBattlesWon
result = 31 * result + totalBattlesLost
result = 31 * result + activityLevel
result = 31 * result + heartRateCurrent
result = 31 * result + characterType.hashCode()
result = 31 * result + spriteIdle.contentHashCode()
result = 31 * result + spriteWidth
result = 31 * result + spriteHeight
result = 31 * result + isBemCard.hashCode()
result = 31 * result + finishesAdventure.hashCode()
result = 31 * result + originalTimeInMinutes.hashCode()
return result
}
}
data class EvolutionRequirementsWithSpritesAndObtained(
val charaId: Long,
@@ -121,7 +337,45 @@ object CharacterDtos {
val requiredWinRate: Int,
val changeTimerHours: Int,
val requiredAdventureLevelCompleted: Int
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EvolutionRequirementsWithSpritesAndObtained
if (charaId != other.charaId) return false
if (fromCharaId != other.fromCharaId) return false
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
if (spriteWidth != other.spriteWidth) return false
if (spriteHeight != other.spriteHeight) return false
if (discoveredOn != other.discoveredOn) return false
if (requiredTrophies != other.requiredTrophies) return false
if (requiredVitals != other.requiredVitals) return false
if (requiredBattles != other.requiredBattles) return false
if (requiredWinRate != other.requiredWinRate) return false
if (changeTimerHours != other.changeTimerHours) return false
if (requiredAdventureLevelCompleted != other.requiredAdventureLevelCompleted) return false
return true
}
override fun hashCode(): Int {
var result = charaId.hashCode()
result = 31 * result + fromCharaId.hashCode()
result = 31 * result + spriteIdle.contentHashCode()
result = 31 * result + spriteWidth
result = 31 * result + spriteHeight
result = 31 * result + (discoveredOn?.hashCode() ?: 0)
result = 31 * result + requiredTrophies
result = 31 * result + requiredVitals
result = 31 * result + requiredBattles
result = 31 * result + requiredWinRate
result = 31 * result + changeTimerHours
result = 31 * result + requiredAdventureLevelCompleted
return result
}
}
data class FusionsWithSpritesAndObtained(
val charaId: Long,
@@ -131,5 +385,33 @@ object CharacterDtos {
val spriteHeight: Int,
val discoveredOn: Long?,
val fusionAttribute: NfcCharacter.Attribute
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FusionsWithSpritesAndObtained
if (charaId != other.charaId) return false
if (fromCharaId != other.fromCharaId) return false
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
if (spriteWidth != other.spriteWidth) return false
if (spriteHeight != other.spriteHeight) return false
if (discoveredOn != other.discoveredOn) return false
if (fusionAttribute != other.fusionAttribute) return false
return true
}
override fun hashCode(): Int {
var result = charaId.hashCode()
result = 31 * result + fromCharaId.hashCode()
result = 31 * result + spriteIdle.contentHashCode()
result = 31 * result + spriteWidth
result = 31 * result + spriteHeight
result = 31 * result + (discoveredOn?.hashCode() ?: 0)
result = 31 * result + fusionAttribute.hashCode()
return result
}
}
}
@@ -1,10 +1,15 @@
package com.github.nacabaro.vbhelper.screens
/**
* This is me, nacabaro.
* File related to battles.
* TODO: Refactor this, 2400+ lines feels like a lot.
*/
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.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
@@ -28,10 +33,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.material3.LinearProgressIndicator
//import androidx.compose.material3.ExposedDropdownMenuBox
//import androidx.compose.material3.ExposedDropdownMenuDefaults
//import androidx.compose.material3.OutlinedTextField
//import androidx.compose.material3.DropdownMenuItem
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.LaunchedEffect
@@ -51,8 +52,6 @@ import android.content.Intent
import android.net.Uri
import android.media.MediaPlayer
import android.os.Environment
//import androidx.compose.animation.core.animateFloatAsState
//import androidx.compose.animation.core.tween
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope
@@ -72,13 +71,8 @@ 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.first
import kotlinx.coroutines.flow.collect
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
//import kotlin.math.sin
//import kotlin.math.PI
//import androidx.compose.animation.core.animateDpAsState
//import androidx.compose.animation.core.animateIntAsState
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Shadow
@@ -86,24 +80,20 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.foundation.Image
import androidx.compose.ui.graphics.asImageBitmap
import android.graphics.BitmapFactory
//import android.os.Environment
import java.io.File
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.foundation.layout.width
import com.github.nacabaro.vbhelper.di.VBHelper
import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.material3.AlertDialog
@Composable
/*@Composable
fun isLandscapeMode(): Boolean {
val configuration = LocalConfiguration.current
return configuration.screenWidthDp > configuration.screenHeightDp
}
}*/
@Composable
fun getLandscapeModifier(): Modifier {
@@ -1735,7 +1725,7 @@ fun BattlesScreen() {
val currentCharacter = activeUserCharacter
if (currentCharacter != null && canBattle && playerBattleType != null) {
try {
RetrofitHelper().getOpponents(context, playerBattleType!!) { opponents ->
RetrofitHelper().getOpponents(context, playerBattleType) { opponents ->
try {
// Create a new list to trigger UI recomposition
opponentsList = ArrayList(opponents.opponentsList)
@@ -2415,7 +2405,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
@@ -2442,7 +2432,7 @@ 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 = apiResult.winner ?: if (playerWon) "You" else "Opponent"
winnerName = apiResult.winner
isWinnerLoaded = true
// Then send the cleanup call - this will have the actual winner name
@@ -2465,7 +2455,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
@@ -45,11 +45,12 @@ class HomeScreenControllerImpl(
.clearSpecialMission(missionId)
if (missionStatus.status == SpecialMission.Status.COMPLETED) {
val randomItem = database
.itemDao()
.getAllItems()
.first()
.random()
val allItems = database.itemDao().getAllItems().first()
if (allItems.isEmpty()) {
onCleared(null, null)
return@launch
}
val randomItem = allItems.random()
val randomItemAmount = (Random.nextFloat() * 5).roundToInt()
@@ -142,7 +142,7 @@ fun VBDiMHomeScreen(
ItemDisplay(
icon = R.drawable.baseline_swords_24,
textValue = when {
activeMon.totalBattlesLost == 0 -> "0.00 %"
activeMon.currentPhaseBattlesWon + activeMon.currentPhaseBattlesLost == 0 -> "0.00 %"
else -> {
val battleWinPercentage =
activeMon.currentPhaseBattlesWon.toFloat() / (activeMon.currentPhaseBattlesWon + activeMon.currentPhaseBattlesLost).toFloat()
@@ -150,7 +150,7 @@ fun VBDiMHomeScreen(
Locale.getDefault(),
"%.2f",
battleWinPercentage * 100
) + " %" // Specify locale
) + " %"
}
},
definition = stringResource(R.string.home_vbdim_current_phase_win),
@@ -193,9 +193,9 @@ fun VBDiMHomeScreen(
onClickMission = { missionId ->
selectedSpecialMissionId = missionId
},
onClickCollect = {
onClickCollect = { missionId ->
homeScreenController
.clearSpecialMission(selectedSpecialMissionId, onClickCollect)
.clearSpecialMission(missionId, onClickCollect)
}
)
}
@@ -4,9 +4,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
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))
TabRow(
PrimaryTabRow(
selectedTabIndex = selectedTabItem,
modifier = Modifier
) {
@@ -44,14 +44,8 @@ fun ScanScreen(
val context = LocalContext.current
LaunchedEffect(storageRepository) {
LaunchedEffect(characterId) {
withContext(Dispatchers.IO) {
/*
First check if there is a character sent through the navigation system
If there is not, that means we got here through the home screen nfc button
If we got here through the home screen, it does not hurt to check if there is
an active character.
*/
if (characterId != null && nfcCharacter == null) {
nfcCharacter = scanScreenController.characterToNfc(characterId)
}
@@ -3,6 +3,7 @@ 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
@@ -15,12 +16,16 @@ import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.card.Card
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.stateIn
@@ -32,8 +37,10 @@ class ScanScreenControllerImpl(
private val componentActivity: ComponentActivity,
private val registerActivityLifecycleListener: (String, ActivityLifecycleListener)->Unit,
private val unregisterActivityLifecycleListener: (String)->Unit,
private val database: AppDatabase,
): ScanScreenController {
private var lastScannedCharacter: NfcCharacter? = null
private var pendingExportCharacterId: Long? = null
private val nfcAdapter: NfcAdapter
init {
@@ -46,7 +53,9 @@ class ScanScreenControllerImpl(
}
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit, onMultipleCards: (List<Card>) -> Unit) {
handleTag(secrets) { tagCommunicator ->
handleTag(
secrets,
handlerFunc = { tagCommunicator ->
try {
val character = tagCommunicator.receiveCharacter()
val resultMessage = characterFromNfc(character) { cards, nfcCharacter ->
@@ -63,7 +72,27 @@ class ScanScreenControllerImpl(
onComplete.invoke()
componentActivity.getString(R.string.scan_error_generic)
}
},
hceHandler = { isoDep ->
try {
val client = VitalWearHceReaderClient(isoDep)
val character = client.receiveCharacterFromWatch()
val importer = VitalWearCharacterImporter(database)
val result = importer.importCharacter(character)
Log.i("NFC_READ", "VitalWear HCE import: ${result.message}")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, result.message, Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Log.e("NFC_READ", "Error reading character from VitalWear HCE", e)
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic) + ": " + (e.message ?: e.javaClass.simpleName), Toast.LENGTH_LONG).show()
}
} finally {
onComplete()
}
}
)
}
override fun cancelRead() {
@@ -83,29 +112,45 @@ class ScanScreenControllerImpl(
unregisterActivityLifecycleListener.invoke(key)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun handleTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String) {
private fun handleTag(
secrets: Secrets,
handlerFunc: (TagCommunicator) -> String,
hceHandler: ((IsoDep) -> Unit)? = null,
) {
if (!nfcAdapter.isEnabled) {
showWirelessSettings()
} 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,
nfcAdapter.enableReaderMode(
componentActivity,
buildOnReadTag(secrets, handlerFunc, hceHandler),
NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
options
)
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun buildOnReadTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
return { tag->
private fun buildOnReadTag(
secrets: Secrets,
handlerFunc: (TagCommunicator) -> String,
hceHandler: ((IsoDep) -> Unit)? = null,
): (Tag) -> Unit {
return { tag ->
val isoDep = IsoDep.get(tag)
if (isoDep != null && hceHandler != null) {
// VitalWear watch via HCE / ISO-DEP
isoDep.connect()
isoDep.use { hceHandler(isoDep) }
} else {
// Physical Vital Bracelet via raw NFC-A
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()
}
}
} else {
nfcData.connect()
nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
@@ -116,6 +161,8 @@ class ScanScreenControllerImpl(
}
}
}
}
}
private fun checkSecrets() {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
@@ -132,16 +179,16 @@ class ScanScreenControllerImpl(
nfcCharacter: NfcCharacter,
onComplete: () -> Unit
) {
handleTag(secrets) { tagCommunicator ->
handleTag(
secrets,
handlerFunc = { tagCommunicator ->
try {
if (nfcCharacter is VBNfcCharacter) {
Log.d("SendCharacter", "VBNfcCharacter")
val castNfcCharacter: VBNfcCharacter = nfcCharacter
tagCommunicator.sendCharacter(castNfcCharacter)
tagCommunicator.sendCharacter(nfcCharacter)
} else if (nfcCharacter is BENfcCharacter) {
Log.d("SendCharacter", "BENfcCharacter")
val castNfcCharacter: BENfcCharacter = nfcCharacter
tagCommunicator.sendCharacter(castNfcCharacter)
tagCommunicator.sendCharacter(nfcCharacter)
}
onComplete.invoke()
componentActivity.getString(R.string.scan_sent_character_success)
@@ -149,7 +196,12 @@ class ScanScreenControllerImpl(
Log.e("TAG", e.stackTraceToString())
componentActivity.getString(R.string.scan_error_generic)
}
},
hceHandler = { _ ->
// Character was already sent in onClickCheckCard's HCE handler.
onComplete()
}
)
}
override fun onClickCheckCard(
@@ -157,11 +209,44 @@ class ScanScreenControllerImpl(
nfcCharacter: NfcCharacter,
onComplete: () -> Unit
) {
handleTag(secrets) { tagCommunicator ->
handleTag(
secrets,
handlerFunc = { tagCommunicator ->
tagCommunicator.prepareDIMForCharacter(nfcCharacter.dimId)
onComplete.invoke()
componentActivity.getString(R.string.scan_sent_dim_success)
},
hceHandler = { isoDep ->
// VitalWear watch: send the character proto directly via HCE.
val characterId = pendingExportCharacterId
if (characterId == null) {
Log.e("NFC_WRITE", "No pending character to send to VitalWear")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic), Toast.LENGTH_SHORT).show()
}
onComplete()
return@handleTag
}
try {
val proto = VitalWearCharacterExporter(componentActivity, database).buildCharacterProto(characterId)
val client = VitalWearHceReaderClient(isoDep)
client.sendCharacterToWatch(proto)
database.userCharacterDao().deleteCharacterById(characterId)
pendingExportCharacterId = null
Log.i("NFC_WRITE", "Character sent to VitalWear and deleted from app (id=$characterId)")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_sent_character_success), Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Log.e("NFC_WRITE", "Error sending character to VitalWear HCE", e)
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic) + ": " + (e.message ?: e.javaClass.simpleName), Toast.LENGTH_LONG).show()
}
} finally {
onComplete()
}
}
)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
@@ -181,10 +266,8 @@ class ScanScreenControllerImpl(
}
override suspend fun characterToNfc(characterId: Long): NfcCharacter {
val nfcGenerator = ToNfcConverter(
componentActivity = componentActivity
)
pendingExportCharacterId = characterId
val nfcGenerator = ToNfcConverter(componentActivity = componentActivity)
val character = nfcGenerator.characterToNfc(characterId)
Log.d("CharacterType", character.toString())
return character
@@ -5,6 +5,7 @@ import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* Utility for converting character device types.
@@ -31,7 +32,7 @@ class DeviceTypeConverter(private val database: AppDatabase) {
true
} catch (e: Exception) {
// Log but don't crash - character can still be sent as-is
android.util.Log.e("DeviceTypeConverter", "Failed to convert character $characterId to VB type", e)
Timber.e(e, "Failed to convert character $characterId to VB type")
false
}
}
@@ -42,7 +43,7 @@ class DeviceTypeConverter(private val database: AppDatabase) {
val vbCharacterData = VBCharacterData(
id = characterId,
generation = 0,
totalTrophies = 0
totalTrophies = 0,
)
database.userCharacterDao().insertVBCharacterData(vbCharacterData)
}
@@ -15,14 +15,13 @@ class VitalWearCharacterExporter(
private val context: Context,
private val database: AppDatabase
) {
fun buildShareIntent(characterId: Long): Intent {
return runBlocking {
fun buildCharacterProto(characterId: Long): Character = 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()
Character.newBuilder()
.setCardId(card.cardId)
.setCardName(card.name)
.setCharacterStats(
@@ -61,6 +60,11 @@ class VitalWearCharacterExporter(
}
)
.build()
}
fun buildShareIntent(characterId: Long): Intent {
return runBlocking {
val proto = buildCharacterProto(characterId)
val exportDir = File(context.cacheDir, "exports").apply { mkdirs() }
val exportFile = File(exportDir, "vbhelper_character_$characterId.vitalwear")
@@ -13,7 +13,27 @@ data class BitmapData (
val bitmap: ByteArray,
val width: Int,
val height: Int
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BitmapData
if (!bitmap.contentEquals(other.bitmap)) return false
if (width != other.width) return false
if (height != other.height) return false
return true
}
override fun hashCode(): Int {
var result = bitmap.contentHashCode()
result = 31 * result + width
result = 31 * result + height
return result
}
}
data class ImageBitmapData(
val imageBitmap: ImageBitmap,
@@ -23,6 +23,13 @@ message Character {
int32 total_wins = 13;
int32 current_phase_wins = 14;
int32 mood = 15;
enum TransferDeviceType {
TRANSFER_DEVICE_TYPE_UNSPECIFIED = 0;
TRANSFER_DEVICE_TYPE_VB = 1;
TRANSFER_DEVICE_TYPE_BE = 2;
}
TransferDeviceType transfer_device_type = 16;
}
CharacterStats character_stats = 3;
+47 -37
View File
@@ -17,21 +17,21 @@
<string name="nav_adventure">アドベンチャー</string>
<string name="nav_credits">クレジット</string>
<string name="adventure_title">アドベンチャー</string>
<string name="adventure_empty_state">何もありません。</string>
<string name="adventure_empty_state">No Data</string>
<string name="home_title">VB Helper</string>
<string name="scan_secrets_not_imported">" APKの読み込みがされていません。設定から読み込をして下さい。 "</string>
<string name="scan_secrets_not_imported">"APKの読み込みがされていません。設定から読み込をして下さい。 "</string>
<string name="scan_title">デバイスをスキャン</string>
<string name="scan_sent_dim_success">DIMの転送に成功しました。</string>
<string name="scan_nfc_must_be_enabled">NFC機能を有効にして下さい。</string>
<string name="settings_title">設定</string>
<string name="scan_missing_secrets">必要なデータが不足しています。設定を開き、Vital ArenaのAPKを読み込んでください。</string>
<string name="scan_missing_secrets">Vital ArenaのAPKを読み込みして下さい。</string>
<string name="scan_sent_character_success">転送が成功しました。</string>
<string name="scan_error_generic">エラー</string>
<string name="settings_about_desc">このアプリについて</string>
<string name="settings_export_data_title">データベースの書き出し</string>
<string name="settings_export_data_desc">" データベースを書き出します。"</string>
<string name="settings_import_data_title">データベースの読み込み</string>
<string name="settings_import_data_desc">" データベースを読み込みます。"</string>
<string name="settings_export_data_title">データベースのエクスポート</string>
<string name="settings_export_data_desc">データベースの書き出しをします。</string>
<string name="settings_import_data_title">データベースのインポート</string>
<string name="settings_import_data_desc">データベースの読み込みをします。</string>
<string name="credits_title">クレジット</string>
<string name="battles_coming_soon">Coming soon</string>
<string name="cards_my_cards_title">カードリスト</string>
@@ -42,46 +42,44 @@
<string name="scan_vb_to_app">デバイスからアプリへ</string>
<string name="scan_app_to_vb">アプリからデバイスへ</string>
<string name="read_character_title">キャラクターを読み込みます。</string>
<string name="read_character_prepare_device">バイタルブレスを準備してください。</string>
<string name="read_character_prepare_device">デバイスを準備してください。</string>
<string name="read_character_go_to_connect">準備が出来たら転送を押してください。</string>
<string name="read_character_confirm">転送</string>
<string name="card_adventure_missions_title">アドベンチャーミッション</string>
<string name="card_entry_characters_obtained">%1$d / %2$d のキャラクターを取得しました。</string>
<string name="card_entry_edit">編集</string>
<string name="card_entry_edit">編集</string>
<string name="card_entry_edit">編集</string>
<string name="card_entry_edit">編集</string>
<string name="card_entry_delete">削除</string>
<string name="home_vbdim_current_phase_win">フェーズ勝率 %</string>
<string name="home_vbdim_special_missions">スペシャルミッション</string>
<string name="storage_delete_character">削除</string>
<string name="storage_close">閉じる</string>
<string name="storage_character_image_description">キャラクター画像</string>
<string name="storage_send_to_watch">転送</string>
<string name="storage_send_to_watch">デバイスへ転送</string>
<string name="storage_send_to_vitalwear">VitalWearへ転送</string>
<string name="storage_set_active">選択</string>
<string name="storage_send_on_adventure">アドベンチャーへ出発</string>
<string name="scan_no_nfc_on_device">NFC機能が見つかりません。</string>
<string name="scan_tag_not_vb">検出されたタグはバイタルブレスでは使用出来ません。</string>
<string name="settings_section_nfc">NFC</string>
<string name="settings_section_dim_bem">DiM / BEm</string>
<string name="settings_section_nfc">NFC通信</string>
<string name="settings_section_dim_bem">DiM / BEm管理</string>
<string name="settings_section_data">データ管理</string>
<string name="settings_import_apk_title">アプリの読み込み</string>
<string name="settings_import_apk_desc">" Vital Arena 2.1.0のAPKファイルを読み込みします。"</string>
<string name="settings_import_card_title">カードの読み込み</string>
<string name="settings_import_apk_title">アプリのインポート</string>
<string name="settings_import_apk_desc">Vital Arena 2.1.0のAPKファイルを読み込みします。</string>
<string name="settings_import_card_title">カードのインポート</string>
<string name="settings_section_about">概要 / クレジット</string>
<string name="dex_chara_fusions_button">ジョグレス</string>
<string name="dex_chara_close_button">閉じる</string>
<string name="settings_import_card_desc">" Dim / BEm カードファイルを読み込みます。"</string>
<string name="settings_import_card_desc">Dim / BEm のデータファイルを読み込みします。</string>
<string name="settings_credits_title">クレジット</string>
<string name="settings_credits_desc">開発者について</string>
<string name="settings_about_title">概要</string>
<string name="beta_warning_message_main">" このアプリは現在アルファ版です。今後のアップデートにより保存されているデータの全てが削除される可能性がある為、大切なデータの保存はしないようにお願い致します。ご不便をおかけして申し訳ございません。 "</string>
<string name="beta_warning_message_compatibility">" このアプリはオリジナルのVBおよびVHで動作するようになりました。 "</string>
<string name="beta_warning_message_thanks">" ご理解とご協力に感謝いたします。\n 開発チーム一同"</string>
<string name="beta_warning_message_main">このアプリは現在開発中です。\n今後のアップデートにより、保存されているデータ全ての削除が必要になる場合があります。\nその為、大切なデータの保存には使用しないようお願い致します。\nご不便をおかけして申し訳ございません。</string>
<string name="beta_warning_message_compatibility">"このアプリはオリジナルのVBおよびVHで動作するようになりました。 "</string>
<string name="beta_warning_message_thanks">ご理解とご協力に感謝致します。\n開発チーム一同</string>
<string name="home_vbdim_vitals">バイタル</string>
<string name="home_adventure_mission_finished">" 1人のキャラクターがアドベンチャーミッションを完了しました。 "</string>
<string name="scan_secrets_not_initialized">" 初期化が出来ませんでした。もう一度試して下さい。 "</string>
<string name="home_special_mission_delete_main">現在の進捗状況が失われますが、本当にこのスペシャルミッションを削除しますか?</string>
<string name="home_adventure_mission_finished">"1人のキャラクターがアドベンチャーミッションを完了しました。 "</string>
<string name="scan_secrets_not_initialized">"初期化が出来ませんでした。もう一度試して下さい。 "</string>
<string name="home_special_mission_delete_main">現在の進捗状況が失われます。本当にこのミッションを削除しますか?</string>
<string name="home_special_mission_delete_dismiss">閉じる</string>
<string name="home_special_mission_delete_remove">削除</string>
<string name="item_dialog_use">アイテム使用</string>
@@ -92,28 +90,27 @@
<string name="battles_online_title">オンラインバトル</string>
<string name="obtained_item_you_also_got_credits">%1$d クレジットを獲得しました。</string>
<string name="obtained_item_dismiss">閉じる</string>
<string name="obtained_item_dismiss">閉じる</string>
<string name="nav_dex">図鑑</string>
<string name="action_place_near_reader">バイタルブレス本体を近くに置いて下さい。</string>
<string name="credits_RedEyez_description">献身的なデバッグ作業を行って頂きました。</string>
<string name="action_place_near_reader">デバイスを近づけて下さい。</string>
<string name="action_cancel">キャンセル</string>
<string name="card_view_discovered_characters">解放済みキャラクター</string>
<string name="dex_chara_icon_description">キャラクターアイコン</string>
<string name="dex_chara_name_icon_description">名前</string>
<string name="credits_section_reverse_engineering">リバースエンジニアリング</string>
<string name="credits_section_reverse_engineering">リバースエンジニアリング</string>
<string name="credits_section_reverse_engineering">プログラム解析</string>
<string name="credits_section_app_development">アプリケーション開発</string>
<string name="credits_cyanic_description">" ファームウェアの解析を行い、開発をサポートしてくれました。"</string>
<string name="credits_cfogrady_description">" vb-lib-nfc開発およびアプリの開発に協力して頂きました。"</string>
<string name="credits_nacabaro_description">" アプリの開発をしました。"</string>
<string name="credits_lightheel_description">" アプリのサーバー含む対戦機能を開発して頂きました。現在も開発を継続して頂いています。"</string>
<string name="credits_shvstrz_description">" アプリのアイコンデザインをして頂きました。"</string>
<string name="credits_cyanic_description">ファームウェアの解析を行い、開発をサポートしてくれました。</string>
<string name="credits_cfogrady_description">vb-lib-nfc開発および本アプリの開発に協力して頂きました。</string>
<string name="credits_nacabaro_description">本アプリの開発をしました。</string>
<string name="credits_lightheel_description">本アプリのサーバー含む対戦機能を開発して頂きました。現在も開発を継続して頂いています。</string>
<string name="credits_shvstrz_description">本アプリのアイコンデザインをして頂きました。</string>
<string name="write_card_title">カード情報書き込み中</string>
<string name="write_card_icon_description">カードアイコン</string>
<string name="write_card_device_ready">デバイスの準備をして下さい。</string>
<string name="write_card_required_card">%1$s カードが必要です。</string>
<string name="write_card_confirm">転送</string>
<string name="write_character_success">" カードの読み込みが完了しました。"</string>
<string name="write_character_wait_ready">" デバイスの準備をしてから確認を押して下さい。"</string>
<string name="write_character_success">カードの読み込みが完了しました。</string>
<string name="write_character_wait_ready">デバイスの準備をしてから確認を押して下さい。</string>
<string name="write_character_confirm">確認</string>
<string name="home_vbdim_trophies">トロフィー</string>
<string name="items_purchase_success">購入しました。</string>
@@ -124,10 +121,10 @@
<string name="dex_chara_stats">HP: %1$d, BP: %2$d, AP: %3$d</string>
<string name="dex_chara_stage_attribute_unknown">Stg: -, Atr: -</string>
<string name="dex_chara_stats_unknown">HP: -, BP: -, AP: -</string>
<string name="dex_chara_requirements">" Tr: %1$d; Bt: %2$d; Vr: %3$d; Wr: %4$d%%; Ct: %5$dh "</string>
<string name="dex_chara_requirements">\n Tr: %1$d; Bt: %2$d; Vr: %3$d; Wr: %4$d%%; Ct: %5$dh</string>
<string name="dex_chara_adventure_level">AdvLvl %1$d</string>
<string name="storage_my_characters_title">キャラクターリスト</string>
<string name="storage_nothing_to_see_here">表示するデータはありません。</string>
<string name="storage_nothing_to_see_here">No Data</string>
<string name="storage_in_adventure_toast">このキャラクターは冒険中です。</string>
<string name="home_vbdim_next_timer">次のタイマー</string>
<string name="home_vbdim_total_battle_win">総勝率 %</string>
@@ -148,4 +145,17 @@
<string name="special_mission_steps_progress">歩数 %1$d 歩</string>
<string name="special_mission_battles_progress">バトル %1$d 回</string>
<string name="special_mission_wins_progress">勝利 %1$d 回</string>
<string name="settings_companion_import_card_image_title">カードのインポート</string>
<string name="companion_validation_connect_vb">デバイスに接続</string>
<string name="companion_card_import_success">カードのインポートに成功しました。</string>
<string name="companion_firmware_no_watch">デバイスが見つかりません。</string>
<string name="companion_validation_insert_card">デバイスへカードを差し込んで下さい。</string>
<string name="companion_validation_success">データチェックが完了しました。</string>
<string name="companion_loading_select_firmware">ファームウェアファイルを選択</string>
<string name="companion_firmware_sent_success">ファームウェアの送信が完了しました。</string>
<string name="companion_logs_receive_failed">ログの受信に失敗しました。</string>
<string name="companion_logs_failed_request">ログの要求に失敗しました。</string>
<string name="companion_logs_warning">注意:ログの転送には時間がかかる場合があります。</string>
<string name="settings_companion_send_watch_logs_title">ログの送信</string>
<string name="companion_transfer_disabled_message">現在、転送は無効になっています。</string>
</resources>
+13
View File
@@ -219,4 +219,17 @@
<string name="home_special_mission_delete_dismiss">Dismiss</string>
<string name="home_special_mission_delete_remove">Remove</string>
<string name="settings_companion_import_card_image_title">Import Card Image</string>
<string name="companion_validation_connect_vb">Connect Vital Bracelet</string>
<string name="companion_card_import_success">Card imported successfully!</string>
<string name="companion_firmware_no_watch">No watch found!</string>
<string name="companion_validation_insert_card">Insert card into Vital Bracelet</string>
<string name="companion_validation_success">Validation successful!</string>
<string name="companion_loading_select_firmware">Select firmware file</string>
<string name="companion_firmware_sent_success">Firmware sent successfully!</string>
<string name="companion_logs_receive_failed">Failed to receive logs</string>
<string name="companion_logs_failed_request">Failed to request logs</string>
<string name="companion_logs_warning">Warning: Log transfer might take a while</string>
<string name="settings_companion_send_watch_logs_title">Send Watch Logs</string>
<string name="companion_transfer_disabled_message">Transfer is currently disabled</string>
</resources>
+2 -6
View File
@@ -3,10 +3,6 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
buildscript {
dependencies {
classpath(libs.protobuf.gradle.plugin)
}
alias(libs.plugins.vksp) apply false
alias(libs.plugins.vprotobuf) apply false
}
+3
View File
@@ -21,3 +21,6 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
+31 -13
View File
@@ -1,22 +1,30 @@
[versions]
agp = "8.13.2"
datastore = "1.2.0"
datastorePreferences = "1.2.0"
kotlin = "2.3.0"
coreKtx = "1.17.0"
agp = "9.2.1"
datastore = "1.2.1"
datastorePreferences = "1.2.1"
kotlin = "2.4.0"
coreKtx = "1.19.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.2"
composeBom = "2026.01.00"
material = "1.13.0"
navigationCompose = "2.9.6"
protobufGradlePlugin = "0.9.6"
protobufJavalite = "4.33.4"
lifecycleRuntimeKtx = "2.11.0"
activityCompose = "1.13.0"
composeBom = "2026.06.00"
material = "1.14.0"
navigationCompose = "2.9.8"
protobufGradlePlugin = "0.10.0"
protobufJavalite = "4.35.1"
roomRuntime = "2.8.4"
vbNfcReader = "0.2.0-SNAPSHOT"
dimReader = "2.1.0"
playServicesWearable = "20.0.1"
kotlinxCoroutinesPlayServices = "1.11.0"
timber = "5.0.1"
tinylog = "2.7.0"
retrofit = "3.0.0"
gson = "2.14.0"
okhttp = "5.4.0"
ksp = "2.3.9"
[libraries]
androidx-compose-material = { module = "androidx.compose.material:material" }
@@ -42,13 +50,23 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
material = { module = "com.google.android.material:material", version.ref = "material" }
protobuf-gradle-plugin = { module = "com.google.protobuf:protobuf-gradle-plugin", version.ref = "protobufGradlePlugin" }
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 = { group = "com.google.android.gms", name = "play-services-wearable", version.ref = "playServicesWearable" }
kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesPlayServices" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tinylog-api = { module = "org.tinylog:tinylog-api", version.ref = "tinylog" }
tinylog-impl = { module = "org.tinylog:tinylog-impl", version.ref = "tinylog" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
vksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
vprotobuf = { id = "com.google.protobuf", version.ref = "protobufGradlePlugin" }
+1 -1
View File
@@ -1,6 +1,6 @@
#Tue Dec 24 10:55:57 UTC 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+1
View File
@@ -17,6 +17,7 @@ dependencyResolutionManagement {
mavenLocal()
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}