From 5787069761a2fca1b03c4876f5504265f4335c17 Mon Sep 17 00:00:00 2001 From: nacabaro Date: Mon, 22 Jun 2026 14:55:12 +0000 Subject: [PATCH] Revert 457R0 work, merged wrong branch Revert "Merge pull request #53 from 457R0/main" This reverts commit 8aa31e819be0f5651295adc76a1a38f6e62331a7, reversing changes made to af27fc4933197a92380cd470fa535fd6292effec. --- app/lint-baseline.xml | 1782 ----------------- .../scanScreen/ScanNfcaIntegrationTest.kt | 439 ---- app/src/main/AndroidManifest.xml | 16 +- .../common/data/SharedDatabaseFactory.kt | 22 - .../common/data/SharedTransferDatabase.kt | 16 - .../common/data/SharedTransferSeenDao.kt | 47 - .../common/data/SharedTransferSeenEntity.kt | 24 - .../github/nacabaro/vbhelper/MainActivity.kt | 90 +- .../companion/card/ByteManipulation.kt | 46 - .../card/CompanionImportCardActivity.kt | 400 ---- .../card/CompanionValidateCardActivity.kt | 162 -- .../vbhelper/companion/card/VBNfcData.kt | 48 - .../vbhelper/companion/common/ChannelTypes.kt | 10 - .../CompanionFirmwareImportActivity.kt | 140 -- .../vbhelper/companion/logging/TinyLogTree.kt | 77 - .../companion/logs/CompanionLogService.kt | 122 -- .../logs/CompanionWatchLogsActivity.kt | 41 - .../logs/WatchCommunicationService.kt | 21 - .../CompanionCharacterImportActivity.kt | 23 - .../companion/ui/CompanionConfirmation.kt | 42 - .../vbhelper/companion/ui/CompanionLoading.kt | 32 - .../validation/CompanionValidatedDatabase.kt | 14 - .../companion/validation/ValidatedCardDao.kt | 16 - .../validation/ValidatedCardEntity.kt | 13 - .../validation/ValidatedCardManager.kt | 35 - .../github/nacabaro/vbhelper/daos/CardDao.kt | 17 +- .../nacabaro/vbhelper/daos/CardProgressDao.kt | 5 +- .../daos/CharacterTransferPolicyDao.kt | 17 - .../nacabaro/vbhelper/daos/TransferSeenDao.kt | 7 - .../vbhelper/daos/UserCharacterDao.kt | 18 +- .../vbhelper/daos/VitalWearSettingsDao.kt | 17 - .../device_data/CharacterTransferPolicy.kt | 32 - .../domain/device_data/UnifiedCharacter.kt | 31 - .../device_data/VitalWearCharacterSettings.kt | 13 - .../domain/transfer/TransferSeenEntity.kt | 7 - .../nacabaro/vbhelper/dtos/CharacterDtos.kt | 9 +- .../vbhelper/navigation/AppNavigation.kt | 3 +- .../navigation/BottomNavigationBar.kt | 15 +- .../navigation/NavigationExtensions.kt | 0 .../screens/homeScreens/HomeScreen.kt | 97 +- .../homeScreens/screens/UnifiedHomeScreen.kt | 241 --- .../scanScreen/ChooseConnectionScreen.kt | 5 - .../screens/scanScreen/DetectedTransport.kt | 8 - .../scanScreen/ScanScreenControllerImpl.kt | 22 +- .../screens/scanScreen/TransferHaptics.kt | 51 - .../screens/TransferAnimationScreen.kt | 267 --- .../screens/settingsScreen/CreditsScreen.kt | 2 - .../screens/storageScreen/StorageDialog.kt | 46 +- .../screens/storageScreen/StorageScreen.kt | 1 - .../vbhelper/source/CardSettingsRepository.kt | 28 - .../vbhelper/source/DeviceTypeConverter.kt | 54 - .../source/VitalWearCharacterExporter.kt | 114 -- .../source/VitalWearCharacterImporter.kt | 163 -- .../VitalWearTransformationCountdown.kt | 38 - .../vbhelper/transfer/CardImportProvider.kt | 106 - .../CharacterTransferPolicyResolver.kt | 112 -- .../transfer/SharedTransferSeenProvider.kt | 67 - .../vbhelper/transfer/TransferRoute.kt | 31 - .../transfer/hce/VitalWearHceReaderClient.kt | 437 ---- .../transfer/hce/VitalWearHceSession.kt | 40 - .../nacabaro/vbhelper/utils/DeviceType.kt | 12 +- .../cfogrady/vitalwear/protos/Character.proto | 51 - app/src/main/res/values/strings.xml | 17 +- app/src/main/res/xml/provider_paths.xml | 6 - .../ToNfcConverterProfileSelectionTest.kt | 25 - .../source/VitalWearCharacterExporterTest.kt | 58 - .../source/VitalWearCharacterImporterTest.kt | 106 - 67 files changed, 88 insertions(+), 5986 deletions(-) delete mode 100644 app/lint-baseline.xml delete mode 100644 app/src/androidTest/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanNfcaIntegrationTest.kt delete mode 100644 app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedDatabaseFactory.kt delete mode 100644 app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferDatabase.kt delete mode 100644 app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferSeenDao.kt delete mode 100644 app/src/main/java/com/github/cfogrady/vitalwear/common/data/SharedTransferSeenEntity.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/card/ByteManipulation.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/card/CompanionImportCardActivity.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/card/CompanionValidateCardActivity.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/card/VBNfcData.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/common/ChannelTypes.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/firmware/CompanionFirmwareImportActivity.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/logging/TinyLogTree.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/CompanionLogService.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/CompanionWatchLogsActivity.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/logs/WatchCommunicationService.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/transfer/CompanionCharacterImportActivity.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/ui/CompanionConfirmation.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/ui/CompanionLoading.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/CompanionValidatedDatabase.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardDao.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardEntity.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/companion/validation/ValidatedCardManager.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterTransferPolicyDao.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/daos/TransferSeenDao.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/daos/VitalWearSettingsDao.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/CharacterTransferPolicy.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/UnifiedCharacter.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/domain/device_data/VitalWearCharacterSettings.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/domain/transfer/TransferSeenEntity.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationExtensions.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/screens/UnifiedHomeScreen.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/DetectedTransport.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/TransferHaptics.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/screens/TransferAnimationScreen.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/CardSettingsRepository.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/DeviceTypeConverter.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporter.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporter.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/VitalWearTransformationCountdown.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/transfer/CardImportProvider.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/transfer/CharacterTransferPolicyResolver.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/transfer/SharedTransferSeenProvider.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/transfer/TransferRoute.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/transfer/hce/VitalWearHceReaderClient.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/transfer/hce/VitalWearHceSession.kt delete mode 100644 app/src/main/proto/com/github/cfogrady/vitalwear/protos/Character.proto delete mode 100644 app/src/main/res/xml/provider_paths.xml delete mode 100644 app/src/test/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverterProfileSelectionTest.kt delete mode 100644 app/src/test/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterExporterTest.kt delete mode 100644 app/src/test/java/com/github/nacabaro/vbhelper/source/VitalWearCharacterImporterTest.kt 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, - ) - } -} -