diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml
deleted file mode 100644
index 48625e4..0000000
--- a/app/lint-baseline.xml
+++ /dev/null
@@ -1,1782 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/androidTest/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanNfcaIntegrationTest.kt b/app/src/androidTest/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanNfcaIntegrationTest.kt
deleted file mode 100644
index eb93b04..0000000
--- a/app/src/androidTest/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanNfcaIntegrationTest.kt
+++ /dev/null
@@ -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
- }
- }
-}
-
-
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 39fa631..8e59f52 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,15 +22,6 @@
android:theme="@style/Theme.VBHelper"
android:usesCleartextTraffic="true"
tools:targetApi="31">
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedDatabaseFactory.kt b/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedDatabaseFactory.kt
deleted file mode 100644
index 381d2f6..0000000
--- a/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedDatabaseFactory.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.github.cfogrady.vitalwear.common.data
-
-import android.content.Context
-import androidx.room.Room
-
-object SharedDatabaseFactory {
- private const val DATABASE_NAME = "shared_transfer.db"
-
- @Volatile
- private var instance: SharedTransferDatabase? = null
-
- fun getDatabase(context: Context): SharedTransferDatabase {
- return instance ?: synchronized(this) {
- instance ?: Room.databaseBuilder(
- context.applicationContext,
- SharedTransferDatabase::class.java,
- DATABASE_NAME
- ).build().also { instance = it }
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferDatabase.kt b/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferDatabase.kt
deleted file mode 100644
index dbee358..0000000
--- a/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferDatabase.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.github.cfogrady.vitalwear.common.data
-
-import androidx.room.Database
-import androidx.room.RoomDatabase
-
-@Database(
- entities = [
- SharedTransferSeenEntity::class,
- ],
- version = 1,
- exportSchema = false,
-)
-abstract class SharedTransferDatabase : RoomDatabase() {
- abstract fun transferSeenDao(): SharedTransferSeenDao
-}
-
diff --git a/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferSeenDao.kt b/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferSeenDao.kt
deleted file mode 100644
index 3ab4d41..0000000
--- a/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferSeenDao.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.github.cfogrady.vitalwear.common.data
-
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import androidx.room.Transaction
-
-@Dao
-interface SharedTransferSeenDao {
- @Insert(onConflict = OnConflictStrategy.IGNORE)
- fun insert(entry: SharedTransferSeenEntity): Long
-
- @Query(
- """
- UPDATE ${SharedTransferSeenEntity.TABLE}
- SET cardName = :cardName,
- seenAtEpochMillis = :seenAtEpochMillis
- WHERE cardLookupKey = :cardLookupKey
- AND slotId = :slotId
- """
- )
- fun update(cardName: String, cardLookupKey: String, slotId: Int, seenAtEpochMillis: Long)
-
- @Transaction
- fun markSeen(cardName: String, slotId: Int, seenAtEpochMillis: Long) {
- if (slotId < 0) {
- return
- }
- val cardLookupKey = cardName.lowercase().filter { it.isLetterOrDigit() }
- if (cardLookupKey.isBlank()) {
- return
- }
- val inserted = insert(
- SharedTransferSeenEntity(
- cardName = cardName,
- cardLookupKey = cardLookupKey,
- slotId = slotId,
- seenAtEpochMillis = seenAtEpochMillis,
- )
- )
- if (inserted == -1L) {
- update(cardName, cardLookupKey, slotId, seenAtEpochMillis)
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferSeenEntity.kt b/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferSeenEntity.kt
deleted file mode 100644
index 192408c..0000000
--- a/app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferSeenEntity.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.cfogrady.vitalwear.common.data
-
-import androidx.room.Entity
-import androidx.room.Index
-import androidx.room.PrimaryKey
-
-@Entity(
- tableName = SharedTransferSeenEntity.TABLE,
- indices = [
- Index(value = ["cardLookupKey", "slotId"], unique = true)
- ]
-)
-data class SharedTransferSeenEntity(
- @PrimaryKey(autoGenerate = true) val id: Long = 0,
- val cardName: String,
- val cardLookupKey: String,
- val slotId: Int,
- val seenAtEpochMillis: Long,
-) {
- companion object {
- const val TABLE = "TransferSeen"
- }
-}
-
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 9d178a1..c8f358b 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt
@@ -1,16 +1,11 @@
package com.github.nacabaro.vbhelper
-import android.content.Intent
-import android.net.Uri
import android.os.Bundle
import android.util.Log
-import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
-import androidx.lifecycle.lifecycleScope
-import com.github.cfogrady.vitalwear.protos.Character
import com.github.nacabaro.vbhelper.navigation.AppNavigation
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers
@@ -22,15 +17,11 @@ 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? = null
private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
if( onActivityLifecycleListeners[key] != null) {
@@ -63,8 +54,6 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
- initialRoute = getInitialRouteFromIntent(intent)
-
setContent {
VBHelperTheme {
MainApplication(
@@ -75,14 +64,12 @@ class MainActivity : ComponentActivity() {
homeScreenController = homeScreenController,
storageScreenController = storageScreenController,
spriteViewerController = spriteViewerController,
- cardScreenController = cardScreenController,
- initialRoute = initialRoute
+ cardScreenController = cardScreenController
)
}
}
Log.i("MainActivity", "Activity onCreated")
- handleImportIntent(intent)
}
override fun onPause() {
@@ -101,71 +88,6 @@ class MainActivity : ComponentActivity() {
}
}
- override fun onNewIntent(intent: Intent) {
- 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) {
- if (data.scheme == "vbhelper" && data.host == "auth") {
- return "Battle"
- }
- }
- return null
- }
-
@Composable
private fun MainApplication(
scanScreenController: ScanScreenControllerImpl,
@@ -175,8 +97,7 @@ class MainActivity : ComponentActivity() {
storageScreenController: StorageScreenControllerImpl,
homeScreenController: HomeScreenControllerImpl,
spriteViewerController: SpriteViewerControllerImpl,
- cardScreenController: CardScreenControllerImpl,
- initialRoute: String? = null
+ cardScreenController: CardScreenControllerImpl
) {
AppNavigation(
applicationNavigationHandlers = AppNavigationHandlers(
@@ -188,12 +109,7 @@ class MainActivity : ComponentActivity() {
homeScreenController,
spriteViewerController,
cardScreenController
- ),
- 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/companion/card/ByteManipulation.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/ByteManipulation.kt
deleted file mode 100644
index 327d3fc..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/ByteManipulation.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.card
-
-enum class Endian {
- Big,
- Little,
-}
-
-fun ByteArray.getUInt32(index: Int = 0, endian: Endian = Endian.Little): UInt {
- require(this.size >= index + 4) { "Must be 4 bytes from index to get a UInt" }
- var result: UInt = 0u
- for (i in 0 until 4) {
- result = if (endian == Endian.Big) {
- result or ((this[index + i].toUInt() and 0xFFu) shl (8 * (3 - i)))
- } else {
- result or ((this[index + i].toUInt() and 0xFFu) shl (8 * i))
- }
- }
- return result
-}
-
-fun ByteArray.getUInt16(index: Int = 0, endian: Endian = Endian.Little): UShort {
- require(this.size >= index + 2) { "Must be 2 bytes from index to get a UInt" }
- var result: UInt = 0u
- for (i in 0 until 2) {
- result = if (endian == Endian.Big) {
- result or ((this[index + i].toUInt() and 0xFFu) shl (8 * (1 - i)))
- } else {
- result or ((this[index + i].toUInt() and 0xFFu) shl (8 * i))
- }
- }
- return result.toUShort()
-}
-
-fun UShort.toByteArray(endian: Endian = Endian.Little): ByteArray {
- val byteArray = byteArrayOf(0, 0)
- val asUInt = this.toUInt()
- for (i in 0 until 2) {
- if (endian == Endian.Little) {
- byteArray[i] = ((asUInt shr (8 * i)) and 255u).toByte()
- } else {
- byteArray[1 - i] = ((asUInt shr (8 * i)) and 255u).toByte()
- }
- }
- return byteArray
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/CompanionImportCardActivity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/CompanionImportCardActivity.kt
deleted file mode 100644
index 1cc8e08..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/CompanionImportCardActivity.kt
+++ /dev/null
@@ -1,400 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.card
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.widget.Toast
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.lifecycle.lifecycleScope
-import androidx.compose.foundation.layout.Arrangement
-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
-import androidx.compose.material3.Button
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-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.cfogrady.vb.dim.adventure.AdventureLevels
-import com.github.cfogrady.vb.dim.adventure.AdventureLevels.AdventureLevel
-import com.github.cfogrady.vb.dim.card.Card
-import com.github.cfogrady.vb.dim.card.DimReader
-import com.github.cfogrady.vb.dim.card.DimWriter
-import com.github.cfogrady.vb.dim.character.CharacterStats
-import com.github.cfogrady.vb.dim.character.CharacterStats.CharacterStatsEntry
-import com.github.cfogrady.vb.dim.fusion.AttributeFusions
-import com.github.cfogrady.vb.dim.fusion.SpecificFusions
-import com.github.cfogrady.vb.dim.fusion.SpecificFusions.SpecificFusionEntry
-import com.github.cfogrady.vb.dim.header.DimHeader
-import com.github.cfogrady.vb.dim.transformation.TransformationRequirements
-import com.github.cfogrady.vb.dim.transformation.TransformationRequirements.TransformationRequirementsEntry
-import com.github.nacabaro.vbhelper.R
-import com.github.nacabaro.vbhelper.companion.card.CompanionValidateCardActivity
-import com.github.nacabaro.vbhelper.companion.common.ChannelTypes
-import com.github.nacabaro.vbhelper.companion.ui.CompanionLoading
-import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
-import com.github.nacabaro.vbhelper.di.VBHelper
-import com.google.android.gms.wearable.Wearable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.tasks.await
-import kotlinx.coroutines.withContext
-import timber.log.Timber
-import java.io.ByteArrayOutputStream
-import java.io.DataOutputStream
-import java.nio.charset.Charset
-
-class CompanionImportCardActivity : ComponentActivity() {
- enum class ImportState {
- PickFile,
- NameOrUnique,
- LoadFile,
- UnlockCard,
- ImportCard,
- Success,
- }
-
- private lateinit var filePickLauncher: ActivityResultLauncher>
- private lateinit var validationLauncher: ActivityResultLauncher
- private var uri: Uri? = null
- private val importState = MutableStateFlow(ImportState.PickFile)
- private val cardName = MutableStateFlow("")
- private val uniqueSprites = MutableStateFlow(false)
- private val convertToBem = MutableStateFlow(false)
- private val importPercent = MutableStateFlow(0)
- private val importStatus = MutableStateFlow("Preparing transfer")
- private lateinit var card: Card, out TransformationRequirements, out AdventureLevels, out AttributeFusions, out SpecificFusions>
-
- private val validatedCardManager by lazy {
- (application as VBHelper).validatedCardManager
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- filePickLauncher = buildFilePickLauncher()
- validationLauncher = buildValidationLauncher()
- setContent {
- val state by importState.collectAsState()
- when (state) {
- ImportState.PickFile -> {
- LaunchedEffect(true) {
- filePickLauncher.launch(arrayOf("*/*"))
- }
- CompanionLoading(loadingText = stringResource(R.string.settings_companion_import_card_image_title))
- }
- ImportState.NameOrUnique -> NameOrUnique()
- ImportState.LoadFile -> {
- CompanionLoading(loadingText = "Loading Card Image")
- LaunchedEffect(true) {
- runCatching {
- loadCard()
- }.onFailure { error ->
- Timber.e(error, "Failed to load card image")
- Toast.makeText(
- this@CompanionImportCardActivity,
- "Card load failed: ${error.message ?: "unknown error"}",
- Toast.LENGTH_LONG,
- ).show()
- importState.value = ImportState.NameOrUnique
- }
- }
- }
- ImportState.UnlockCard -> {
- CompanionLoading(loadingText = stringResource(R.string.companion_validation_connect_vb))
- LaunchedEffect(true) {
- validationLauncher.launch(
- Intent(applicationContext, CompanionValidateCardActivity::class.java).apply {
- putExtra(CompanionValidateCardActivity.CARD_VALIDATED_KEY, card.header.dimId)
- }
- )
- }
- }
- ImportState.ImportCard -> {
- val percent by importPercent.collectAsState()
- val status by importStatus.collectAsState()
- CompanionLoading(loadingText = "$status ($percent%)")
- LaunchedEffect(true) {
- withContext(Dispatchers.IO) {
- try {
- val transferred = importCard()
- importState.value = if (transferred) ImportState.Success else ImportState.NameOrUnique
- } catch (error: Exception) {
- Timber.e(error, "Card transfer failed")
- withContext(Dispatchers.Main) {
- Toast.makeText(
- this@CompanionImportCardActivity,
- "Card transfer failed: ${error.message ?: "unknown error"}",
- Toast.LENGTH_LONG,
- ).show()
- }
- importState.value = ImportState.NameOrUnique
- }
- }
- }
- }
- ImportState.Success -> {
- LaunchedEffect(true) {
- Handler(Looper.getMainLooper()).postDelayed({
- finish()
- }, 1000)
- }
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- Text(text = stringResource(R.string.companion_card_import_success))
- }
- }
- }
- }
- }
-
- @Composable
- private fun NameOrUnique() {
- val selectedUri = uri
- val filePath = selectedUri?.path ?: "No file selected"
- val name by cardName.collectAsState()
- val unique by uniqueSprites.collectAsState()
- val convert by convertToBem.collectAsState()
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Button(modifier = Modifier.padding(end = 10.dp), onClick = { importState.value = ImportState.PickFile }) {
- Text(text = "File")
- }
- Text(text = filePath)
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(text = "Name:", modifier = Modifier.padding(end = 10.dp))
- TextField(value = name, onValueChange = { cardName.value = it })
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(text = "Unique Sprites:")
- Checkbox(checked = unique, onCheckedChange = { uniqueSprites.value = it })
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(text = "Convert to BeM:")
- Checkbox(checked = convert, onCheckedChange = { convertToBem.value = it })
- }
- Button(
- modifier = Modifier.padding(top = 16.dp),
- enabled = selectedUri != null,
- onClick = { importState.value = ImportState.LoadFile },
- ) {
- Text(text = "Import")
- }
- }
- }
-
- private fun buildFilePickLauncher(): ActivityResultLauncher> {
- return registerForActivityResult(ActivityResultContracts.OpenDocument()) {
- if (it == null) {
- Toast.makeText(
- this,
- "Card file selection cancelled.",
- Toast.LENGTH_SHORT,
- ).show()
- importState.value = ImportState.NameOrUnique
- } else {
- uri = it
- val path = it.path ?: ""
- var name = path.substringAfterLast("/")
- if (name.contains('.')) {
- name = name.substringBeforeLast('.')
- }
- cardName.value = name
- importState.value = ImportState.NameOrUnique
- }
- }
- }
-
- private fun buildValidationLauncher(): ActivityResultLauncher {
- return registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- val validatedCardId = result.data?.extras?.getInt(CompanionValidateCardActivity.CARD_VALIDATED_KEY, -1)
- if (result.resultCode != RESULT_OK || validatedCardId == null || validatedCardId == -1) {
- Toast.makeText(
- this,
- "Card validation was cancelled.",
- Toast.LENGTH_SHORT,
- ).show()
- importState.value = ImportState.NameOrUnique
- } else {
- lifecycleScope.launch(Dispatchers.IO) {
- validatedCardManager.addValidatedCard(validatedCardId)
- importState.value = ImportState.ImportCard
- }
- }
- }
- }
-
- private suspend fun loadCard() {
- val selectedUri = uri
- if (selectedUri == null) {
- withContext(Dispatchers.Main) {
- Toast.makeText(
- this@CompanionImportCardActivity,
- "No card file selected.",
- Toast.LENGTH_SHORT,
- ).show()
- }
- importState.value = ImportState.NameOrUnique
- return
- }
- val dimReader = DimReader()
- val inputStream = contentResolver.openInputStream(selectedUri)
- if (inputStream == null) {
- withContext(Dispatchers.Main) {
- Toast.makeText(
- this@CompanionImportCardActivity,
- "Unable to open selected card file.",
- Toast.LENGTH_LONG,
- ).show()
- }
- importState.value = ImportState.NameOrUnique
- return
- }
- inputStream.use {
- card = dimReader.readCard(it, false)
- if (validatedCardManager.isValidatedCard(card.header.dimId)) {
- importState.value = ImportState.ImportCard
- } else {
- importState.value = ImportState.UnlockCard
- }
- }
- }
-
- private suspend fun importCard(): Boolean {
- importPercent.value = 0
- importStatus.value = "Preparing card"
- val channelClient = Wearable.getChannelClient(this)
- val nodes = Wearable.getNodeClient(this).connectedNodes.await()
- Timber.i("Card transfer start: connectedNodes=${nodes.size}, ids=${nodes.joinToString { it.id }}")
- if (nodes.isEmpty()) {
- importStatus.value = getString(R.string.companion_firmware_no_watch)
- withContext(Dispatchers.Main) {
- Toast.makeText(
- this@CompanionImportCardActivity,
- getString(R.string.companion_firmware_no_watch),
- Toast.LENGTH_LONG,
- ).show()
- }
- return false
- }
-
- val cardWriter = DimWriter()
- val cardPayload = ByteArrayOutputStream().use { outputStream ->
- cardWriter.writeCard(card, outputStream)
- outputStream.toByteArray()
- }
- val cardNameBytes = cardName.value.toByteArray(Charset.defaultCharset())
- val metadataLength = cardNameBytes.size + 1 + 1 + 1 + 4
- val totalBytes = (metadataLength + cardPayload.size).toLong() * nodes.size
- var transferredBytes = 0L
- var successfulTransfers = 0
- val failedNodes = mutableListOf()
- for (node in nodes) {
- val nodeName = node.displayName.takeIf { it.isNotBlank() } ?: node.id
- importStatus.value = "Connecting to $nodeName"
- try {
- Timber.i("Opening card channel for nodeId=${node.id}, displayName=${node.displayName}")
- val channel = channelClient.openChannel(node.id, ChannelTypes.CARD_DATA).await()
- channelClient.getOutputStream(channel).await().use { os ->
- val output = DataOutputStream(os)
- output.write(cardNameBytes)
- output.writeByte(0)
- output.writeByte(if (uniqueSprites.value) 1 else 0)
- output.writeByte(if (convertToBem.value) 1 else 0)
- output.writeInt(cardPayload.size)
- transferredBytes += metadataLength
- val metadataPercent = if (totalBytes == 0L) 0 else ((transferredBytes * 100) / totalBytes).toInt().coerceIn(0, 100)
- importStatus.value = "Sending card to $nodeName"
- importPercent.value = metadataPercent
-
- val buffer = ByteArray(4096)
- var bytesRead: Int
- cardPayload.inputStream().use { payloadInput ->
- while (payloadInput.read(buffer).also { bytesRead = it } >= 0) {
- output.write(buffer, 0, bytesRead)
- transferredBytes += bytesRead
- val transferPercent = if (totalBytes == 0L) 0 else ((transferredBytes * 100) / totalBytes).toInt().coerceIn(0, 100)
- importPercent.value = transferPercent
- }
- }
- output.flush()
- }
- channelClient.close(channel).await()
- successfulTransfers++
- Timber.i("Card transfer complete for nodeId=${node.id}, card=${cardName.value}, payloadBytes=${cardPayload.size}")
- } catch (error: Exception) {
- failedNodes.add("$nodeName: ${error.message ?: "unknown error"}")
- Timber.e(error, "Card transfer failed for nodeId=${node.id}, displayName=${node.displayName}")
- }
- }
-
- if (successfulTransfers == 0) {
- importStatus.value = "Transfer failed"
- importPercent.value = 0
- withContext(Dispatchers.Main) {
- val reason = failedNodes.firstOrNull() ?: "No watch accepted the transfer"
- Toast.makeText(this@CompanionImportCardActivity, "Card transfer failed: $reason", Toast.LENGTH_LONG).show()
- }
- return false
- }
-
- importStatus.value = "Transfer complete"
- importPercent.value = 100
- if (failedNodes.isNotEmpty()) {
- withContext(Dispatchers.Main) {
- Toast.makeText(
- this@CompanionImportCardActivity,
- "Sent to $successfulTransfers watch(es); ${failedNodes.size} failed.",
- Toast.LENGTH_LONG,
- ).show()
- }
- }
-
- Timber.i("Companion-style card import sent to watch for ${cardName.value}; success=$successfulTransfers failures=${failedNodes.size}")
- return true
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/CompanionValidateCardActivity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/CompanionValidateCardActivity.kt
deleted file mode 100644
index dcae9a5..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/CompanionValidateCardActivity.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.card
-
-import android.content.Intent
-import android.nfc.NfcAdapter
-import android.nfc.Tag
-import android.nfc.tech.MifareUltralight
-import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.provider.Settings
-import android.widget.Toast
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.res.stringResource
-import com.github.nacabaro.vbhelper.R
-import com.github.nacabaro.vbhelper.companion.ui.CompanionLoading
-import kotlinx.coroutines.flow.MutableStateFlow
-import timber.log.Timber
-
-class CompanionValidateCardActivity : ComponentActivity(), NfcAdapter.ReaderCallback {
- companion object {
- const val CARD_VALIDATED_KEY = "CARD_VALIDATED"
- }
-
- enum class CardValidationState {
- WaitingForVBConnect,
- ValidateCardOnVB,
- Success,
- }
-
- private lateinit var nfcAdapter: NfcAdapter
- private val validationStateFlow = MutableStateFlow(CardValidationState.WaitingForVBConnect)
- private var cardIdToValidate: UShort = 0u
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
- val inputCardId = intent.getIntExtra(CARD_VALIDATED_KEY, -1)
- if (inputCardId == -1) {
- Timber.e("CompanionValidateCardActivity called without card number!")
- finish()
- return
- }
- cardIdToValidate = inputCardId.toUShort()
- if (maybeNfcAdapter == null) {
- Toast.makeText(this, getString(R.string.scan_no_nfc_on_device), Toast.LENGTH_SHORT).show()
- finish()
- return
- }
- nfcAdapter = maybeNfcAdapter
- setContent {
- DisplayContent()
- }
- }
-
- @Composable
- fun DisplayContent() {
- val validationState by validationStateFlow.collectAsState()
- when (validationState) {
- CardValidationState.WaitingForVBConnect -> {
- CompanionLoading(loadingText = stringResource(R.string.companion_validation_connect_vb))
- }
- CardValidationState.ValidateCardOnVB -> {
- CompanionLoading(loadingText = stringResource(R.string.companion_validation_insert_card))
- }
- CardValidationState.Success -> {
- LaunchedEffect(true) {
- Handler(Looper.getMainLooper()).postDelayed({
- val intent = Intent().apply {
- putExtra(CARD_VALIDATED_KEY, cardIdToValidate.toInt())
- }
- setResult(RESULT_OK, intent)
- finish()
- }, 1000)
- }
- Text(text = stringResource(R.string.companion_validation_success))
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- if (!nfcAdapter.isEnabled) {
- showWirelessSettings()
- } else {
- nfcAdapter.enableReaderMode(this, this, NfcAdapter.FLAG_READER_NFC_A, Bundle())
- }
- }
-
- override fun onPause() {
- super.onPause()
- if (this::nfcAdapter.isInitialized) {
- nfcAdapter.disableReaderMode(this)
- }
- }
-
- private fun showWirelessSettings() {
- Toast.makeText(this, getString(R.string.scan_nfc_must_be_enabled), Toast.LENGTH_SHORT).show()
- startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
- }
-
- override fun onTagDiscovered(tag: Tag?) {
- if (tag == null) {
- Timber.w("Tag discovery returned null tag")
- return
- }
- when (validationStateFlow.value) {
- CardValidationState.WaitingForVBConnect -> {
- val nfcData = MifareUltralight.get(tag)
- if (nfcData == null) {
- Timber.w("Unsupported NFC tech for validation")
- return
- }
- val writeSuccessful = runCatching {
- nfcData.connect()
- nfcData.use {
- val vbData = VBNfcData(nfcData)
- vbData.writeCardCheck(nfcData, cardIdToValidate)
- }
- true
- }.onFailure {
- Timber.e(it, "Failed during first validation NFC step")
- runOnUiThread {
- Toast.makeText(
- this,
- "Failed to validate card on bracelet. Please retry.",
- Toast.LENGTH_SHORT,
- ).show()
- }
- }.getOrDefault(false)
- if (writeSuccessful) {
- validationStateFlow.value = CardValidationState.ValidateCardOnVB
- }
- }
- CardValidationState.ValidateCardOnVB -> {
- val nfcData = MifareUltralight.get(tag)
- if (nfcData == null) {
- Timber.w("Unsupported NFC tech for validation")
- return
- }
- runCatching {
- nfcData.connect()
- nfcData.use {
- val vbData = VBNfcData(nfcData)
- if (vbData.wasCardIdValidated(cardIdToValidate)) {
- validationStateFlow.value = CardValidationState.Success
- }
- }
- }.onFailure {
- Timber.e(it, "Failed during second validation NFC step")
- }
- }
- else -> Timber.w("Incorrect state")
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/VBNfcData.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/VBNfcData.kt
deleted file mode 100644
index 224c084..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/card/VBNfcData.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.card
-
-import android.nfc.tech.MifareUltralight
-import kotlin.experimental.and
-import kotlin.experimental.or
-
-class VBNfcData(nfcData: MifareUltralight) {
- companion object {
- private const val ITEM_ID_BE: UShort = 4u
- private const val STATUS_READY_FLAG: Byte = 0b00000001
- private const val STATUS_DIM_READY_FLAG: Byte = 0b00000010
- private val STATUS_DIM_IS_READY = STATUS_READY_FLAG or STATUS_DIM_READY_FLAG
- private const val OPERATION_READY: Byte = 1
- private const val OPERATION_CHECK_DIM: Byte = 3
- }
-
- val itemId: UShort
- val status: Byte
- val operation: Byte
- val dimId: UShort
-
- init {
- val readData = nfcData.transceive(byteArrayOf(0x30, 0x04))
- itemId = readData.getUInt16(4, Endian.Big)
- status = readData[8]
- if (itemId == ITEM_ID_BE) {
- operation = readData[9]
- dimId = readData.getUInt16(10, Endian.Big)
- } else {
- dimId = readData[9].toUShort()
- operation = readData[10]
- }
- }
-
- fun writeCardCheck(nfcData: MifareUltralight, dimId: UShort) {
- if (itemId == ITEM_ID_BE) {
- val dimData = dimId.toByteArray(endian = Endian.Big)
- nfcData.writePage(6, byteArrayOf(STATUS_READY_FLAG, OPERATION_CHECK_DIM, dimData[0], dimData[1]))
- } else {
- nfcData.writePage(6, byteArrayOf(STATUS_READY_FLAG, dimId.toByte(), OPERATION_CHECK_DIM, 0))
- }
- }
-
- fun wasCardIdValidated(dimId: UShort): Boolean {
- return dimId == this.dimId && operation == OPERATION_READY && (status and STATUS_DIM_IS_READY == STATUS_DIM_IS_READY)
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/common/ChannelTypes.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/common/ChannelTypes.kt
deleted file mode 100644
index 3a76ee7..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/common/ChannelTypes.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.common
-
-object ChannelTypes {
- const val CARD_DATA = "com.github.cfogrady.vitalwear/CARD_DATA"
- const val FIRMWARE_DATA = "com.github.cfogrady.vitalwear/FIRMWARE_DATA"
- const val CHARACTER_DATA = "com.github.cfogrady.vitalwear/CHARACTER_DATA"
- const val SEND_LOGS_REQUEST = "SEND_LOGS_REQUEST"
- const val LOGS_DATA = "LOG_DATA"
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/firmware/CompanionFirmwareImportActivity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/firmware/CompanionFirmwareImportActivity.kt
deleted file mode 100644
index b843189..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/firmware/CompanionFirmwareImportActivity.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.firmware
-
-import android.net.Uri
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.lifecycle.lifecycleScope
-import com.github.nacabaro.vbhelper.R
-import com.github.nacabaro.vbhelper.companion.common.ChannelTypes
-import com.github.nacabaro.vbhelper.companion.ui.CompanionLoading
-import com.google.android.gms.wearable.Wearable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.tasks.await
-import kotlinx.coroutines.withContext
-import timber.log.Timber
-import java.io.DataOutputStream
-
-class CompanionFirmwareImportActivity : ComponentActivity() {
- enum class FirmwareImportState {
- PickFirmware,
- LoadFirmware,
- }
-
- private val importState = MutableStateFlow(FirmwareImportState.PickFirmware)
- private val transferPercent = MutableStateFlow(0)
- private val transferStatus = MutableStateFlow("Preparing transfer")
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val firmwareImportActivity = buildFirmwareFilePickLauncher()
- setContent {
- BuildScreen(firmwareImportActivity)
- }
- }
-
- @Composable
- fun BuildScreen(firmwareImportActivity: ActivityResultLauncher>) {
- val state by importState.collectAsState()
- val percent by transferPercent.collectAsState()
- val status by transferStatus.collectAsState()
- if (state == FirmwareImportState.PickFirmware) {
- LaunchedEffect(true) {
- firmwareImportActivity.launch(arrayOf("*/*"))
- }
- }
- val loadingText = if (state == FirmwareImportState.PickFirmware) {
- getString(R.string.companion_loading_select_firmware)
- } else {
- "$status ($percent%)"
- }
- CompanionLoading(loadingText = loadingText)
- }
-
- private fun buildFirmwareFilePickLauncher(): ActivityResultLauncher> {
- return registerForActivityResult(ActivityResultContracts.OpenDocument()) {
- importState.value = FirmwareImportState.LoadFirmware
- if (it == null) {
- finish()
- } else {
- Timber.i("Path: ${it.path}")
- importFirmware(it)
- }
- }
- }
-
- private fun importFirmware(uri: Uri) {
- transferPercent.value = 0
- transferStatus.value = "Reading firmware file"
- val channelClient = Wearable.getChannelClient(this)
- val nodeListTask = Wearable.getNodeClient(this).connectedNodes
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- val nodes = nodeListTask.await()
- if (nodes.isEmpty()) {
- transferStatus.value = getString(R.string.companion_firmware_no_watch)
- withContext(Dispatchers.Main) {
- Toast.makeText(this@CompanionFirmwareImportActivity, getString(R.string.companion_firmware_no_watch), Toast.LENGTH_LONG).show()
- }
- importState.value = FirmwareImportState.PickFirmware
- return@launch
- }
-
- val firmware = contentResolver.openInputStream(uri)?.use { input ->
- input.readBytes()
- } ?: throw IllegalStateException("Unable to read selected firmware file")
-
- transferPercent.value = 5
- transferStatus.value = "Sending firmware"
- val totalBytes = (firmware.size + 4).toLong() * nodes.size
- var transferredBytes = 0L
- for (node in nodes) {
- val channel = channelClient.openChannel(node.id, ChannelTypes.FIRMWARE_DATA).await()
- try {
- channelClient.getOutputStream(channel).await().use {
- val output = DataOutputStream(it)
- output.writeInt(firmware.size)
- transferredBytes += 4
- transferPercent.value = if (totalBytes == 0L) 0 else ((transferredBytes * 100) / totalBytes).toInt().coerceIn(0, 100)
- val buffer = ByteArray(4096)
- var bytesRead: Int
- firmware.inputStream().use { firmwareInput ->
- while (firmwareInput.read(buffer).also { bytesRead = it } >= 0) {
- output.write(buffer, 0, bytesRead)
- transferredBytes += bytesRead
- transferPercent.value = if (totalBytes == 0L) 0 else ((transferredBytes * 100) / totalBytes).toInt().coerceIn(0, 100)
- }
- }
- output.flush()
- }
- } finally {
- runCatching { channelClient.close(channel).await() }
- }
- }
- transferStatus.value = "Transfer complete"
- transferPercent.value = 100
- withContext(Dispatchers.Main) {
- Toast.makeText(this@CompanionFirmwareImportActivity, getString(R.string.companion_firmware_sent_success), Toast.LENGTH_SHORT).show()
- finish()
- }
- } catch (e: Exception) {
- Timber.e(e, "Failed to send firmware to watch")
- transferStatus.value = "Transfer failed: ${e.message ?: "unknown"}"
- withContext(Dispatchers.Main) {
- Toast.makeText(this@CompanionFirmwareImportActivity, "Firmware transfer failed: ${e.message ?: "unknown error"}", Toast.LENGTH_LONG).show()
- }
- importState.value = FirmwareImportState.PickFirmware
- }
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/logging/TinyLogTree.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/logging/TinyLogTree.kt
deleted file mode 100644
index 579c593..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/logging/TinyLogTree.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.logging
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.util.Log
-import org.tinylog.Logger
-import org.tinylog.configuration.Configuration
-import org.tinylog.provider.ProviderRegistry
-import timber.log.Timber
-import java.io.File
-
-@SuppressLint("LogNotTimber")
-class TinyLogTree(context: Context) : Timber.DebugTree() {
- companion object {
- private val logFileRegexp = Regex("""log_(\d{4}-\d{2}-\d{2})_(\d+)\.txt""")
- private const val START_DATE = "0000-00-00"
-
- fun shutdown() {
- ProviderRegistry.getLoggingProvider().shutdown()
- }
-
- private fun setupRollingLogs(logsDir: String) {
- Configuration.set("writer", "rolling file")
- Configuration.set("writer.level", "info")
- Configuration.set("writer.backups", "3")
- Configuration.set("writer.format", "{date: yyyy-MM-dd HH:mm:ss.SSS}{pipe}{tag}{pipe}{level}{pipe}{message}")
- Configuration.set("writer.file", "$logsDir/log_{date:yyyy-MM-dd}_{count}.txt")
- Configuration.set("writer.policies", "daily, size: 10mb")
- Configuration.set("writer.buffered", "true")
- }
-
- private fun getLogsDir(context: Context): File = File(context.filesDir, "logs")
-
- fun getMostRecentLogFile(context: Context): File? {
- val logsDir = getLogsDir(context)
- val files = logsDir.listFiles() ?: return null
- return getMostRecentLogFileFromFiles(files)
- }
-
- internal fun getMostRecentLogFileFromFiles(files: Array): File? {
- var date = START_DATE
- var count = 0
- var mostRecentFile: File? = null
- for (child in files) {
- val matchResult = logFileRegexp.find(child.name) ?: continue
- val childDate = matchResult.groups[1]?.value ?: START_DATE
- val childCount = matchResult.groups[2]?.value?.toInt() ?: 0
- if (childDate > date || (childDate == date && childCount > count)) {
- mostRecentFile = child
- date = childDate
- count = childCount
- }
- }
- Log.i("TinyLogTree", "Most Recent Log: $mostRecentFile")
- return mostRecentFile
- }
- }
-
- init {
- val logsDir = getLogsDir(context).absolutePath
- setupRollingLogs(logsDir)
- Configuration.set("writingthread", "true")
- Log.i("TinyLogTree", "Log Dir: $logsDir")
- }
-
- override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
- val taggedLogger = Logger.tag(tag)
- when (priority) {
- Log.VERBOSE, Log.DEBUG -> taggedLogger.debug { message }
- Log.INFO -> taggedLogger.info { message }
- Log.WARN -> taggedLogger.warn { message }
- Log.ERROR, Log.ASSERT -> taggedLogger.error { message }
- else -> taggedLogger.error { "Invalid priority $priority For Message! $message" }
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/CompanionLogService.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/CompanionLogService.kt
deleted file mode 100644
index ff4e694..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/CompanionLogService.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.logs
-
-import android.annotation.SuppressLint
-import android.app.Activity
-import android.content.ClipData
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.util.Log
-import android.widget.Toast
-import androidx.core.content.FileProvider
-import com.github.nacabaro.vbhelper.R
-import com.github.nacabaro.vbhelper.companion.common.ChannelTypes
-import com.google.android.gms.wearable.CapabilityClient
-import com.google.android.gms.wearable.ChannelClient
-import com.google.android.gms.wearable.ChannelClient.ChannelCallback
-import com.google.android.gms.wearable.Wearable
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.tasks.await
-import timber.log.Timber
-import java.io.File
-
-class CompanionLogService {
- companion object {
- private const val TAG = "CompanionLogService"
- }
-
- var initializedActivity: Activity? = null
-
- fun sendLogFile(context: Context, file: File) {
- initializedActivity?.let {
- sendLogFile(context, file, it)
- initializedActivity?.finish()
- }
- initializedActivity = null
- }
-
- @SuppressLint("LogNotTimber")
- fun sendLogFile(context: Context, file: File, activity: Activity) {
- val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
- data = Uri.parse("mailto:")
- putExtra(Intent.EXTRA_EMAIL, arrayOf("vitalwear-developers@googlegroups.com", "taveonnelsonn@gmail.com"))
- putExtra(Intent.EXTRA_SUBJECT, "VBHelper Log")
- putExtra(Intent.EXTRA_TEXT, "Please take a look at these logs to help diagnose the below issue:\n")
- }
- val fileUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
- emailIntent.putExtra(Intent.EXTRA_STREAM, fileUri)
- emailIntent.clipData = ClipData.newRawUri("", fileUri)
- emailIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
- Log.i(TAG, "Starting activity to send email")
- activity.startActivity(Intent.createChooser(emailIntent, "Select Email Client"))
- }
-
- fun receiveFile(context: Context, channel: ChannelClient.Channel) {
- val channelClient = Wearable.getChannelClient(context)
- val logs = File(context.filesDir, "logs")
- if (!logs.exists()) {
- logs.mkdirs()
- }
- val file = File(logs, "watch_log.txt")
- channelClient.registerChannelCallback(channel, object : ChannelCallback() {
- override fun onInputClosed(channel: ChannelClient.Channel, closeReason: Int, appErrorCode: Int) {
- super.onInputClosed(channel, closeReason, appErrorCode)
- if (closeReason != ChannelCallback.CLOSE_REASON_NORMAL) {
- Timber.w("Failed to receive file. Close Reason: ${closeReasonString(closeReason)}")
- } else {
- Timber.i("Successfully downloaded log file. Input closed.")
- sendLogFile(context, file)
- }
- channelClient.close(channel)
- }
- })
- channelClient.receiveFile(channel, Uri.fromFile(file), false).apply {
- addOnFailureListener {
- Toast.makeText(context, context.getString(R.string.companion_logs_receive_failed), Toast.LENGTH_SHORT).show()
- initializedActivity?.finish()
- initializedActivity = null
- }
- }
- }
-
- fun fetchWatchLogs(callingActivity: Activity) {
- val messageClient = Wearable.getMessageClient(callingActivity)
- val capabilityInfoTask = Wearable.getCapabilityClient(callingActivity)
- .getCapability(ChannelTypes.SEND_LOGS_REQUEST, CapabilityClient.FILTER_REACHABLE)
- CoroutineScope(Dispatchers.IO).launch {
- val capabilityInfo = capabilityInfoTask.await()
- if (capabilityInfo.nodes.isEmpty()) {
- CoroutineScope(Dispatchers.Main).launch {
- Toast.makeText(callingActivity, callingActivity.getString(R.string.companion_firmware_no_watch), Toast.LENGTH_SHORT).show()
- callingActivity.finish()
- }
- return@launch
- }
- for (node in capabilityInfo.nodes) {
- messageClient.sendMessage(node.id, ChannelTypes.SEND_LOGS_REQUEST, null).apply {
- addOnFailureListener {
- Toast.makeText(callingActivity, callingActivity.getString(R.string.companion_logs_failed_request), Toast.LENGTH_SHORT).show()
- callingActivity.finish()
- }
- addOnSuccessListener {
- initializedActivity = callingActivity
- }
- }
- }
- }
- }
-
- private fun closeReasonString(closeReason: Int): String {
- return when (closeReason) {
- ChannelCallback.CLOSE_REASON_NORMAL -> "Normal"
- ChannelCallback.CLOSE_REASON_LOCAL_CLOSE -> "Closed Locally"
- ChannelCallback.CLOSE_REASON_REMOTE_CLOSE -> "Closed Remotely"
- ChannelCallback.CLOSE_REASON_DISCONNECTED -> "Disconnected"
- ChannelCallback.CLOSE_REASON_CONNECTION_TIMEOUT -> "Timeout"
- else -> "Unknown"
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/CompanionWatchLogsActivity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/CompanionWatchLogsActivity.kt
deleted file mode 100644
index 3a43336..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/CompanionWatchLogsActivity.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.logs
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.res.stringResource
-import com.github.nacabaro.vbhelper.R
-import com.github.nacabaro.vbhelper.companion.ui.CompanionConfirmation
-import com.github.nacabaro.vbhelper.companion.ui.CompanionLoading
-import com.github.nacabaro.vbhelper.di.VBHelper
-
-class CompanionWatchLogsActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val activity = this
- setContent {
- var accepted by remember { mutableStateOf(false) }
- if (!accepted) {
- CompanionConfirmation(prompt = stringResource(R.string.companion_logs_warning)) {
- if (it) {
- accepted = true
- } else {
- finish()
- }
- }
- } else {
- CompanionLoading(loadingText = stringResource(R.string.settings_companion_send_watch_logs_title))
- LaunchedEffect(true) {
- (application as VBHelper).companionLogService.fetchWatchLogs(activity)
- }
- }
- }
- }
-}
-
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/WatchCommunicationService.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/WatchCommunicationService.kt
deleted file mode 100644
index d19bfa9..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/WatchCommunicationService.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.logs
-
-import com.github.nacabaro.vbhelper.companion.common.ChannelTypes
-import com.github.nacabaro.vbhelper.di.VBHelper
-import com.google.android.gms.wearable.ChannelClient
-import com.google.android.gms.wearable.WearableListenerService
-import timber.log.Timber
-
-class WatchCommunicationService : WearableListenerService() {
- override fun onChannelOpened(channel: ChannelClient.Channel) {
- super.onChannelOpened(channel)
- when (channel.path) {
- ChannelTypes.LOGS_DATA -> {
- val logService = (application as VBHelper).companionLogService
- logService.receiveFile(this, channel)
- }
- else -> Timber.i("Unknown channel: ${channel.path}")
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/transfer/CompanionCharacterImportActivity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/transfer/CompanionCharacterImportActivity.kt
deleted file mode 100644
index 18f6a6e..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/transfer/CompanionCharacterImportActivity.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.transfer
-
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.ComponentActivity
-import com.github.nacabaro.vbhelper.R
-
-/**
- * Compatibility endpoint kept for legacy links/intents from older companion flows.
- * Character transfer remains watch-driven and is not supported from phone companion UI.
- */
-class CompanionCharacterImportActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- Toast.makeText(
- this,
- getString(R.string.companion_transfer_disabled_message),
- Toast.LENGTH_LONG,
- ).show()
- finish()
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/ui/CompanionConfirmation.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/ui/CompanionConfirmation.kt
deleted file mode 100644
index 4fbd602..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/ui/CompanionConfirmation.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.ui
-
-import androidx.compose.foundation.layout.Arrangement
-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
-import androidx.compose.material3.Button
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-
-@Composable
-fun CompanionConfirmation(
- prompt: String,
- onResult: (Boolean) -> Unit,
-) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(24.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(text = prompt, modifier = Modifier.padding(bottom = 24.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceEvenly,
- ) {
- Button(onClick = { onResult(true) }) {
- Text(text = "Ok")
- }
- Button(onClick = { onResult(false) }) {
- Text(text = "Cancel")
- }
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/ui/CompanionLoading.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/ui/CompanionLoading.kt
deleted file mode 100644
index 8326013..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/ui/CompanionLoading.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.ui
-
-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.CircularProgressIndicator
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-
-@Composable
-fun CompanionLoading(
- loadingText: String,
-) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(24.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- CircularProgressIndicator()
- Text(
- text = loadingText,
- modifier = Modifier.padding(top = 16.dp),
- )
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/CompanionValidatedDatabase.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/CompanionValidatedDatabase.kt
deleted file mode 100644
index ac12102..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/CompanionValidatedDatabase.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.validation
-
-import androidx.room.Database
-import androidx.room.RoomDatabase
-
-@Database(
- entities = [ValidatedCardEntity::class],
- version = 1,
- exportSchema = false,
-)
-abstract class CompanionValidatedDatabase : RoomDatabase() {
- abstract fun validatedCardDao(): ValidatedCardDao
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardDao.kt
deleted file mode 100644
index 39fb0a4..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardDao.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.validation
-
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-
-@Dao
-interface ValidatedCardDao {
- @Query("select cardId from ${ValidatedCardEntity.TABLE}")
- suspend fun getIds(): List
-
- @Insert(onConflict = OnConflictStrategy.IGNORE)
- suspend fun insert(validatedCardEntity: ValidatedCardEntity)
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardEntity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardEntity.kt
deleted file mode 100644
index b794d67..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardEntity.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.validation
-
-import androidx.room.Entity
-
-@Entity(tableName = ValidatedCardEntity.TABLE, primaryKeys = ["cardId"])
-data class ValidatedCardEntity(
- val cardId: Int,
-) {
- companion object {
- const val TABLE = "validated"
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardManager.kt
deleted file mode 100644
index fbb178f..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardManager.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.github.nacabaro.vbhelper.companion.validation
-
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-
-class ValidatedCardManager(
- private val validatedCardDao: ValidatedCardDao,
-) {
- private val mutex = Mutex()
- private var validatedIds: Set? = null
-
- private suspend fun ensureLoaded(): Set {
- validatedIds?.let { return it }
- return mutex.withLock {
- validatedIds ?: validatedCardDao.getIds().toSet().also { validatedIds = it }
- }
- }
-
- suspend fun addValidatedCard(cardId: Int) {
- mutex.withLock {
- val current = validatedIds ?: validatedCardDao.getIds().toSet()
- if (!current.contains(cardId)) {
- validatedCardDao.insert(ValidatedCardEntity(cardId))
- validatedIds = current + cardId
- } else {
- validatedIds = current
- }
- }
- }
-
- suspend fun isValidatedCard(cardId: Int): Boolean {
- return ensureLoaded().contains(cardId)
- }
-}
-
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 b2d61e0..c87a1f7 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardDao.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardDao.kt
@@ -19,9 +19,6 @@ interface CardDao {
@Query("SELECT * FROM Card WHERE id = :id")
fun getCardById(id: Long): Card?
- @Query("SELECT * FROM Card WHERE LOWER(name) = LOWER(:name) LIMIT 1")
- fun getCardByName(name: String): Card?
-
@Query(
"""
SELECT ca.*
@@ -33,18 +30,6 @@ interface CardDao {
)
fun getCardByCharacterId(id: Long): Flow
- @Query(
- """
- SELECT ca.*
- FROM Card ca
- JOIN CardCharacter ch ON ca.id = ch.cardId
- JOIN UserCharacter uc ON ch.id = uc.charId
- WHERE uc.id = :id
- LIMIT 1
- """
- )
- fun getCardByCharacterIdSync(id: Long): Card?
-
@Query("UPDATE Card SET name = :newName WHERE id = :id")
suspend fun renameCard(id: Int, newName: String)
@@ -61,4 +46,4 @@ interface CardDao {
WHERE cc.id = :charaId
""")
fun getCardIconByCharaId(charaId: Long): Flow
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt
index 93d86c3..3fde614 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt
@@ -24,9 +24,6 @@ interface CardProgressDao {
)
fun getCardProgress(cardId: Long): Flow
- @Query("SELECT currentStage FROM CardProgress WHERE cardId = :cardId LIMIT 1")
- fun getCardProgressSync(cardId: Long): Int?
-
@Insert
fun insertCardProgress(cardProgress: CardProgress)
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterTransferPolicyDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterTransferPolicyDao.kt
deleted file mode 100644
index 7f65e07..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterTransferPolicyDao.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.nacabaro.vbhelper.daos
-
-import androidx.room.Dao
-import androidx.room.Query
-import androidx.room.Upsert
-import com.github.nacabaro.vbhelper.domain.device_data.CharacterTransferPolicy
-
-@Dao
-interface CharacterTransferPolicyDao {
- @Query("SELECT * FROM CharacterTransferPolicy WHERE characterId = :characterId")
- fun getByCharacterId(characterId: Long): CharacterTransferPolicy?
-
- @Upsert
- fun upsert(policy: CharacterTransferPolicy)
-}
-
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/TransferSeenDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/TransferSeenDao.kt
deleted file mode 100644
index fb337dd..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/TransferSeenDao.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.github.nacabaro.vbhelper.daos
-
-import com.github.cfogrady.vitalwear.common.data.SharedTransferSeenDao
-
-@Deprecated("Use SharedTransferSeenDao from the shared transfer database.")
-typealias TransferSeenDao = SharedTransferSeenDao
-
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 284bca9..986ce01 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/UserCharacterDao.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/UserCharacterDao.kt
@@ -55,22 +55,6 @@ interface UserCharacterDao {
)
fun getTransformationHistory(monId: Long): Flow>
- @Query(
- """
- SELECT
- t.stageId as stageId,
- c.charaIndex AS monIndex,
- ca.name as cardName,
- t.transformationDate AS transformationDate
- FROM TransformationHistory t
- JOIN CardCharacter c ON c.id = t.stageId
- JOIN Card ca ON ca.id = c.cardId
- WHERE t.monId = :monId
- ORDER BY t.transformationDate ASC, t.id ASC
- """
- )
- suspend fun getTransformationHistoryForExport(monId: Long): List
-
@Query(
"""
SELECT
@@ -250,4 +234,4 @@ interface UserCharacterDao {
"""
)
suspend fun getVBDimCharacters(): List
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/VitalWearSettingsDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/VitalWearSettingsDao.kt
deleted file mode 100644
index 282fd05..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/VitalWearSettingsDao.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.nacabaro.vbhelper.daos
-
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import com.github.nacabaro.vbhelper.domain.device_data.VitalWearCharacterSettings
-
-@Dao
-interface VitalWearSettingsDao {
- @Query("SELECT * FROM VitalWearCharacterSettings WHERE characterId = :characterId LIMIT 1")
- suspend fun getByCharacterId(characterId: Long): VitalWearCharacterSettings?
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun upsert(settings: VitalWearCharacterSettings)
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/CharacterTransferPolicy.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/CharacterTransferPolicy.kt
deleted file mode 100644
index 0a541d6..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/CharacterTransferPolicy.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.github.nacabaro.vbhelper.domain.device_data
-
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.PrimaryKey
-import com.github.nacabaro.vbhelper.transfer.ExportFormat
-import com.github.nacabaro.vbhelper.transfer.TransferTarget
-import com.github.nacabaro.vbhelper.transfer.TransferTransport
-import com.github.nacabaro.vbhelper.utils.DeviceType
-
-@Entity(
- foreignKeys = [
- ForeignKey(
- entity = UserCharacter::class,
- parentColumns = ["id"],
- childColumns = ["characterId"],
- onDelete = ForeignKey.CASCADE,
- )
- ]
-)
-data class CharacterTransferPolicy(
- @PrimaryKey val characterId: Long,
- val nativeDeviceType: DeviceType,
- val preferredHceExportFormat: ExportFormat,
- val preferredNfcaExportFormat: ExportFormat,
- val lastObservedImportFormat: ExportFormat?,
- val lastTransferTransport: TransferTransport?,
- val lastTransferTarget: TransferTarget?,
- val preserveVbRoundTrip: Boolean,
- val preserveBeRoundTrip: Boolean,
-)
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/UnifiedCharacter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/UnifiedCharacter.kt
deleted file mode 100644
index 0acb709..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/UnifiedCharacter.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.github.nacabaro.vbhelper.domain.device_data
-
-import com.github.nacabaro.vbhelper.utils.DeviceType
-
-/**
- * UnifiedCharacter provides a single access point for common character data
- * and bridges the specific data for VB and BE characters.
- */
-data class UnifiedCharacter(
- val userCharacter: UserCharacter,
- val vbData: VBCharacterData? = null,
- val beData: BECharacterData? = null
-) {
- val characterType: DeviceType get() = userCharacter.characterType
-
- fun isVB() = characterType == DeviceType.VBDevice
- fun isBE() = characterType == DeviceType.BEDevice
-
- // Unified accessors for core stats
- val trophies: Int get() = userCharacter.trophies
- val ageInDays: Int get() = userCharacter.ageInDays
- val mood: Int get() = userCharacter.mood
- val vitalPoints: Int get() = userCharacter.vitalPoints
- val injuryStatus = userCharacter.injuryStatus
-
- // BE specific accessors with defaults
- val trainingHp: Int get() = beData?.trainingHp ?: 0
- val trainingAp: Int get() = beData?.trainingAp ?: 0
- val trainingBp: Int get() = beData?.trainingBp ?: 0
- val abilityRarity = beData?.abilityRarity
-}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/VitalWearCharacterSettings.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/VitalWearCharacterSettings.kt
deleted file mode 100644
index 845604f..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/VitalWearCharacterSettings.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.github.nacabaro.vbhelper.domain.device_data
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-
-@Entity
-data class VitalWearCharacterSettings(
- @PrimaryKey val characterId: Long,
- val trainingInBackground: Boolean = false,
- val allowedBattles: Int = 1,
- val accumulatedDailyInjuries: Int = 0,
-)
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/transfer/TransferSeenEntity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/transfer/TransferSeenEntity.kt
deleted file mode 100644
index be60828..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/transfer/TransferSeenEntity.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.github.nacabaro.vbhelper.domain.transfer
-
-import com.github.cfogrady.vitalwear.common.data.SharedTransferSeenEntity
-
-@Deprecated("Use SharedTransferSeenEntity from the shared transfer database.")
-typealias TransferSeenEntity = SharedTransferSeenEntity
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt b/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt
index 0131e6f..3cb7831 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt
@@ -52,13 +52,6 @@ object CharacterDtos {
val transformationDate: Long
)
- data class TransformationHistoryExport(
- val stageId: Long,
- val monIndex: Int,
- val cardName: String,
- val transformationDate: Long
- )
-
data class CardCharaProgress(
val id: Long,
val spriteIdle: ByteArray,
@@ -132,4 +125,4 @@ object CharacterDtos {
val discoveredOn: Long?,
val fusionAttribute: NfcCharacter.Attribute
)
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt
index 86b474d..7d207b2 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
@@ -54,7 +54,6 @@ data class AppNavigationHandlers(
@Composable
fun AppNavigation(
applicationNavigationHandlers: AppNavigationHandlers,
- initialRoute: String? = null
) {
val navController = rememberNavController()
@@ -65,7 +64,7 @@ fun AppNavigation(
) { contentPadding ->
NavHost(
navController = navController,
- startDestination = initialRoute ?: NavigationItems.Home.route,
+ startDestination = NavigationItems.Home.route,
enterTransition = {
fadeIn(
animationSpec = tween(200)
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 16e6728..96f4275 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
@@ -30,17 +30,10 @@ fun BottomNavigationBar(navController: NavController) {
label = { Text(text = stringResource(item.label)) },
selected = currentRoute == item.route,
onClick = {
- 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
- }
+ navController.navigate(item.route) {
+ popUpTo(navController.graph.startDestinationId) { saveState = true }
+ launchSingleTop = true
+ restoreState = true
}
}
)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationExtensions.kt b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationExtensions.kt
deleted file mode 100644
index e69de29..0000000
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 37551e8..0cacb5d 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt
@@ -2,7 +2,6 @@ package com.github.nacabaro.vbhelper.screens.homeScreens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -41,8 +40,6 @@ 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
@@ -139,66 +136,42 @@ fun HomeScreen(
Text(text = stringResource(R.string.adventure_empty_state))
}
} else {
- Column(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) {
- val cardIcon = BitmapData(
- bitmap = cardIconData!!.cardIcon,
- width = cardIconData!!.cardIconWidth,
- height = cardIconData!!.cardIconHeight
+ val cardIcon = BitmapData(
+ bitmap = cardIconData!!.cardIcon,
+ width = cardIconData!!.cardIconWidth,
+ height = cardIconData!!.cardIconHeight
+ )
+
+ if (activeMon!!.isBemCard && beData != null) {
+ BEBEmHomeScreen(
+ activeMon = activeMon!!,
+ beData = beData!!,
+ transformationHistory = transformationHistory,
+ contentPadding = contentPadding,
+ cardIcon = cardIcon
)
-
- if (activeMon!!.isBemCard && beData != null) {
- BEBEmHomeScreen(
- activeMon = activeMon!!,
- beData = beData!!,
- transformationHistory = transformationHistory,
- contentPadding = PaddingValues(0.dp),
- cardIcon = cardIcon
- )
- } else if (!activeMon!!.isBemCard && activeMon!!.characterType == DeviceType.BEDevice && beData != null) {
- BEDiMHomeScreen(
- activeMon = activeMon!!,
- beData = beData!!,
- transformationHistory = transformationHistory,
- contentPadding = PaddingValues(0.dp),
- cardIcon = cardIcon
- )
- } else if (vbData != null) {
- VBDiMHomeScreen(
- activeMon = activeMon!!,
- vbData = vbData!!,
- transformationHistory = transformationHistory,
- contentPadding = PaddingValues(0.dp),
- specialMissions = vbSpecialMissions,
- homeScreenController = homeScreenController,
- onClickCollect = { item, currency ->
- collectedItem = item
- collectedCurrency = currency
- },
- cardIcon = cardIcon
- )
- }
-
- Button(
- onClick = {
- try {
- val intent = VitalWearCharacterExporter(application, application.container.db)
- .buildShareIntent(activeMon!!.id)
- .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
- application.startActivity(intent)
- } catch (e: Exception) {
- Toast.makeText(
- application,
- "Could not send character to VitalWear: ${e.message}",
- Toast.LENGTH_LONG
- ).show()
- }
+ } else if (!activeMon!!.isBemCard && activeMon!!.characterType == DeviceType.BEDevice && beData != null) {
+ BEDiMHomeScreen(
+ activeMon = activeMon!!,
+ beData = beData!!,
+ transformationHistory = transformationHistory,
+ contentPadding = contentPadding,
+ cardIcon = cardIcon
+ )
+ } else if (vbData != null) {
+ VBDiMHomeScreen(
+ activeMon = activeMon!!,
+ vbData = vbData!!,
+ transformationHistory = transformationHistory,
+ contentPadding = contentPadding,
+ specialMissions = vbSpecialMissions,
+ homeScreenController = homeScreenController,
+ onClickCollect = { item, currency ->
+ collectedItem = item
+ collectedCurrency = currency
},
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Text(text = "Send to VitalWear")
- }
+ cardIcon = cardIcon
+ )
}
}
}
@@ -248,3 +221,5 @@ fun HomeScreen(
}
}
}
+
+
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/screens/UnifiedHomeScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/screens/UnifiedHomeScreen.kt
deleted file mode 100644
index e7db1bf..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/screens/UnifiedHomeScreen.kt
+++ /dev/null
@@ -1,241 +0,0 @@
-package com.github.nacabaro.vbhelper.screens.homeScreens.screens
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.res.stringResource
-import com.github.nacabaro.vbhelper.R
-import com.github.nacabaro.vbhelper.screens.homeScreens.dialogs.DeleteSpecialMissionDialog
-import com.github.nacabaro.vbhelper.components.CharacterEntry
-import com.github.nacabaro.vbhelper.components.ItemDisplay
-import com.github.nacabaro.vbhelper.components.SpecialMissionsEntry
-import com.github.nacabaro.vbhelper.components.TransformationHistoryCard
-import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
-import com.github.nacabaro.vbhelper.domain.device_data.UnifiedCharacter
-import com.github.nacabaro.vbhelper.dtos.CharacterDtos
-import com.github.nacabaro.vbhelper.dtos.ItemDtos
-import com.github.nacabaro.vbhelper.screens.homeScreens.HomeScreenControllerImpl
-import com.github.nacabaro.vbhelper.screens.itemsScreen.getIconResource
-import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl
-import com.github.nacabaro.vbhelper.utils.BitmapData
-import java.util.Locale
-
-@Composable
-fun UnifiedHomeScreen(
- activeMon: CharacterDtos.CharacterWithSprites,
- cardIcon: BitmapData,
- unifiedChar: UnifiedCharacter,
- specialMissions: List,
- transformationHistory: List,
- contentPadding: PaddingValues,
- homeScreenController: HomeScreenControllerImpl,
- onClickCollect: (ItemDtos.PurchasedItem?, Int?) -> Unit
-) {
- var selectedSpecialMissionId by remember { mutableStateOf(-1) }
-
- Column(
- modifier = Modifier
- .padding(top = contentPadding.calculateTopPadding())
- .verticalScroll(state = rememberScrollState())
- ) {
- // CORE SLOT: Always the same for VB and BE
- Row(modifier = Modifier.fillMaxWidth()) {
- CharacterEntry(
- icon = BitmapData(
- bitmap = activeMon.spriteIdle,
- width = activeMon.spriteWidth,
- height = activeMon.spriteHeight
- ),
- cardIcon = cardIcon,
- multiplier = 8,
- shape = androidx.compose.material.MaterialTheme.shapes.small,
- modifier = Modifier.weight(1f).aspectRatio(1f)
- )
- Column(modifier = Modifier.weight(0.5f).aspectRatio(0.5f)) {
- ItemDisplay(
- icon = R.drawable.baseline_vitals_24,
- textValue = activeMon.vitalPoints.toString(),
- definition = stringResource(R.string.home_vbdim_vitals),
- modifier = Modifier.weight(0.5f).aspectRatio(1f).padding(8.dp)
- )
- ItemDisplay(
- icon = R.drawable.baseline_trophy_24,
- textValue = activeMon.trophies.toString(),
- definition = stringResource(R.string.home_vbdim_trophies),
- modifier = Modifier.weight(0.5f).aspectRatio(1f).padding(8.dp)
- )
- }
- }
-
- // STATS ROW 1: mood and timing (VB timer vs BE training limit)
- Row(modifier = Modifier.fillMaxWidth()) {
- ItemDisplay(
- icon = R.drawable.baseline_mood_24,
- textValue = activeMon.mood.toString(),
- definition = stringResource(R.string.home_vbdim_mood),
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
-
- if (unifiedChar.isVB()) {
- val transformationCountdownInHours = activeMon.transformationCountdown / 60
- ItemDisplay(
- icon = R.drawable.baseline_next_24,
- textValue = when (transformationCountdownInHours) {
- 0 -> "${activeMon.transformationCountdown} m"
- else -> "$transformationCountdownInHours h"
- },
- definition = stringResource(R.string.home_vbdim_next_timer),
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
- } else {
- val timeInHours = (unifiedChar.beData?.remainingTrainingTimeInMinutes ?: 0) / 60
- ItemDisplay(
- icon = R.drawable.baseline_timer_24,
- textValue = "$timeInHours h",
- definition = "Training limit",
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
- }
-
- if (unifiedChar.isBE()) {
- ItemDisplay(
- icon = R.drawable.baseline_rank_24,
- textValue = (unifiedChar.beData?.rank ?: 0).toString(),
- definition = "Rank",
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
- } else {
- // For VB, maybe put a spacer or other stat here to keep row balance
- }
- }
-
- // STATS ROW 2: Battle stats and Items
- Row(modifier = Modifier.fillMaxWidth()) {
- val transformationCountdownInHours = activeMon.transformationCountdown / 60
- ItemDisplay(
- icon = R.drawable.baseline_next_24,
- textValue = when (transformationCountdownInHours) {
- 0 -> "${activeMon.transformationCountdown} m"
- else -> "$transformationCountdownInHours h"
- },
- definition = "Next timer",
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
-
- // Battle stats (Common)
- ItemDisplay(
- icon = R.drawable.baseline_swords_24,
- textValue = calculateWinPercent(activeMon.totalBattlesWon, activeMon.totalBattlesLost),
- definition = "Total battle win %",
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
- ItemDisplay(
- icon = R.drawable.baseline_swords_24,
- textValue = calculateWinPercent(activeMon.currentPhaseBattlesWon, activeMon.currentPhaseBattlesLost),
- definition = "Current phase win %",
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
-
- if (unifiedChar.isBE()) {
- val beData = unifiedChar.beData
- if (beData != null && beData.itemRemainingTime != 0) {
- ItemDisplay(
- icon = getIconResource(beData.itemType),
- textValue = "${beData.itemRemainingTime} m",
- definition = getItemTypeName(beData.itemType),
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
- }
- }
- }
-
- // HISTORY SLOT: Common
- Row(modifier = Modifier.fillMaxWidth()) {
- TransformationHistoryCard(
- transformationHistory = transformationHistory,
- modifier = Modifier.weight(1f).padding(8.dp)
- )
- }
-
- // CONTEXTUAL BOTTOM SLOT
- if (unifiedChar.isVB()) {
- Row(modifier = Modifier.padding(16.dp)) {
- Text(text = stringResource(R.string.home_vbdim_special_missions), fontSize = 24.sp)
- }
- for (mission in specialMissions) {
- Row(modifier = Modifier.fillMaxWidth()) {
- SpecialMissionsEntry(
- specialMission = mission,
- modifier = Modifier.weight(1f).padding(8.dp),
- onClickMission = { missionId -> selectedSpecialMissionId = missionId },
- onClickCollect = { homeScreenController.clearSpecialMission(selectedSpecialMissionId, onClickCollect) }
- )
- }
- }
- } else if (unifiedChar.isBE()) {
- Row(modifier = Modifier.fillMaxWidth()) {
- val beData = unifiedChar.beData ?: return@Column
- ItemDisplay(
- icon = R.drawable.baseline_health_24,
- textValue = "+${beData.trainingHp}",
- definition = "Training HP",
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
- ItemDisplay(
- icon = R.drawable.baseline_agility_24,
- textValue = "+${beData.trainingBp}",
- definition = "Training BP",
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
- ItemDisplay(
- icon = R.drawable.baseline_attack_24,
- textValue = "+${beData.trainingAp}",
- definition = "Training AP",
- modifier = Modifier.weight(1f).aspectRatio(1f).padding(8.dp)
- )
- }
- }
- }
-
- if (selectedSpecialMissionId.toInt() != -1 && unifiedChar.isVB()) {
- DeleteSpecialMissionDialog(
- onClickDismiss = { selectedSpecialMissionId = -1 },
- onClickDelete = {
- homeScreenController.clearSpecialMission(selectedSpecialMissionId, onClickCollect)
- selectedSpecialMissionId = -1
- }
- )
- }
-}
-
-private fun calculateWinPercent(won: Int, lost: Int): String {
- return if (lost == 0) "0.00 %" else {
- val percentage = won.toFloat() / (won + lost).toFloat()
- String.format(Locale.getDefault(), "%.2f", percentage * 100) + " %"
- }
-}
-
-private fun getItemTypeName(itemId: Int): String {
- return when (itemId) {
- ItemsScreenControllerImpl.ItemTypes.PPTraining.id -> "PP Training"
- ItemsScreenControllerImpl.ItemTypes.HPTraining.id -> "HP Training"
- ItemsScreenControllerImpl.ItemTypes.APTraining.id -> "AP Training"
- ItemsScreenControllerImpl.ItemTypes.BPTraining.id -> "BP Training"
- ItemsScreenControllerImpl.ItemTypes.AllTraining.id -> "All Training"
- else -> ""
- }
-}
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 d5fe878..d2025aa 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ChooseConnectionScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ChooseConnectionScreen.kt
@@ -42,11 +42,6 @@ fun ChooseConnectOption(
.fillMaxSize()
.padding(contentPadding)
) {
- Text(
- text = "Bandai Toys = Vital Bracelet, Vital Hero, BE Bracelet",
- fontSize = 14.sp,
- modifier = Modifier.padding(bottom = 24.dp)
- )
ScanButton(
text = stringResource(R.string.scan_vb_to_app),
disabled = onClickRead == null,
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/DetectedTransport.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/DetectedTransport.kt
deleted file mode 100644
index abb0ada..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/DetectedTransport.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.github.nacabaro.vbhelper.screens.scanScreen
-
-enum class DetectedTransport {
- UNKNOWN,
- NFC_A,
- ISO_DEP,
-}
-
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 eb52382..487ab2d 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
@@ -47,22 +47,14 @@ class ScanScreenControllerImpl(
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)
- }
- 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)
+ val character = tagCommunicator.receiveCharacter()
+ val resultMessage = characterFromNfc(character) { cards, nfcCharacter ->
+ lastScannedCharacter = nfcCharacter
+ onMultipleCards(cards)
+
}
+ onComplete.invoke()
+ resultMessage
}
}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/TransferHaptics.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/TransferHaptics.kt
deleted file mode 100644
index 85ac89f..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/TransferHaptics.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.github.nacabaro.vbhelper.screens.scanScreen
-
-import android.content.Context
-import android.os.Build
-import android.os.VibrationEffect
-import android.os.Vibrator
-import android.os.VibratorManager
-import timber.log.Timber
-
-class TransferHaptics(context: Context) {
- private val vibrator: Vibrator? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- val manager = context.getSystemService(VibratorManager::class.java)
- manager?.defaultVibrator
- } else {
- @Suppress("DEPRECATION")
- context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
- }
-
- fun onTransferStart() {
- vibrate(VibrationEffect.createOneShot(100L, VibrationEffect.DEFAULT_AMPLITUDE))
- }
-
- fun onTransferProgress() {
- vibrate(VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE))
- }
-
- fun onTransferComplete() {
- vibrate(VibrationEffect.createWaveform(longArrayOf(0L, 100L, 80L, 150L), -1))
- }
-
- fun onTransferFailed() {
- vibrate(VibrationEffect.createOneShot(250L, VibrationEffect.DEFAULT_AMPLITUDE))
- }
-
- fun onDeviceDetected() {
- vibrate(VibrationEffect.createOneShot(80L, VibrationEffect.DEFAULT_AMPLITUDE))
- }
-
- private fun vibrate(effect: VibrationEffect) {
- val deviceVibrator = vibrator ?: return
- if (!deviceVibrator.hasVibrator()) {
- return
- }
- runCatching {
- deviceVibrator.vibrate(effect)
- }.onFailure { error ->
- Timber.w(error, "Transfer haptics vibration skipped")
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/TransferAnimationScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/TransferAnimationScreen.kt
deleted file mode 100644
index 137478b..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/TransferAnimationScreen.kt
+++ /dev/null
@@ -1,267 +0,0 @@
-package com.github.nacabaro.vbhelper.screens.scanScreen.screens
-
-import androidx.compose.animation.core.FastOutLinearInEasing
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.animateIntAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material3.Button
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Card
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.graphics.FilterQuality
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.github.nacabaro.vbhelper.components.TopBanner
-import com.github.nacabaro.vbhelper.R
-import com.github.nacabaro.vbhelper.utils.ImageBitmapData
-import kotlinx.coroutines.delay
-
-@Composable
-fun TransferAnimationScreen(
- topBannerText: String,
- detectedTransportMessage: String?,
- transferStatusMessage: String?,
- characterPreview: ImageBitmapData? = null,
- onClickCancel: () -> Unit,
- isTransferring: Boolean = true,
-) {
- var pulseScale by remember { mutableStateOf(1f) }
- var animationProgress by remember { mutableStateOf(0) }
-
- val scale by animateFloatAsState(
- targetValue = pulseScale,
- animationSpec = tween(durationMillis = 800),
- label = "pulse_scale"
- )
-
- val progress by animateIntAsState(
- targetValue = animationProgress,
- animationSpec = tween(durationMillis = 100, easing = FastOutLinearInEasing),
- label = "progress"
- )
-
- LaunchedEffect(isTransferring) {
- if (isTransferring) {
- while (true) {
- pulseScale = 1.2f
- delay(400)
- pulseScale = 1f
- delay(400)
- }
- }
- }
-
- LaunchedEffect(isTransferring) {
- if (isTransferring) {
- while (animationProgress < 100) {
- delay(50)
- animationProgress += 2
- }
- }
- }
-
- Scaffold(
- topBar = {
- TopBanner(
- text = topBannerText,
- onBackClick = onClickCancel
- )
- }
- ) { innerPadding ->
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier = Modifier
- .padding(innerPadding)
- .fillMaxSize()
- ) {
- characterPreview?.let { preview ->
- Card(
- modifier = Modifier.padding(bottom = 24.dp)
- ) {
- Image(
- bitmap = preview.imageBitmap,
- contentDescription = "Transferred Digimon sprite",
- modifier = Modifier
- .size(preview.dpWidth, preview.dpHeight)
- .padding(8.dp),
- filterQuality = FilterQuality.None
- )
- }
- }
-
- // Animated pulsing circle indicator
- Box(
- modifier = Modifier
- .size(120.dp)
- .scale(scale)
- .background(
- color = Color(0xFF6366F1).copy(alpha = 0.2f),
- shape = CircleShape
- ),
- contentAlignment = Alignment.Center
- ) {
- Box(
- modifier = Modifier
- .size(80.dp)
- .background(
- color = Color(0xFF6366F1),
- shape = CircleShape
- ),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = "🔄",
- fontSize = 40.sp,
- modifier = Modifier.scale(if (scale > 1f) 1.1f else 1f)
- )
- }
- }
-
- Text(
- text = stringResource(R.string.action_place_near_reader),
- fontSize = 16.sp,
- modifier = Modifier.padding(top = 24.dp),
- fontWeight = FontWeight.Medium
- )
-
- if (!detectedTransportMessage.isNullOrBlank()) {
- Text(
- text = detectedTransportMessage,
- modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp),
- fontSize = 14.sp
- )
- }
-
- if (!transferStatusMessage.isNullOrBlank()) {
- Text(
- text = transferStatusMessage,
- modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp),
- fontSize = 14.sp,
- fontWeight = FontWeight.Bold,
- color = Color(0xFF6366F1)
- )
- }
-
- // Progress ring
- if (isTransferring) {
- CircularProgressIndicator(
- progress = { progress / 100f },
- modifier = Modifier
- .size(100.dp)
- .padding(top = 24.dp),
- strokeWidth = 4.dp
- )
-
- Text(
- text = "$progress%",
- modifier = Modifier.padding(top = 8.dp),
- fontSize = 12.sp,
- color = Color.Gray
- )
- }
-
- Button(
- onClick = onClickCancel,
- modifier = Modifier.padding(top = 32.dp, bottom = 16.dp)
- ) {
- Text(stringResource(R.string.action_cancel))
- }
- }
- }
-}
-
-@Composable
-fun TransferCompleteScreen(
- topBannerText: String,
- resultMessage: String,
- onClickOk: () -> Unit,
- isSuccess: Boolean = true,
-) {
- var showCheckmark by remember { mutableStateOf(false) }
-
- val scale by animateFloatAsState(
- targetValue = if (showCheckmark) 1f else 0f,
- animationSpec = tween(durationMillis = 600, easing = FastOutLinearInEasing),
- label = "checkmark_scale"
- )
-
- LaunchedEffect(Unit) {
- delay(300)
- showCheckmark = true
- }
-
- Scaffold(
- topBar = {
- TopBanner(
- text = topBannerText,
- onBackClick = {}
- )
- }
- ) { innerPadding ->
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier = Modifier
- .padding(innerPadding)
- .fillMaxSize()
- ) {
- Box(
- modifier = Modifier
- .size(120.dp)
- .scale(scale)
- .background(
- color = if (isSuccess) Color(0xFF10B981) else Color(0xFFEF4444),
- shape = CircleShape
- ),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = if (isSuccess) "✓" else "✕",
- fontSize = 60.sp,
- color = Color.White,
- fontWeight = FontWeight.Bold
- )
- }
-
- Text(
- text = resultMessage,
- fontSize = 16.sp,
- modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.dp),
- fontWeight = FontWeight.Medium,
- textAlign = TextAlign.Center
- )
-
- Button(
- onClick = onClickOk,
- modifier = Modifier.padding(top = 32.dp, bottom = 16.dp)
- ) {
- Text("OK")
- }
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/CreditsScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/CreditsScreen.kt
index 8350a90..ad09456 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/CreditsScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/CreditsScreen.kt
@@ -38,8 +38,6 @@ fun CreditsScreen(
SettingsEntry(title = "nacabaro", description = stringResource(R.string.credits_nacabaro_description)) { }
SettingsEntry(title = "lightheel", description = stringResource(R.string.credits_lightheel_description)) { }
SettingsEntry(title = "shvstrz", description = stringResource(R.string.credits_shvstrz_description)) { }
-
- SettingsEntry(title = "redeyez", description = stringResource(R.string.credits_RedEyez_description)) { }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageDialog.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageDialog.kt
index ec0173d..33ac860 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageDialog.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageDialog.kt
@@ -114,28 +114,28 @@ fun StorageDialog(
)
}
}
- Row(
- horizontalArrangement = Arrangement.Center,
- modifier = Modifier
- .fillMaxWidth()
- ) {
- Button(
- onClick = onSendToBracelet,
- modifier = Modifier
- .weight(1f)
- ) {
- Text(text = stringResource(R.string.storage_send_to_watch))
- }
- Spacer(
- modifier = Modifier
- .padding(4.dp)
- )
- Button(
- onClick = onClickSetActive,
- ) {
- Text(text = stringResource(R.string.storage_set_active))
- }
- }
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ Button(
+ onClick = onSendToBracelet,
+ modifier = Modifier
+ .weight(1f)
+ ) {
+ Text(text = stringResource(R.string.storage_send_to_watch))
+ }
+ Spacer(
+ modifier = Modifier
+ .padding(4.dp)
+ )
+ Button(
+ onClick = onClickSetActive,
+ ) {
+ Text(text = stringResource(R.string.storage_set_active))
+ }
+ }
Button(
onClick = {
onSendToAdventureClicked = true
@@ -171,4 +171,4 @@ fun StorageDialog(
onDismissRequest = { onSendToAdventureClicked = false }
)
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt
index cc9d3d0..e33e6dd 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt
@@ -33,7 +33,6 @@ import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenControllerImpl
import com.github.nacabaro.vbhelper.source.StorageRepository
-import com.github.nacabaro.vbhelper.source.VitalWearCharacterExporter
import com.github.nacabaro.vbhelper.utils.BitmapData
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/CardSettingsRepository.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/CardSettingsRepository.kt
deleted file mode 100644
index 557cf67..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/source/CardSettingsRepository.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.github.nacabaro.vbhelper.source
-
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-class CardSettingsRepository(
- private val dataStore: DataStore
-) {
- private companion object {
- val ENABLE_DIM_TO_BEM_CONVERSION = booleanPreferencesKey("enable_dim_to_bem_conversion")
- }
-
- val enableDimToBemConversion: Flow = dataStore.data
- .map { preferences ->
- preferences[ENABLE_DIM_TO_BEM_CONVERSION] ?: false
- }
-
- suspend fun setEnableDimToBemConversion(enable: Boolean) {
- dataStore.edit { preferences ->
- preferences[ENABLE_DIM_TO_BEM_CONVERSION] = enable
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/DeviceTypeConverter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/DeviceTypeConverter.kt
deleted file mode 100644
index 6a97062..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/source/DeviceTypeConverter.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.github.nacabaro.vbhelper.source
-
-import com.github.nacabaro.vbhelper.database.AppDatabase
-import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
-import com.github.nacabaro.vbhelper.utils.DeviceType
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-/**
- * Utility for converting character device types.
- * Used to fix mismatched device types when sending to physical NFC devices.
- */
-class DeviceTypeConverter(private val database: AppDatabase) {
-
- /**
- * Prepares a BE character for VB-target exports without deleting BE stats.
- * This keeps the BE profile intact while ensuring a VB sidecar exists.
- */
- suspend fun convertBEToVB(characterId: Long): Boolean = withContext(Dispatchers.IO) {
- try {
- val userCharacter = database.userCharacterDao().getCharacter(characterId)
-
- if (userCharacter.characterType != DeviceType.BEDevice) {
- // Already VB or other type, no conversion needed
- return@withContext true
- }
-
- // Keep BE typing/stats as-is; only ensure a VB sidecar profile exists for exports.
- ensureVBCharacterDataExists(characterId)
-
- 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)
- false
- }
- }
-
- private suspend fun ensureVBCharacterDataExists(characterId: Long) {
- val vbData = database.userCharacterDao().getVbDataOrNull(characterId)
- if (vbData == null) {
- val vbCharacterData = VBCharacterData(
- id = characterId,
- generation = 0,
- totalTrophies = 0
- )
- database.userCharacterDao().insertVBCharacterData(vbCharacterData)
- }
- }
-}
-
-
-
-
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
deleted file mode 100644
index bc3ad95..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporter.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.github.nacabaro.vbhelper.source
-
-import android.content.ClipData
-import android.content.Context
-import android.content.Intent
-import androidx.core.content.FileProvider
-import com.github.cfogrady.vitalwear.protos.Character
-import com.github.nacabaro.vbhelper.database.AppDatabase
-import com.github.nacabaro.vbhelper.utils.DeviceType
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.runBlocking
-import java.io.File
-
-class VitalWearCharacterExporter(
- private val context: Context,
- private val database: AppDatabase
-) {
- fun buildShareIntent(characterId: Long): Intent {
- return runBlocking {
- val characterWithSprites = database.userCharacterDao().getCharacterWithSprites(characterId)
- val userCharacter = database.userCharacterDao().getCharacter(characterId)
- val card = database.cardDao().getCardByCharacterIdSync(characterId)
- ?: error("Card not found for character $characterId")
- val cardProgress = database.cardProgressDao().getCardProgressSync(card.id) ?: 0
- val proto = Character.newBuilder()
- .setCardId(card.cardId)
- .setCardName(card.name)
- .setCharacterStats(
- Character.CharacterStats.newBuilder()
- .setSlotId(database.userCharacterDao().getCharacterInfo(characterId).charaIndex)
- .setVitals(characterWithSprites.vitalPoints)
- .setTrainingTimeRemainingInSeconds(resolveTrainingSeconds(characterId, userCharacter.characterType))
- .setTimeUntilNextTransformation(characterWithSprites.transformationCountdown.toLong() * 60L)
- .setTrainedBp(resolveTrainedBp(characterId, userCharacter.characterType))
- .setTrainedHp(resolveTrainedHp(characterId, userCharacter.characterType))
- .setTrainedAp(resolveTrainedAp(characterId, userCharacter.characterType))
- .setTrainedPp(characterWithSprites.trophies)
- .setInjured(characterWithSprites.injuryStatus.name.lowercase().contains("inj"))
- .setAccumulatedDailyInjuries(0)
- .setTotalBattles(characterWithSprites.totalBattlesWon + characterWithSprites.totalBattlesLost)
- .setCurrentPhaseBattles(characterWithSprites.currentPhaseBattlesWon + characterWithSprites.currentPhaseBattlesLost)
- .setTotalWins(characterWithSprites.totalBattlesWon)
- .setCurrentPhaseWins(characterWithSprites.currentPhaseBattlesWon)
- .setMood(characterWithSprites.mood)
- .build()
- )
- .setSettings(
- Character.Settings.newBuilder()
- .setTrainingInBackground(false)
- .setAllowedBattles(Character.Settings.AllowedBattles.CARD_ONLY)
- .build()
- )
- .putMaxAdventureCompletedByCard(card.name, (cardProgress - 1).coerceAtLeast(0))
- .addAllTransformationHistory(
- database.userCharacterDao().getTransformationHistoryForExport(characterId).map {
- Character.TransformationEvent.newBuilder()
- .setCardName(it.cardName)
- .setPhase(0)
- .setSlotId(it.monIndex)
- .build()
- }
- )
- .build()
-
- val exportDir = File(context.cacheDir, "exports").apply { mkdirs() }
- val exportFile = File(exportDir, "vbhelper_character_$characterId.vitalwear")
- exportFile.writeBytes(proto.toByteArray())
- val exportUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", exportFile)
-
- Intent(Intent.ACTION_SEND).apply {
- `package` = "com.github.cfogrady.vitalwear"
- type = VITALWEAR_CHARACTER_MIME
- putExtra(Intent.EXTRA_STREAM, exportUri)
- clipData = ClipData.newRawUri("", exportUri)
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- }
- }
-
- private fun resolveTrainingSeconds(characterId: Long, deviceType: DeviceType): Long {
- if (deviceType != DeviceType.BEDevice) {
- return 0L
- }
- return database.userCharacterDao().getBeData(characterId).valueOrNull()?.remainingTrainingTimeInMinutes?.toLong()?.times(60L) ?: 0L
- }
-
- private fun resolveTrainedBp(characterId: Long, deviceType: DeviceType): Int {
- return if (deviceType == DeviceType.BEDevice) {
- database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingBp ?: 0
- } else 0
- }
-
- private fun resolveTrainedHp(characterId: Long, deviceType: DeviceType): Int {
- return if (deviceType == DeviceType.BEDevice) {
- database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingHp ?: 0
- } else 0
- }
-
- private fun resolveTrainedAp(characterId: Long, deviceType: DeviceType): Int {
- return if (deviceType == DeviceType.BEDevice) {
- database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingAp ?: 0
- } else 0
- }
-
- private fun kotlinx.coroutines.flow.Flow.valueOrNull(): T? {
- return runBlocking {
- firstOrNull()
- }
- }
-
- companion object {
- const val VITALWEAR_CHARACTER_MIME = "application/x-vitalwear-character"
- }
-}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt
deleted file mode 100644
index 6ec3b65..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt
+++ /dev/null
@@ -1,163 +0,0 @@
-package com.github.nacabaro.vbhelper.source
-
-import com.github.cfogrady.vbnfc.data.NfcCharacter
-import com.github.cfogrady.vitalwear.protos.Character
-import com.github.nacabaro.vbhelper.database.AppDatabase
-import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
-import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
-import com.github.nacabaro.vbhelper.utils.DeviceType
-import kotlin.math.max
-
-class VitalWearCharacterImporter(
- private val database: AppDatabase
-) {
- data class ImportResult(
- val success: Boolean,
- val message: String
- )
-
- fun importCharacter(character: Character): ImportResult {
- val importedCard = resolveCard(character)
- ?: return ImportResult(
- success = false,
- message = "Matching card not found in VBHelper. Import that card first."
- )
-
- val slotId = character.characterStats.slotId
- val cardCharacter = runCatching {
- database.characterDao().getCharacterByMonIndex(slotId, importedCard.id)
- }.getOrNull() ?: return ImportResult(
- success = false,
- message = "Character slot $slotId was not found on card ${importedCard.name}."
- )
-
- database.userCharacterDao().clearActiveCharacter()
-
- val totalBattles = character.characterStats.totalBattles
- val totalWins = character.characterStats.totalWins.coerceAtMost(totalBattles)
- val currentPhaseBattles = character.characterStats.currentPhaseBattles
- val currentPhaseWins = character.characterStats.currentPhaseWins.coerceAtMost(currentPhaseBattles)
-
- val userCharacterId = database.userCharacterDao().insertCharacterData(
- UserCharacter(
- charId = cardCharacter.id,
- ageInDays = max(character.transformationHistoryCount - 1, 0),
- mood = character.characterStats.mood,
- vitalPoints = character.characterStats.vitals,
- transformationCountdown = secondsToMinutes(character.characterStats.timeUntilNextTransformation),
- injuryStatus = resolveInjuryStatus(character.characterStats.injured),
- trophies = character.characterStats.trainedPp,
- currentPhaseBattlesWon = currentPhaseWins,
- currentPhaseBattlesLost = (currentPhaseBattles - currentPhaseWins).coerceAtLeast(0),
- totalBattlesWon = totalWins,
- totalBattlesLost = (totalBattles - totalWins).coerceAtLeast(0),
- activityLevel = 0,
- heartRateCurrent = 0,
- characterType = DeviceType.BEDevice,
- isActive = true
- )
- )
-
- database.userCharacterDao().insertBECharacterData(
- BECharacterData(
- id = userCharacterId,
- trainingHp = character.characterStats.trainedHp,
- trainingAp = character.characterStats.trainedAp,
- trainingBp = character.characterStats.trainedBp,
- remainingTrainingTimeInMinutes = secondsToMinutes(character.characterStats.trainingTimeRemainingInSeconds),
- itemEffectMentalStateValue = 0,
- itemEffectMentalStateMinutesRemaining = 0,
- itemEffectActivityLevelValue = 0,
- itemEffectActivityLevelMinutesRemaining = 0,
- itemEffectVitalPointsChangeValue = 0,
- itemEffectVitalPointsChangeMinutesRemaining = 0,
- abilityRarity = resolveDefaultAbilityRarity(),
- abilityType = 0,
- abilityBranch = 0,
- abilityReset = 0,
- rank = 0,
- itemType = 0,
- itemMultiplier = 0,
- itemRemainingTime = 0,
- otp0 = "",
- otp1 = "",
- minorVersion = 0,
- majorVersion = 0
- )
- )
-
- val now = System.currentTimeMillis()
- database.dexDao().insertCharacter(slotId, importedCard.id, now)
-
- for (transformation in character.transformationHistoryList) {
- val transformationCard = resolveCard(transformation.cardName, character.cardId)
- if (transformationCard != null) {
- database.userCharacterDao().insertTransformation(
- userCharacterId,
- transformation.slotId,
- transformationCard.id,
- now
- )
- database.dexDao().insertCharacter(transformation.slotId, transformationCard.id, now)
- }
- }
-
- for ((cardName, maxAdventureCompleted) in character.maxAdventureCompletedByCardMap) {
- val adventureCard = resolveCard(cardName, null) ?: continue
- val currentStage = (maxAdventureCompleted + 1).coerceAtLeast(0)
- database.cardProgressDao().updateCardProgress(
- currentStage = currentStage,
- cardId = adventureCard.id,
- unlocked = currentStage > adventureCard.stageCount
- )
- }
-
- return ImportResult(
- success = true,
- message = "Imported ${importedCard.name} slot $slotId from VitalWear."
- )
- }
-
- private fun resolveCard(character: Character) = resolveCard(character.cardName, character.cardId)
-
- private fun resolveCard(cardName: String?, cardId: Int?): com.github.nacabaro.vbhelper.domain.card.Card? {
- if (!cardName.isNullOrBlank()) {
- database.cardDao().getCardByName(cardName)?.let { return it }
- }
-
- if (cardId != null) {
- val matches = database.cardDao().getCardByCardId(cardId)
- if (matches.size == 1) {
- return matches.first()
- }
- }
-
- return null
- }
-
- private fun secondsToMinutes(seconds: Long): Int {
- if (seconds <= 0L) {
- return 0
- }
- return (seconds / 60L).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
- }
-
- private fun resolveInjuryStatus(injured: Boolean): NfcCharacter.InjuryStatus {
- val statuses = enumValues()
- if (!injured) {
- return statuses.firstOrNull {
- val normalized = it.name.lowercase()
- normalized.contains("none") || normalized.contains("normal") || normalized.contains("healthy")
- } ?: statuses.first()
- }
-
- return statuses.firstOrNull {
- val normalized = it.name.lowercase()
- normalized.contains("inj")
- } ?: statuses.last()
- }
-
- private fun resolveDefaultAbilityRarity(): NfcCharacter.AbilityRarity {
- return enumValues().first()
- }
-}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearTransformationCountdown.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearTransformationCountdown.kt
deleted file mode 100644
index 13c8dfd..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearTransformationCountdown.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.github.nacabaro.vbhelper.source
-
-import com.github.cfogrady.vitalwear.protos.Character
-import com.github.nacabaro.vbhelper.utils.DeviceType
-
-internal const val MINIMUM_TRANSFORMATION_COUNTDOWN_MINUTES = 1
-
-internal fun normalizeTransformationCountdownMinutes(
- transformationCountdownMinutes: Int,
- hasPossibleTransformations: Boolean,
-): Int {
- val sanitizedCountdown = transformationCountdownMinutes.coerceAtLeast(0)
- if (!hasPossibleTransformations) {
- return sanitizedCountdown
- }
- return sanitizedCountdown.coerceAtLeast(MINIMUM_TRANSFORMATION_COUNTDOWN_MINUTES)
-}
-
-internal fun DeviceType.toTransferDeviceType(): Character.CharacterStats.TransferDeviceType {
- return when (this) {
- DeviceType.BEDevice -> Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_BE
- DeviceType.VBDevice,
- DeviceType.VitalWear -> Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_VB
- }
-}
-
-internal fun resolveDeviceType(
- transferDeviceType: Character.CharacterStats.TransferDeviceType,
- fallbackIsBeCharacter: Boolean,
-): DeviceType {
- return when (transferDeviceType) {
- Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_BE -> DeviceType.BEDevice
- Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_VB -> DeviceType.VBDevice
- Character.CharacterStats.TransferDeviceType.UNRECOGNIZED,
- Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_UNSPECIFIED -> if (fallbackIsBeCharacter) DeviceType.BEDevice else DeviceType.VBDevice
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/CardImportProvider.kt b/app/src/main/java/com/github/nacabaro/vbhelper/transfer/CardImportProvider.kt
deleted file mode 100644
index 674354a..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/CardImportProvider.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-package com.github.nacabaro.vbhelper.transfer
-
-import android.content.ContentProvider
-import android.content.ContentValues
-import android.database.Cursor
-import android.net.Uri
-import android.os.Bundle
-import com.github.cfogrady.vb.dim.card.DimReader
-import com.github.nacabaro.vbhelper.di.VBHelper
-import com.github.nacabaro.vbhelper.screens.settingsScreen.controllers.CardImportController
-import kotlinx.coroutines.runBlocking
-
-/**
- * ContentProvider that lets the VitalWear Companion app read and write card data into
- * VBHelper's single database (internalDb). Both apps are signed with the same key, so
- * the signature-level permission is automatically granted without user interaction.
- *
- * Usage from the companion:
- * val names = call(METHOD_GET_CARD_NAMES)?.getStringArray(EXTRA_CARD_NAMES_LIST)
- * call(METHOD_IMPORT_CARD, null, Bundle { putByteArray(EXTRA_CARD_BYTES, ...) })
- */
-class CardImportProvider : ContentProvider() {
-
- companion object {
- const val AUTHORITY = "com.github.nacabaro.vbhelper.cardimport"
- val URI: Uri = Uri.parse("content://$AUTHORITY")
-
- /** Returns a Bundle with EXTRA_CARD_NAMES_LIST containing all imported card names. */
- const val METHOD_GET_CARD_NAMES = "getCardNames"
-
- /**
- * Parses and stores a DIM card.
- * Required extras: EXTRA_CARD_BYTES (ByteArray)
- * Optional extras: EXTRA_CARD_NAME (String) — user-visible label; if omitted the DIM
- * file's built-in name is used.
- * Returns a Bundle with RESULT_OK = true on success.
- */
- const val METHOD_IMPORT_CARD = "importCard"
-
- const val EXTRA_CARD_BYTES = "cardBytes"
- const val EXTRA_CARD_NAME = "cardName"
- const val EXTRA_CARD_NAMES_LIST = "cardNames"
- const val RESULT_OK = "ok"
- }
-
- override fun onCreate(): Boolean = true
-
- override fun call(method: String, arg: String?, extras: Bundle?): Bundle {
- val ctx = context ?: return Bundle().apply { putBoolean(RESULT_OK, false) }
- val db = (ctx.applicationContext as VBHelper).container.db
-
- return when (method) {
-
- METHOD_GET_CARD_NAMES -> runCatching {
- val names = db.cardDao().getAllCards().map { it.name }.toTypedArray()
- Bundle().apply { putStringArray(EXTRA_CARD_NAMES_LIST, names) }
- }.getOrElse {
- Bundle().apply { putStringArray(EXTRA_CARD_NAMES_LIST, emptyArray()) }
- }
-
- METHOD_IMPORT_CARD -> {
- val cardBytes = extras?.getByteArray(EXTRA_CARD_BYTES)
- ?: return Bundle().apply { putBoolean(RESULT_OK, false) }
- val customName = extras.getString(EXTRA_CARD_NAME)
-
- runCatching {
- runBlocking {
- val dimReader = DimReader()
- val peeked = cardBytes.inputStream().use { dimReader.readCard(it, false) }
- val builtInName = peeked.spriteData.text
- val dimId = peeked.header.dimId
-
- val alreadyExists =
- db.cardDao().getCardByName(builtInName) != null ||
- db.cardDao().getCardByCardId(dimId).isNotEmpty()
-
- if (!alreadyExists) {
- CardImportController(db).importCard(cardBytes.inputStream())
- // Rename to the user's custom label if it differs from the DIM's text
- if (!customName.isNullOrBlank() && customName != builtInName) {
- db.cardDao().getCardByName(builtInName)?.let { card ->
- db.cardDao().renameCard(card.id.toInt(), customName)
- }
- }
- }
- }
- Bundle().apply { putBoolean(RESULT_OK, true) }
- }.getOrElse {
- Bundle().apply { putBoolean(RESULT_OK, false) }
- }
- }
-
- else -> Bundle().apply { putBoolean(RESULT_OK, false) }
- }
- }
-
- // ---- Unused mandatory overrides ----
- override fun query(uri: Uri, projection: Array?, selection: String?,
- selectionArgs: Array?, sortOrder: String?): Cursor? = null
- override fun getType(uri: Uri): String? = null
- override fun insert(uri: Uri, values: ContentValues?): Uri? = null
- override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
- override fun update(uri: Uri, values: ContentValues?, selection: String?,
- selectionArgs: Array?): Int = 0
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/CharacterTransferPolicyResolver.kt b/app/src/main/java/com/github/nacabaro/vbhelper/transfer/CharacterTransferPolicyResolver.kt
deleted file mode 100644
index 811885a..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/CharacterTransferPolicyResolver.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package com.github.nacabaro.vbhelper.transfer
-
-import com.github.nacabaro.vbhelper.daos.CharacterTransferPolicyDao
-import com.github.nacabaro.vbhelper.domain.device_data.CharacterTransferPolicy
-import com.github.nacabaro.vbhelper.utils.DeviceType
-
-class CharacterTransferPolicyResolver(
- private val policyDao: CharacterTransferPolicyDao,
-) {
- fun resolveExportFormat(
- characterId: Long,
- characterType: DeviceType,
- transport: TransferTransport,
- target: TransferTarget,
- isBemCard: Boolean,
- ): ExportFormat {
- val policy = policyDao.getByCharacterId(characterId)
- return when (transport) {
- TransferTransport.HCE -> policy?.preferredHceExportFormat
- ?: defaultHceExportFormat(characterType, isBemCard, target)
- TransferTransport.NFCA -> policy?.preferredNfcaExportFormat
- ?: defaultNfcaExportFormat(characterType, isBemCard, target)
- }
- }
-
- fun policyForNfcaImport(
- characterId: Long,
- observedFormat: ExportFormat,
- ): CharacterTransferPolicy {
- val nativeDeviceType = observedFormat.toNativeDeviceType()
- return CharacterTransferPolicy(
- characterId = characterId,
- nativeDeviceType = nativeDeviceType,
- preferredHceExportFormat = defaultHceExportFormat(nativeDeviceType, nativeDeviceType == DeviceType.BEDevice, TransferTarget.VITAL_WEAR),
- preferredNfcaExportFormat = observedFormat,
- lastObservedImportFormat = observedFormat,
- lastTransferTransport = TransferTransport.NFCA,
- lastTransferTarget = TransferTarget.REAL_BRACELET,
- preserveVbRoundTrip = observedFormat == ExportFormat.VB,
- preserveBeRoundTrip = observedFormat == ExportFormat.BE,
- )
- }
-
- fun policyForHceImport(
- characterId: Long,
- importedCardIsBem: Boolean,
- resolvedDeviceType: DeviceType,
- observedFormat: ExportFormat,
- ): CharacterTransferPolicy {
- val nativeDeviceType = when {
- importedCardIsBem -> DeviceType.BEDevice
- resolvedDeviceType == DeviceType.BEDevice && observedFormat == ExportFormat.BE -> DeviceType.VBDevice
- resolvedDeviceType == DeviceType.VitalWear -> DeviceType.VBDevice
- else -> resolvedDeviceType
- }
- val preferredNfcaExportFormat = if (importedCardIsBem || nativeDeviceType == DeviceType.BEDevice) {
- ExportFormat.BE
- } else {
- ExportFormat.VB
- }
-
- return CharacterTransferPolicy(
- characterId = characterId,
- nativeDeviceType = nativeDeviceType,
- preferredHceExportFormat = defaultHceExportFormat(nativeDeviceType, importedCardIsBem, TransferTarget.VITAL_WEAR),
- preferredNfcaExportFormat = preferredNfcaExportFormat,
- lastObservedImportFormat = observedFormat,
- lastTransferTransport = TransferTransport.HCE,
- lastTransferTarget = TransferTarget.VITAL_WEAR,
- preserveVbRoundTrip = preferredNfcaExportFormat == ExportFormat.VB,
- preserveBeRoundTrip = importedCardIsBem || observedFormat == ExportFormat.BE,
- )
- }
-
- private fun defaultHceExportFormat(
- characterType: DeviceType,
- isBemCard: Boolean,
- target: TransferTarget,
- ): ExportFormat {
- if (target != TransferTarget.VITAL_WEAR) {
- return defaultNfcaExportFormat(characterType, isBemCard, target)
- }
-
- // VitalWear is the HCE endpoint. Default to BE projection so HCE transfers can carry the
- // richer payload, while NFCA policy still controls what is sent back to real bracelets.
- return ExportFormat.BE
- }
-
- private fun defaultNfcaExportFormat(
- characterType: DeviceType,
- isBemCard: Boolean,
- target: TransferTarget,
- ): ExportFormat {
- require(target == TransferTarget.REAL_BRACELET) {
- "NFCA exports are only supported for real bracelets."
- }
- return if (isBemCard || characterType == DeviceType.BEDevice) {
- ExportFormat.BE
- } else {
- ExportFormat.VB
- }
- }
-
- private fun ExportFormat.toNativeDeviceType(): DeviceType {
- return when (this) {
- ExportFormat.BE -> DeviceType.BEDevice
- ExportFormat.VB -> DeviceType.VBDevice
- }
- }
-}
-
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/SharedTransferSeenProvider.kt b/app/src/main/java/com/github/nacabaro/vbhelper/transfer/SharedTransferSeenProvider.kt
deleted file mode 100644
index 8734c22..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/SharedTransferSeenProvider.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.github.nacabaro.vbhelper.transfer
-
-import android.content.ContentProvider
-import android.content.ContentValues
-import android.database.Cursor
-import android.net.Uri
-import android.os.Bundle
-import com.github.cfogrady.vitalwear.common.data.SharedDatabaseFactory
-
-class SharedTransferSeenProvider : ContentProvider() {
- companion object {
- const val AUTHORITY = "com.github.nacabaro.vbhelper.transferseen"
- val URI: Uri = Uri.parse("content://$AUTHORITY")
-
- const val METHOD_MARK_SEEN = "markSeen"
- const val EXTRA_CARD_NAME = "cardName"
- const val EXTRA_SLOT_ID = "slotId"
- const val EXTRA_SEEN_AT_EPOCH_MILLIS = "seenAtEpochMillis"
- const val RESULT_OK = "ok"
- }
-
- override fun onCreate(): Boolean = true
-
- override fun call(method: String, arg: String?, extras: Bundle?): Bundle {
- if (method != METHOD_MARK_SEEN) {
- return Bundle().apply { putBoolean(RESULT_OK, false) }
- }
-
- val context = context ?: return Bundle().apply { putBoolean(RESULT_OK, false) }
- val cardName = extras?.getString(EXTRA_CARD_NAME).orEmpty()
- val slotId = extras?.getInt(EXTRA_SLOT_ID, -1) ?: -1
- val seenAt = extras?.getLong(EXTRA_SEEN_AT_EPOCH_MILLIS, 0L) ?: 0L
-
- if (cardName.isBlank() || slotId < 0 || seenAt <= 0L) {
- return Bundle().apply { putBoolean(RESULT_OK, false) }
- }
-
- return runCatching {
- SharedDatabaseFactory.getDatabase(context).transferSeenDao().markSeen(cardName, slotId, seenAt)
- Bundle().apply { putBoolean(RESULT_OK, true) }
- }.getOrElse {
- Bundle().apply { putBoolean(RESULT_OK, false) }
- }
- }
-
- override fun query(
- uri: Uri,
- projection: Array?,
- selection: String?,
- selectionArgs: Array?,
- sortOrder: String?
- ): Cursor? = null
-
- override fun getType(uri: Uri): String? = null
-
- override fun insert(uri: Uri, values: ContentValues?): Uri? = null
-
- override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
-
- override fun update(
- uri: Uri,
- values: ContentValues?,
- selection: String?,
- selectionArgs: Array?
- ): Int = 0
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/TransferRoute.kt b/app/src/main/java/com/github/nacabaro/vbhelper/transfer/TransferRoute.kt
deleted file mode 100644
index 72c47f2..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/TransferRoute.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.github.nacabaro.vbhelper.transfer
-
-import com.github.cfogrady.vitalwear.protos.Character
-
-/**
- * Real bracelets talk over NFC-A while VitalWear uses HCE / ISO-DEP.
- * Keep transport, target, and payload format separate so one canonical character can be
- * projected differently depending on where it is being transferred.
- */
-enum class TransferTransport {
- NFCA,
- HCE,
-}
-
-enum class TransferTarget {
- REAL_BRACELET,
- VITAL_WEAR,
-}
-
-enum class ExportFormat {
- VB,
- BE,
-}
-
-fun ExportFormat.toTransferDeviceType(): Character.CharacterStats.TransferDeviceType {
- return when (this) {
- ExportFormat.VB -> Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_VB
- ExportFormat.BE -> Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_BE
- }
-}
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/hce/VitalWearHceReaderClient.kt b/app/src/main/java/com/github/nacabaro/vbhelper/transfer/hce/VitalWearHceReaderClient.kt
deleted file mode 100644
index 74b6948..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/hce/VitalWearHceReaderClient.kt
+++ /dev/null
@@ -1,437 +0,0 @@
-package com.github.nacabaro.vbhelper.transfer.hce
-import android.nfc.tech.IsoDep
-import android.os.SystemClock
-import android.util.Log
-import com.github.cfogrady.vitalwear.protos.Character
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.runBlocking
-/**
- * Phone-side ISO-DEP client that drives the VitalWear HCE session.
- * Mirrors the APDU protocol defined in VitalWearHceProtocol on the watch.
- */
-class VitalWearHceReaderClient(
- private val isoDep: IsoDep,
- private val fastMode: Boolean = true,
-) {
- companion object {
- private const val MIN_CHUNK_SIZE = 256
- private val bestChunkByDevice = linkedMapOf()
- }
-
- init {
- // Import + DB writes can make COMMIT slower than default transceive time on some devices.
- isoDep.timeout = 10_000
- }
-
- private val AID = byteArrayOf(
- 0xF0.toByte(), 0x56, 0x49, 0x54, 0x41, 0x4C, 0x57, 0x45, 0x41, 0x52
- )
- private val CLA: Byte = 0x80.toByte()
- private val INS_NEGOTIATE: Byte = 0x10
- private val INS_READ_CHUNK: Byte = 0x20
- private val INS_WRITE_CHUNK: Byte = 0x30
- private val INS_COMMIT: Byte = 0x40
- private val INS_STATUS: Byte = 0x50
- private val INS_SYNC_UI: Byte = 0x60
- private val INS_VIBRATE: Byte = 0x70
- private val MODE_WATCH_TO_PHONE: Byte = 0x01
- private val MODE_PHONE_TO_WATCH: Byte = 0x02
- private val VERSION: Byte = 0x01
- private val SW_OK = 0x9000
- private val STATUS_SUCCESS: Byte = 0x04
- private val STATUS_FAILURE: Byte = 0x05
- private val STATUS_SYNCING: Byte = 0x03
- private val STATUS_ARMED_SEND: Byte = 0x01
- private val STATUS_ARMED_RECEIVE: Byte = 0x02
- private val STATUS_IDLE: Byte = 0x00
- private val CONFIRMATION_MAX_POLLS = 84 // ~21s with adaptive delay schedule
- private val PREFERRED_MAX_CHUNK_SIZE = 2048
- // Fast mode is the default; ceremony APDUs are only enabled when fastMode is false.
- private val ENABLE_TRANSFER_CEREMONY = !fastMode
-
- private data class ReadPayloadResult(
- val character: Character,
- val session: VitalWearHceSessionInfo,
- )
-
- private data class HceTransferMetrics(
- val direction: String,
- val payloadBytes: Int,
- val negotiatedChunkBytes: Int,
- val apduCountTotal: Int,
- val apduCountData: Int,
- val statusPollCount: Int,
- val tSelectNegotiateMs: Long,
- val tChunkLoopMs: Long,
- val tCommitAckMs: Long,
- val tConfirmMs: Long,
- val tTotalMs: Long,
- val result: String,
- ) {
- fun log() {
- Log.i(
- "HCE_CLIENT_METRICS",
- "dir=$direction payloadBytes=$payloadBytes chunk=$negotiatedChunkBytes apduTotal=$apduCountTotal " +
- "apduData=$apduCountData polls=$statusPollCount selectNegotiateMs=$tSelectNegotiateMs " +
- "chunkLoopMs=$tChunkLoopMs commitAckMs=$tCommitAckMs confirmMs=$tConfirmMs totalMs=$tTotalMs result=$result"
- )
- }
- }
-
- /** MOVE READ: read character bytes first, let caller import/validate, then COMMIT only on success.
- * If [onCharacterRead] returns false, COMMIT is skipped so source can remain on the watch.
- */
- fun moveCharacterFromWatch(onCharacterRead: (Character) -> Boolean): Boolean {
- val readResult = readCharacterFromWatchWithoutCommit()
- if (!onCharacterRead(readResult.character)) {
- return false
- }
- if (!readResult.session.canCommit()) {
- error("Session does not allow COMMIT for watch->phone transfer")
- }
- requireOk(sendApdu(INS_COMMIT, byteArrayOf()), "COMMIT")
- return true
- }
-
- /** READ: watch has called armSend() — phone reads the character off the watch. */
- fun receiveCharacterFromWatch(): Character {
- val readResult = readCharacterFromWatchWithoutCommit()
- if (!readResult.session.canCommit()) {
- error("Session does not allow COMMIT for watch->phone transfer")
- }
- requireOk(sendApdu(INS_COMMIT, byteArrayOf()), "COMMIT")
- return readResult.character
- }
-
- private fun readCharacterFromWatchWithoutCommit(): ReadPayloadResult {
- val transferStartMs = SystemClock.elapsedRealtime()
- var apduCount = 0
- var dataApduCount = 0
- selectAid()
- apduCount++
- val session = negotiateSession(MODE_WATCH_TO_PHONE, desiredMaxChunkSize())
- apduCount++
- val negotiatedMs = SystemClock.elapsedRealtime()
- check(session.isWatchToPhone()) { "NEGOTIATE direction mismatch for WATCH_TO_PHONE" }
- Log.d("HCE_CLIENT", "Receiving ${session.payloadLength} bytes in chunks of ${session.maxChunkSize}")
- val payload = ByteArray(session.payloadLength)
- var offset = 0
- while (offset < session.payloadLength) {
- val chunkResponse = sendApdu(INS_READ_CHUNK, intToBytes(offset))
- apduCount++
- dataApduCount++
- requireOk(chunkResponse, "READ_CHUNK @ $offset")
- val chunkData = chunkResponse.dropLast(2).toByteArray()
- chunkData.copyInto(payload, offset)
- offset += chunkData.size
- }
- val chunksDoneMs = SystemClock.elapsedRealtime()
- HceTransferMetrics(
- direction = "WATCH_TO_PHONE_READ",
- payloadBytes = payload.size,
- negotiatedChunkBytes = session.maxChunkSize,
- apduCountTotal = apduCount,
- apduCountData = dataApduCount,
- statusPollCount = 0,
- tSelectNegotiateMs = negotiatedMs - transferStartMs,
- tChunkLoopMs = chunksDoneMs - negotiatedMs,
- tCommitAckMs = 0L,
- tConfirmMs = 0L,
- tTotalMs = chunksDoneMs - transferStartMs,
- result = "read_complete",
- ).log()
- return ReadPayloadResult(
- character = Character.parseFrom(payload),
- session = session,
- )
- }
-
- /** WRITE: watch has called armReceive() — phone pushes a character to the watch. */
- fun sendCharacterToWatch(character: Character, closeSyncUiAfterCommit: Boolean = true) {
- val session = sendCharacterToWatchInternal(
- character = character,
- closeSyncUiAfterCommit = closeSyncUiAfterCommit,
- requestedChunkSize = desiredMaxChunkSize(),
- )
- rememberBestChunk(session.maxChunkSize)
- }
-
- private fun sendCharacterToWatchInternal(
- character: Character,
- closeSyncUiAfterCommit: Boolean,
- requestedChunkSize: Int,
- ): VitalWearHceSessionInfo {
- val transferStartMs = SystemClock.elapsedRealtime()
- var apduCount = 0
- var dataApduCount = 0
- selectAid()
- apduCount++
- if (ENABLE_TRANSFER_CEREMONY) {
- // Optional toy-like ceremony; kept behind a toggle to avoid transfer overhead.
- sendApdu(INS_VIBRATE, byteArrayOf())
- apduCount++
- sendApdu(INS_SYNC_UI, byteArrayOf(0x01))
- apduCount++
- }
-
- val payload = character.toByteArray()
- val session = negotiateSession(MODE_PHONE_TO_WATCH, requestedChunkSize)
- apduCount++
- val negotiatedMs = SystemClock.elapsedRealtime()
- check(session.isPhoneToWatch()) { "NEGOTIATE direction mismatch for PHONE_TO_WATCH" }
- Log.d("HCE_CLIENT", "Sending ${payload.size} bytes in chunks of ${session.maxChunkSize}")
- var offset = 0
- while (offset < payload.size) {
- val chunkEnd = (offset + session.maxChunkSize).coerceAtMost(payload.size)
- val chunk = payload.copyOfRange(offset, chunkEnd)
- requireOk(sendApdu(INS_WRITE_CHUNK, intToBytes(offset) + chunk), "WRITE_CHUNK @ $offset")
- apduCount++
- dataApduCount++
- offset = chunkEnd
- }
- val chunksDoneMs = SystemClock.elapsedRealtime()
- if (!session.canCommit()) {
- error("Session does not allow COMMIT for phone->watch transfer")
- }
- val commitStartMs = SystemClock.elapsedRealtime()
- requireOk(sendApdu(INS_COMMIT, byteArrayOf()), "COMMIT")
- apduCount++
- val commitDoneMs = SystemClock.elapsedRealtime()
-
- if (ENABLE_TRANSFER_CEREMONY && closeSyncUiAfterCommit) {
- sendApdu(INS_SYNC_UI, byteArrayOf(0x00))
- apduCount++
- }
- HceTransferMetrics(
- direction = "PHONE_TO_WATCH_WRITE",
- payloadBytes = payload.size,
- negotiatedChunkBytes = session.maxChunkSize,
- apduCountTotal = apduCount,
- apduCountData = dataApduCount,
- statusPollCount = 0,
- tSelectNegotiateMs = negotiatedMs - transferStartMs,
- tChunkLoopMs = chunksDoneMs - negotiatedMs,
- tCommitAckMs = commitDoneMs - commitStartMs,
- tConfirmMs = 0L,
- tTotalMs = commitDoneMs - transferStartMs,
- result = "commit_acked",
- ).log()
- return session
- }
-
- /**
- * WRITE + confirmation polling.
- * Returns true only when the watch reports import success.
- */
- fun sendCharacterToWatchAndConfirm(character: Character): Boolean {
- var lastError: Exception? = null
- for (candidateChunk in chunkCandidates()) {
- val confirmStartMs = SystemClock.elapsedRealtime()
- val session = try {
- sendCharacterToWatchInternal(
- character = character,
- closeSyncUiAfterCommit = false,
- requestedChunkSize = candidateChunk,
- )
- } catch (e: Exception) {
- lastError = e
- Log.w("HCE_CLIENT", "Chunk attempt failed before confirmation at chunk=$candidateChunk", e)
- continue
- }
- if (!session.canPollStatus()) {
- return false
- }
- var polls = 0
- var apduCount = 0
- var result = "timeout"
- val confirmed = runBlocking {
- repeat(CONFIRMATION_MAX_POLLS) {
- val statusResponse = sendApdu(INS_STATUS, byteArrayOf())
- apduCount++
- polls++
- requireOk(statusResponse, "STATUS")
- val statusByte = statusResponse.dropLast(2).firstOrNull()
- Log.d("HCE_CLIENT", "STATUS poll[$it]: statusByte=${statusByte?.toInt()?.and(0xFF)}")
- if (statusByte == STATUS_SUCCESS) {
- result = "success"
- return@runBlocking true
- }
- if (statusByte == STATUS_FAILURE) {
- result = "failure"
- return@runBlocking false
- }
- if (
- statusByte == STATUS_SYNCING ||
- statusByte == STATUS_ARMED_RECEIVE ||
- statusByte == STATUS_ARMED_SEND ||
- statusByte == STATUS_IDLE
- ) {
- delay(delayForPoll(it))
- return@repeat
- }
- // Unknown status byte: keep waiting briefly instead of failing early.
- delay(delayForPoll(it))
- }
- false
- }
- if (ENABLE_TRANSFER_CEREMONY) {
- // End the sync UI after we have consumed terminal status.
- runCatching {
- sendApdu(INS_SYNC_UI, byteArrayOf(0x00))
- }
- }
- HceTransferMetrics(
- direction = "PHONE_TO_WATCH_CONFIRM",
- payloadBytes = character.serializedSize,
- negotiatedChunkBytes = session.maxChunkSize,
- apduCountTotal = apduCount,
- apduCountData = 0,
- statusPollCount = polls,
- tSelectNegotiateMs = 0L,
- tChunkLoopMs = 0L,
- tCommitAckMs = 0L,
- tConfirmMs = SystemClock.elapsedRealtime() - confirmStartMs,
- tTotalMs = SystemClock.elapsedRealtime() - confirmStartMs,
- result = "$result(requested=$candidateChunk)",
- ).log()
- if (confirmed) {
- rememberBestChunk(session.maxChunkSize)
- return true
- }
- // Explicit failure means import rejected payload; retrying chunk size won't help.
- if (result == "failure") {
- return false
- }
- }
- lastError?.let { throw it }
- return false
- }
-
- /**
- * Verification step for two-tap transfer UX:
- * confirms the watch is armed to receive before the actual write step.
- */
- fun verifyWatchReadyToReceive(): Boolean {
- selectAid()
- val session = negotiateSession(MODE_PHONE_TO_WATCH, desiredMaxChunkSize())
- return session.canCommit()
- }
-
- private fun selectAid() {
- val selectApdu = byteArrayOf(0x00, 0xA4.toByte(), 0x04, 0x00, AID.size.toByte()) + AID
- val response = isoDep.transceive(selectApdu)
- if (statusWord(response) != SW_OK) error("SELECT AID failed: ${response.toHex()}")
- }
- private fun sendApdu(ins: Byte, data: ByteArray): ByteArray {
- val apdu = if (data.size <= 0xFF) {
- byteArrayOf(CLA, ins, 0x00, 0x00, data.size.toByte()) + data
- } else {
- byteArrayOf(
- CLA,
- ins,
- 0x00,
- 0x00,
- 0x00,
- ((data.size ushr 8) and 0xFF).toByte(),
- (data.size and 0xFF).toByte(),
- ) + data
- }
- return isoDep.transceive(apdu)
- }
-
- private fun negotiateSession(mode: Byte, requestedChunkSize: Int): VitalWearHceSessionInfo {
- val negResponse = sendApdu(
- INS_NEGOTIATE,
- byteArrayOf(mode, VERSION) + intToShortBytes(requestedChunkSize)
- )
- requireOk(negResponse, "NEGOTIATE")
- val negotiatedMode = when (negResponse[1]) {
- MODE_WATCH_TO_PHONE -> VitalWearHceTransferDirection.WATCH_TO_PHONE
- MODE_PHONE_TO_WATCH -> VitalWearHceTransferDirection.PHONE_TO_WATCH
- else -> error("NEGOTIATE returned unknown direction: ${negResponse[1]}")
- }
- val maxChunk = ((negResponse[2].toInt() and 0xFF) shl 8) or (negResponse[3].toInt() and 0xFF)
- val payloadLen =
- ((negResponse[4].toInt() and 0xFF) shl 24) or
- ((negResponse[5].toInt() and 0xFF) shl 16) or
- ((negResponse[6].toInt() and 0xFF) shl 8) or
- (negResponse[7].toInt() and 0xFF)
- return VitalWearHceSession(
- direction = negotiatedMode,
- maxChunkSize = maxChunk,
- payloadLength = payloadLen,
- )
- }
-
- private fun desiredMaxChunkSize(): Int {
- val transceiveLimit = runCatching { isoDep.maxTransceiveLength }.getOrNull()
- val maxByDevice = if (transceiveLimit != null && transceiveLimit > 0) {
- // Extended APDU write overhead: 7-byte header + 4-byte offset.
- (transceiveLimit - 11).coerceAtLeast(1)
- } else {
- PREFERRED_MAX_CHUNK_SIZE
- }
- val cappedByDevice = maxByDevice.coerceAtMost(PREFERRED_MAX_CHUNK_SIZE)
- val cachedBest = cachedBestChunk()
- return if (cachedBest == null) {
- cappedByDevice
- } else {
- cachedBest.coerceIn(MIN_CHUNK_SIZE, cappedByDevice)
- }
- }
-
- private fun chunkCandidates(): List {
- val maxChunk = desiredMaxChunkSize()
- val baselineCandidates = listOf(maxChunk, 2048, 1536, 1024, 768, 512, MIN_CHUNK_SIZE)
- return baselineCandidates
- .map { it.coerceAtMost(maxChunk) }
- .filter { it in MIN_CHUNK_SIZE..maxChunk }
- .distinct()
- }
-
- private fun deviceKey(): String {
- val tagId = isoDep.tag?.id ?: return "unknown-device"
- return tagId.toHex()
- }
-
- private fun cachedBestChunk(): Int? {
- synchronized(bestChunkByDevice) {
- return bestChunkByDevice[deviceKey()]
- }
- }
-
- private fun rememberBestChunk(chunkSize: Int) {
- synchronized(bestChunkByDevice) {
- bestChunkByDevice[deviceKey()] = chunkSize
- }
- }
-
- private fun delayForPoll(pollIndex: Int): Long {
- return when {
- pollIndex < 20 -> 80L
- pollIndex < 56 -> 175L
- else -> 300L
- }
- }
-
- private fun intToShortBytes(value: Int): ByteArray = byteArrayOf(
- ((value ushr 8) and 0xFF).toByte(),
- (value and 0xFF).toByte()
- )
-
- private fun requireOk(response: ByteArray, context: String) {
- if (statusWord(response) != SW_OK) error("$context failed: ${response.toHex()}")
- }
- 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)
- }
- private fun intToBytes(value: Int): ByteArray = byteArrayOf(
- ((value ushr 24) and 0xFF).toByte(),
- ((value ushr 16) and 0xFF).toByte(),
- ((value ushr 8) and 0xFF).toByte(),
- (value and 0xFF).toByte()
- )
- private fun ByteArray.toHex() = joinToString("") { "%02X".format(it) }
-}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/hce/VitalWearHceSession.kt b/app/src/main/java/com/github/nacabaro/vbhelper/transfer/hce/VitalWearHceSession.kt
deleted file mode 100644
index 64d9605..0000000
--- a/app/src/main/java/com/github/nacabaro/vbhelper/transfer/hce/VitalWearHceSession.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.github.nacabaro.vbhelper.transfer.hce
-
-internal enum class VitalWearHceTransferDirection {
- WATCH_TO_PHONE,
- PHONE_TO_WATCH,
-}
-
-internal interface VitalWearHceSessionInfo {
- val direction: VitalWearHceTransferDirection
- val maxChunkSize: Int
- val payloadLength: Int
-
- fun isWatchToPhone(): Boolean {
- return direction == VitalWearHceTransferDirection.WATCH_TO_PHONE
- }
-
- fun isPhoneToWatch(): Boolean {
- return direction == VitalWearHceTransferDirection.PHONE_TO_WATCH
- }
-
- fun canCommit(): Boolean {
- return true
- }
-
- fun canPollStatus(): Boolean {
- // Import confirmation polling only applies for phone->watch transfers.
- return isPhoneToWatch()
- }
-}
-
-internal data class VitalWearHceSession(
- override val direction: VitalWearHceTransferDirection,
- override val maxChunkSize: Int,
- override val payloadLength: Int,
-) : VitalWearHceSessionInfo
-
-
-
-
-
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/utils/DeviceType.kt b/app/src/main/java/com/github/nacabaro/vbhelper/utils/DeviceType.kt
index 7912e82..5d06c67 100644
--- a/app/src/main/java/com/github/nacabaro/vbhelper/utils/DeviceType.kt
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/utils/DeviceType.kt
@@ -1,14 +1,6 @@
package com.github.nacabaro.vbhelper.utils
enum class DeviceType {
- VBDevice, // Bandai Vital Bracelet
- BEDevice, // Bandai BE Bracelet
- VitalWear // WearOS VitalWear app
-}
-
-fun detectDeviceType(deviceName: String): DeviceType = when {
- deviceName.startsWith("VitalWear", ignoreCase = true) || deviceName.startsWith("VW-", ignoreCase = true) -> DeviceType.VitalWear
- deviceName.startsWith("BE-", ignoreCase = true) -> DeviceType.BEDevice
- deviceName.startsWith("VB-", ignoreCase = true) -> DeviceType.VBDevice
- else -> DeviceType.VBDevice // Default/fallback, can be refined
+ VBDevice,
+ BEDevice
}
\ 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
deleted file mode 100644
index 263b4d8..0000000
--- a/app/src/main/proto/com/github/cfogrady/vitalwear/protos/Character.proto
+++ /dev/null
@@ -1,51 +0,0 @@
-syntax = "proto3";
-
-option java_package = "com.github.cfogrady.vitalwear.protos";
-option java_multiple_files = true;
-
-message Character {
- string card_name = 1;
- int32 card_id = 2;
-
- message CharacterStats {
- int32 slot_id = 1;
- int32 vitals = 2;
- int64 training_time_remaining_in_seconds = 3;
- int64 time_until_next_transformation = 4;
- int32 trained_bp = 5;
- int32 trained_hp = 6;
- int32 trained_ap = 7;
- int32 trained_pp = 8;
- bool injured = 9;
- int32 accumulated_daily_injuries = 10;
- int32 total_battles = 11;
- int32 current_phase_battles = 12;
- int32 total_wins = 13;
- int32 current_phase_wins = 14;
- int32 mood = 15;
- }
- CharacterStats character_stats = 3;
-
- message Settings {
- bool training_in_background = 1;
- enum AllowedBattles {
- CARD_ONLY = 0;
- ALL_FRANCHISE = 1;
- ALL_FRANCHISE_AND_DIM = 2;
- ALL = 3;
- }
- AllowedBattles allowed_battles = 2;
- optional int32 assumed_franchise = 3;
- }
- Settings settings = 4;
-
- message TransformationEvent {
- string card_name = 1;
- int32 phase = 2;
- int32 slot_id = 3;
- }
-
- repeated TransformationEvent transformation_history = 5;
-
- map max_adventure_completed_by_card = 6;
-}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ac8a85d..ea7b2b2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -44,8 +44,8 @@
Scan a Vital Bracelet
- Watch to VBH
- VBH to Watch
+ Vital Bracelet to App
+ App to Vital Bracelet
No NFC on device!
Tag detected is not VB
@@ -65,12 +65,12 @@
Import APK
- Import Secrets From Vital Arena APK
+ Import Secrets From Vital Arena 2.1.0 APK
Import card
- Import DiM/BEm
+ Import DiM/BEm card file
Credits
@@ -106,9 +106,7 @@
Designing the app icon in SVG.
-
- Devout Debugger.
-
+
Place your Vital Bracelet near the reader...
Cancel
@@ -186,12 +184,11 @@
Delete card
My characters
- Storage is Empty
+ Nothing to see here
This character is in an adventure
Character image
Send to watch
- Send to VitalWear
Set active
Send on adventure
Delete character
@@ -219,4 +216,4 @@
Dismiss
Remove
-
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
deleted file mode 100644
index 8a3c842..0000000
--- a/app/src/main/res/xml/provider_paths.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
diff --git a/app/src/test/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverterProfileSelectionTest.kt b/app/src/test/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverterProfileSelectionTest.kt
deleted file mode 100644
index ff4fef7..0000000
--- a/app/src/test/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverterProfileSelectionTest.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.github.nacabaro.vbhelper.screens.scanScreen.converters
-
-import com.github.nacabaro.vbhelper.utils.DeviceType
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-class ToNfcConverterProfileSelectionTest {
- @Test
- fun shouldEncodeAsBem_usesStoredTypeWhenForcedProfileMissing() {
- assertTrue(ToNfcConverter.shouldEncodeAsBem(null, DeviceType.BEDevice))
- assertFalse(ToNfcConverter.shouldEncodeAsBem(null, DeviceType.VBDevice))
- }
-
- @Test
- fun shouldEncodeAsBem_forcedVBWinsOverStoredBE() {
- assertFalse(ToNfcConverter.shouldEncodeAsBem(DeviceType.VBDevice, DeviceType.BEDevice))
- }
-
- @Test
- fun shouldEncodeAsBem_forcedBEWinsOverStoredVB() {
- assertTrue(ToNfcConverter.shouldEncodeAsBem(DeviceType.BEDevice, DeviceType.VBDevice))
- }
-}
-
diff --git a/app/src/test/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporterTest.kt b/app/src/test/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporterTest.kt
deleted file mode 100644
index edc09d5..0000000
--- a/app/src/test/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporterTest.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.github.nacabaro.vbhelper.source
-
-import com.github.cfogrady.vbnfc.data.NfcCharacter
-import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
-import com.github.nacabaro.vbhelper.utils.DeviceType
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class VitalWearCharacterExporterTest {
- @Test
- fun resolveTrainingSeconds_preservesBeTrainingMinutesAsSeconds() {
- val beData = testBeData(remainingTrainingTimeInMinutes = 37)
-
- assertEquals(37L * 60L, resolveTrainingSeconds(DeviceType.BEDevice, beData))
- }
-
- @Test
- fun resolveTrainingSeconds_defaultsNonBeCharactersToVitalWearTrainingWindow() {
- assertEquals(
- DEFAULT_VITALWEAR_TRAINING_TIME_SECONDS,
- resolveTrainingSeconds(DeviceType.VBDevice, beData = null)
- )
- }
-
- @Test
- fun resolveTrainingSeconds_keepsZeroForBeCharactersWithoutStoredBeData() {
- assertEquals(0L, resolveTrainingSeconds(DeviceType.BEDevice, beData = null))
- }
-
- private fun testBeData(remainingTrainingTimeInMinutes: Int): BECharacterData {
- return BECharacterData(
- id = 1L,
- trainingHp = 0,
- trainingAp = 0,
- trainingBp = 0,
- remainingTrainingTimeInMinutes = remainingTrainingTimeInMinutes,
- itemEffectMentalStateValue = 0,
- itemEffectMentalStateMinutesRemaining = 0,
- itemEffectActivityLevelValue = 0,
- itemEffectActivityLevelMinutesRemaining = 0,
- itemEffectVitalPointsChangeValue = 0,
- itemEffectVitalPointsChangeMinutesRemaining = 0,
- abilityRarity = NfcCharacter.AbilityRarity.entries.first(),
- abilityType = 0,
- abilityBranch = 0,
- abilityReset = 0,
- rank = 0,
- itemType = 0,
- itemMultiplier = 0,
- itemRemainingTime = 0,
- otp0 = "",
- otp1 = "",
- minorVersion = 0,
- majorVersion = 0,
- )
- }
-}
-
diff --git a/app/src/test/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporterTest.kt b/app/src/test/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporterTest.kt
deleted file mode 100644
index 7a1f634..0000000
--- a/app/src/test/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporterTest.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-package com.github.nacabaro.vbhelper.source
-
-import com.github.cfogrady.vitalwear.protos.Character
-import com.github.nacabaro.vbhelper.domain.card.Card
-import com.github.nacabaro.vbhelper.utils.DeviceType
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Test
-
-class VitalWearCharacterImporterTest {
- @Test
- fun normalizeTransformationCountdownMinutes_keepsOneMinuteMinimumWhenCharacterCanStillEvolve() {
- assertEquals(1, normalizeTransformationCountdownMinutes(0, hasPossibleTransformations = true))
- }
-
- @Test
- fun normalizeTransformationCountdownMinutes_preservesPositiveCountdownWhenCharacterCanStillEvolve() {
- assertEquals(12, normalizeTransformationCountdownMinutes(12, hasPossibleTransformations = true))
- }
-
- @Test
- fun normalizeTransformationCountdownMinutes_allowsZeroWhenNoTransformationsRemain() {
- assertEquals(0, normalizeTransformationCountdownMinutes(0, hasPossibleTransformations = false))
- }
-
- @Test
- fun resolveDeviceType_prefersExplicitTransferTypeOverFallbackHeuristic() {
- assertEquals(
- DeviceType.VBDevice,
- resolveDeviceType(
- transferDeviceType = Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_VB,
- fallbackIsBeCharacter = true,
- )
- )
- }
-
- @Test
- fun resolveDeviceType_fallsBackToBeWhenTransferTypeIsUnspecified() {
- assertEquals(
- DeviceType.BEDevice,
- resolveDeviceType(
- transferDeviceType = Character.CharacterStats.TransferDeviceType.TRANSFER_DEVICE_TYPE_UNSPECIFIED,
- fallbackIsBeCharacter = true,
- )
- )
- }
-
- @Test
- fun resolveDeviceType_usesFallbackWhenTransferTypeIsUnrecognized() {
- assertEquals(
- DeviceType.VBDevice,
- resolveDeviceType(
- transferDeviceType = Character.CharacterStats.TransferDeviceType.UNRECOGNIZED,
- fallbackIsBeCharacter = false,
- )
- )
- }
-
- @Test
- fun selectImportedCard_prefersUniqueCardIdWhenNameWasRenamed() {
- val selected = selectImportedCard(
- candidates = listOf(
- testCard(id = 1, cardId = 77, name = "My Custom DIM"),
- testCard(id = 2, cardId = 88, name = "Other"),
- ),
- incomingCardName = "Impulse City",
- incomingCardId = 77,
- )
-
- assertNotNull(selected)
- assertEquals("My Custom DIM", selected?.name)
- }
-
- @Test
- fun selectImportedCard_matchesNormalizedRenamedNameWhenCardIdMissing() {
- val selected = selectImportedCard(
- candidates = listOf(
- testCard(id = 1, cardId = 0, name = "Impulse City!"),
- ),
- incomingCardName = "impulse-city",
- incomingCardId = null,
- )
-
- assertNotNull(selected)
- assertEquals("Impulse City!", selected?.name)
- }
-
- @Test
- fun cardNamesMatch_ignoresCaseAndPunctuation() {
- assertEquals(true, cardNamesMatch("My DIM: Zero", "my-dim zero"))
- }
-
- private fun testCard(id: Long, cardId: Int, name: String): Card {
- return Card(
- id = id,
- cardId = cardId,
- logo = byteArrayOf(),
- logoWidth = 0,
- logoHeight = 0,
- name = name,
- stageCount = 0,
- isBEm = false,
- )
- }
-}
-