Compare commits

...
Author SHA1 Message Date
nacabaroandGitHub 81bfec5ac5 Merge pull request #55 from b13st/fix/vitalwear-hce-transfer
Fix VitalWear HCE transfer in both directions
2026-06-23 20:30:46 +00:00
nacabaroandGitHub 8ec98b9d39 Merge pull request #54 from shiki8826/main
Thank you very much
2026-06-23 20:30:04 +00:00
b13st e549fb4469 Fix VitalWear HCE transfer in both directions
- Detect ISO-DEP tags (VitalWear watch) vs NFC-A (physical bracelets)
- Watch->Phone: use VitalWearHceReaderClient + VitalWearCharacterImporter
- Phone->Watch: use VitalWearHceReaderClient + VitalWearCharacterExporter
- Add AppDatabase to ScanScreenControllerImpl for HCE import/export
- Extract buildCharacterProto() from VitalWearCharacterExporter
- Add JitPack repository to settings.gradle.kts
2026-06-23 13:44:26 +02:00
shiki8826 8d62437aae 日本語翻訳の修正及び追加項目の翻訳 2026-06-19 20:46:23 +09:00
nacabaro 7a8815694d Remove test file, i'm not figuring this out tonight 2026-06-18 22:44:14 +00:00
nacabaro 464f2e48a7 Add basic migration to support the vitalwear thing 2026-06-18 22:37:51 +00:00
nacabaro f641e8aaa0 Compile fix before merging 2026-06-18 22:17:52 +00:00
20 changed files with 381 additions and 609 deletions
+12 -15
View File
@@ -4,8 +4,8 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp") version "2.3.0"
id("com.google.protobuf")
alias(libs.plugins.vksp)
alias(libs.plugins.vprotobuf)
}
android {
@@ -17,7 +17,7 @@ android {
minSdk = 28
targetSdk = 36
versionCode = 1
versionName = "Alpha 0.6.3"
versionName = "Alpha 0.6.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -90,6 +90,11 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.play.services.wearable)
implementation(libs.kotlinx.coroutines.play.services)
implementation(libs.timber)
implementation(libs.tinylog.api)
implementation(libs.tinylog.impl)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
@@ -104,18 +109,10 @@ dependencies {
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation("androidx.navigation:navigation-compose:2.7.0")
implementation("com.google.android.material:material:1.2.0")
implementation(libs.protobuf.javalite)
implementation("androidx.compose.material:material")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.gson)
// HTTP request logging
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation(libs.okhttp.logging.interceptor)
}
@@ -1,439 +0,0 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
import android.nfc.IsoDep
import android.nfc.NfcA
import android.nfc.Tag
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import org.junit.Test
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever
import org.mockito.kotlin.any
import org.mockito.kotlin.verify
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
/**
* Instrumented tests for NFC-A (Bandai toy) transfer functionality.
*
* These tests verify:
* - Transport detection (NFC-A vs ISO-DEP)
* - Read operations (Watch to VBH)
* - Write operations (VBH to Watch)
* - Slot state detection
* - Error handling
*
* Run with:
* ./gradlew connectedAndroidTest --tests ScanNfcaIntegrationTest
*/
@RunWith(AndroidJUnit4::class)
class ScanNfcaIntegrationTest {
@Mock
private lateinit var mockTag: Tag
@Mock
private lateinit var mockNfcA: NfcA
@Mock
private lateinit var mockIsoDep: IsoDep
@Mock
private lateinit var mockTagCommunicator: TagCommunicator
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
}
// ==========================================
// TEST GROUP 1: Transport Detection
// ==========================================
/**
* TEST 1.1: Verify NFC-A is correctly detected
*
* When: Tag with NFC-A support is detected
* Then: Transport should be NFC_A
*/
@Test
fun testNfcATransportDetection() {
// Arrange
whenever(NfcA.get(mockTag)).thenReturn(mockNfcA)
whenever(IsoDep.get(mockTag)).thenReturn(null)
// Act
val nfcA = NfcA.get(mockTag)
val isoDep = IsoDep.get(mockTag)
// Assert
assertNotNull(nfcA, "NfcA should be detected")
assertNull(isoDep, "IsoDep should not be present")
}
/**
* TEST 1.2: Verify ISO-DEP vs NFC-A detection priority
*
* When: Both NFC-A and ISO-DEP present (shouldn't happen, but test anyway)
* Then: ISO-DEP should take priority if it's VitalWear
*/
@Test
fun testIsoDepPriorityOverNfcA() {
// Arrange
whenever(NfcA.get(mockTag)).thenReturn(mockNfcA)
whenever(IsoDep.get(mockTag)).thenReturn(mockIsoDep)
// Act
val isoDep = IsoDep.get(mockTag)
val nfcA = NfcA.get(mockTag)
// Assert
// In actual code, ISO-DEP is checked first
assertNotNull(isoDep, "ISO-DEP should be detected first")
assertNotNull(nfcA, "NFC-A should also be present")
}
// ==========================================
// TEST GROUP 2: Slot State Detection
// ==========================================
/**
* TEST 2.1: Verify device not full detection
*
* When: Device has active=true, backup=false
* Then: Should allow write operation
*/
@Test
fun testSlotStateAllowsWriteWhenNotFull() {
// Arrange: Create mock state - device not full
val slotState = NfcASlotState(
count = 2,
activePresent = true,
backupPresent = false
)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(false, isFull, "Device should not be full")
}
/**
* TEST 2.2: Verify device full detection
*
* When: Device has active=true, backup=true
* Then: Should block write operation
*/
@Test
fun testSlotStateBlocksWriteWhenFull() {
// Arrange: Create mock state - device full
val slotState = NfcASlotState(
count = 2,
activePresent = true,
backupPresent = true
)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(true, isFull, "Device should be full")
}
/**
* TEST 2.3: Verify count-based full detection
*
* When: Device reports count >= 2
* Then: Should be considered full
*/
@Test
fun testSlotStateFullByCount() {
// Arrange
val slotStateFull = NfcASlotState(count = 2, activePresent = null, backupPresent = null)
val slotStateNotFull = NfcASlotState(count = 1, activePresent = null, backupPresent = null)
// Act
val isFullCount2 = slotStateFull.isFull()
val isFullCount1 = slotStateNotFull.isFull()
// Assert
assertEquals(true, isFullCount2, "Count >= 2 should be full")
assertEquals(false, isFullCount1, "Count < 2 should not be full")
}
/**
* TEST 2.4: Verify fallback when introspection fails
*
* When: Cannot introspect slot state (all null)
* Then: Should default to non-blocking path
*/
@Test
fun testSlotStateFallbackWhenUnknown() {
// Arrange: No slot info available
val slotState = NfcASlotState(count = null, activePresent = null, backupPresent = null)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(false, isFull, "Unknown state should not block (default to false)")
}
// ==========================================
// TEST GROUP 3: Character Conversion
// ==========================================
/**
* TEST 3.1: Verify NFC character to DB character conversion
*
* When: VBNfcCharacter is read from toy
* Then: Should convert to database format correctly
*/
@Test
fun testNfcCharacterConversionVB() {
// Arrange
val vbNfcCharacter = VBNfcCharacter()
// In real test, would populate with actual data
// Act
// This would call FromNfcConverter.addCharacter()
// For now, just verify the object exists
assertNotNull(vbNfcCharacter, "VBNfcCharacter should be created")
}
// ==========================================
// TEST GROUP 4: Write Operation Validation
// ==========================================
/**
* TEST 4.1: Verify write is blocked when device full
*
* Scenario:
* - User selects character to write
* - Device has 2 active+backup (full)
* - Click "VBH to Watch"
*
* Expected:
* - WriteResult.BLOCKED_DEVICE_FULL
* - No transfer occurs
*/
@Test
fun testWriteBlockedWhenDeviceFull() {
// Arrange
val slotState = NfcASlotState(
count = 2,
activePresent = true,
backupPresent = true
)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(true, isFull, "Should block when full")
// In real test, would verify WriteResult.BLOCKED_DEVICE_FULL returned
}
/**
* TEST 4.2: Verify write allowed when slot available
*
* Scenario:
* - User selects character to write
* - Device has active only (backup empty)
* - Click "VBH to Watch"
*
* Expected:
* - WriteResult.MOVE_CONFIRMED
* - Active→Backup migration performed
* - Character transferred
*/
@Test
fun testWriteAllowedWhenSlotAvailable() {
// Arrange
val slotState = NfcASlotState(
count = 2,
activePresent = true,
backupPresent = false
)
// Act
val isFull = slotState.isFull()
// Assert
assertEquals(false, isFull, "Should allow when slot available")
}
/**
* TEST 4.3: Verify write rejected when no active character
*
* Scenario:
* - Device only has backup (no active)
* - Click "VBH to Watch"
*
* Expected:
* - WriteResult.COPIED
* - Transfer skipped
*/
@Test
fun testWriteSkippedWhenNoActiveCharacter() {
// Arrange: Only backup, no active
val slotState = NfcASlotState(
count = 1,
activePresent = false,
backupPresent = true
)
// Act
val isValidForWrite = slotState.activePresent == true && slotState.backupPresent == false
// Assert
assertEquals(false, isValidForWrite, "Should reject write when no active")
}
// ==========================================
// TEST GROUP 5: Debouncing
// ==========================================
/**
* TEST 5.1: Verify rapid re-taps are debounced
*
* Scenario:
* - User taps toy for read
* - User immediately taps again (< 1.5 sec)
*
* Expected:
* - Only first tap processes
* - Second tap ignored
* - Database contains only 1 character
*/
@Test
fun testRapidTapDebouncing() {
// Arrange
val TAG_DEBOUNCE_MS = 1500L
val now = System.currentTimeMillis()
val firstTapTime = now
val secondTapTime = now + 500 // 500ms later (within debounce)
// Act
val timeDifference = secondTapTime - firstTapTime
val isWithinDebounce = timeDifference < TAG_DEBOUNCE_MS
// Assert
assertEquals(true, isWithinDebounce, "Second tap should be debounced")
}
/**
* TEST 5.2: Verify tap after debounce window is processed
*
* Scenario:
* - User taps toy for read
* - User taps again after 2 seconds (> 1.5 sec)
*
* Expected:
* - First tap processes
* - Second tap also processes
* - Database contains 2 characters
*/
@Test
fun testTapAllowedAfterDebounceWindow() {
// Arrange
val TAG_DEBOUNCE_MS = 1500L
val now = System.currentTimeMillis()
val firstTapTime = now
val secondTapTime = now + 2000 // 2 seconds later (outside debounce)
// Act
val timeDifference = secondTapTime - firstTapTime
val isWithinDebounce = timeDifference < TAG_DEBOUNCE_MS
// Assert
assertEquals(false, isWithinDebounce, "Second tap should be processed")
}
// ==========================================
// TEST GROUP 6: Error Handling
// ==========================================
/**
* TEST 6.1: Verify graceful handling of corrupt character data
*
* Scenario:
* - Toy has corrupted character data
* - User clicks "Watch to VBH"
* - Tap toy
*
* Expected:
* - Exception caught
* - Toast shown: "Whoops"
* - App doesn't crash
* - User can retry
*/
@Test
fun testCorruptCharacterHandling() {
// This is more of an integration test
// Would require mocking TagCommunicator.receiveCharacter() to throw
// Pseudocode:
// whenever(mockTagCommunicator.receiveCharacter())
// .thenThrow(IOException("Corrupt data"))
// In real test:
// Call onClickRead()
// Verify: Toast shown, no crash, app recovers
}
/**
* TEST 6.2: Verify NFC connection timeout handling
*
* Scenario:
* - User starts read/write
* - Remove toy during transfer
*
* Expected:
* - Connection timeout
* - Toast: "Whoops"
* - Reader mode disabled
* - App recovers
*/
@Test
fun testNfcConnectionTimeoutHandling() {
// Pseudocode:
// whenever(mockNfcA.connect())
// .thenThrow(IOException("NFC connection lost"))
// In real test:
// Verify timeout is caught, UI updates, no crash
}
// ==========================================
// Helper Classes (from ScanScreenControllerImpl)
// ==========================================
/**
* Mirrors the NfcASlotState data class from ScanScreenControllerImpl.kt
*/
private data class NfcASlotState(
val count: Int?,
val activePresent: Boolean?,
val backupPresent: Boolean?,
) {
fun isFull(): Boolean {
if (count != null) {
return count >= 2
}
if (activePresent != null && backupPresent != null) {
return activePresent && backupPresent
}
return false
}
}
}
@@ -49,7 +49,8 @@ class MainActivity : ComponentActivity() {
application.container.dataStoreSecretsRepository.secretsFlow,
this,
this::registerActivityLifecycleListener,
this::unregisterActivityLifecycleListener
this::unregisterActivityLifecycleListener,
application.container.db,
)
val settingsScreenController = SettingsScreenControllerImpl(this)
val itemsScreenController = ItemsScreenControllerImpl(this)
@@ -13,6 +13,9 @@ interface CardDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertNewCard(card: Card): Long
@Query("SELECT * FROM Card")
fun getAllCards(): List<Card>
@Query("SELECT * FROM Card WHERE cardId = :id")
fun getCardByCardId(id: Int): List<Card>
@@ -131,6 +131,9 @@ interface UserCharacterDao {
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
fun getVbData(id: Long): Flow<VBCharacterData>
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
suspend fun getVbDataOrNull(id: Long): VBCharacterData?
@Query("SELECT * FROM SpecialMissions WHERE characterId = :id")
fun getSpecialMissions(id: Long): Flow<List<SpecialMissions>>
@@ -2,6 +2,8 @@ package com.github.nacabaro.vbhelper.database
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.nacabaro.vbhelper.daos.AdventureDao
import com.github.nacabaro.vbhelper.daos.CardAdventureDao
import com.github.nacabaro.vbhelper.daos.CharacterDao
@@ -13,6 +15,10 @@ import com.github.nacabaro.vbhelper.daos.ItemDao
import com.github.nacabaro.vbhelper.daos.SpecialMissionDao
import com.github.nacabaro.vbhelper.daos.SpriteDao
import com.github.nacabaro.vbhelper.daos.UserCharacterDao
import com.github.nacabaro.vbhelper.daos.VitalWearSettingsDao
import com.github.nacabaro.vbhelper.daos.CharacterTransferPolicyDao
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardDao
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardEntity
import com.github.nacabaro.vbhelper.domain.card.Background
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.card.Card
@@ -29,10 +35,12 @@ import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.domain.device_data.VitalsHistory
import com.github.nacabaro.vbhelper.domain.device_data.VitalWearCharacterSettings
import com.github.nacabaro.vbhelper.domain.device_data.CharacterTransferPolicy
import com.github.nacabaro.vbhelper.domain.items.Items
@Database(
version = 1,
version = 2,
entities = [
Card::class,
CardProgress::class,
@@ -50,7 +58,10 @@ import com.github.nacabaro.vbhelper.domain.items.Items
Items::class,
Adventure::class,
Background::class,
PossibleTransformations::class
PossibleTransformations::class,
ValidatedCardEntity::class,
VitalWearCharacterSettings::class,
CharacterTransferPolicy::class
]
)
abstract class AppDatabase : RoomDatabase() {
@@ -65,4 +76,35 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun specialMissionDao(): SpecialMissionDao
abstract fun cardAdventureDao(): CardAdventureDao
abstract fun cardFusionsDao(): CardFusionsDao
abstract fun validatedCardDao(): ValidatedCardDao
abstract fun vitalWearSettingsDao(): VitalWearSettingsDao
abstract fun characterTransferPolicyDao(): CharacterTransferPolicyDao
companion object {
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// Create new tables
db.execSQL("CREATE TABLE IF NOT EXISTS `Items` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `itemIcon` INTEGER NOT NULL, `itemLength` INTEGER NOT NULL, `price` INTEGER NOT NULL, `quantity` INTEGER NOT NULL, `itemType` TEXT NOT NULL, PRIMARY KEY(`id`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `SpecialMissions` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `characterId` INTEGER NOT NULL, `goal` INTEGER NOT NULL, `watchId` INTEGER NOT NULL, `progress` INTEGER NOT NULL, `status` TEXT NOT NULL, `timeElapsedInMinutes` INTEGER NOT NULL, `timeLimitInMinutes` INTEGER NOT NULL, `missionType` TEXT NOT NULL, FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `VBCharacterData` (`id` INTEGER NOT NULL, `generation` INTEGER NOT NULL, `totalTrophies` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `TransformationHistory` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monId` INTEGER NOT NULL, `stageId` INTEGER NOT NULL, `transformationDate` INTEGER NOT NULL, FOREIGN KEY(`monId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stageId`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `VitalsHistory` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `charId` INTEGER NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `vitalPoints` INTEGER NOT NULL, FOREIGN KEY(`charId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `Dex` (`id` INTEGER NOT NULL, `discoveredOn` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `Adventure` (`characterId` INTEGER NOT NULL, `originalDuration` INTEGER NOT NULL, `finishesAdventure` INTEGER NOT NULL, PRIMARY KEY(`characterId`), FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `Background` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cardId` INTEGER NOT NULL, `background` BLOB NOT NULL, `backgroundWidth` INTEGER NOT NULL, `backgroundHeight` INTEGER NOT NULL, FOREIGN KEY(`cardId`) REFERENCES `Card`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `PossibleTransformations` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `charaId` INTEGER NOT NULL, `requiredVitals` INTEGER NOT NULL, `requiredTrophies` INTEGER NOT NULL, `requiredBattles` INTEGER NOT NULL, `requiredWinRate` INTEGER NOT NULL, `changeTimerHours` INTEGER NOT NULL, `requiredAdventureLevelCompleted` INTEGER NOT NULL, `toCharaId` INTEGER, FOREIGN KEY(`charaId`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`toCharaId`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `validated` (`cardId` INTEGER NOT NULL, PRIMARY KEY(`cardId`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `VitalWearCharacterSettings` (`characterId` INTEGER NOT NULL, `trainingInBackground` INTEGER NOT NULL, `allowedBattles` INTEGER NOT NULL, `accumulatedDailyInjuries` INTEGER NOT NULL, PRIMARY KEY(`characterId`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `CharacterTransferPolicy` (`characterId` INTEGER NOT NULL, `nativeDeviceType` TEXT NOT NULL, `preferredHceExportFormat` TEXT NOT NULL, `preferredNfcaExportFormat` TEXT NOT NULL, `lastObservedImportFormat` TEXT, `lastTransferTransport` TEXT, `lastTransferTarget` TEXT, `preserveVbRoundTrip` INTEGER NOT NULL, `preserveBeRoundTrip` INTEGER NOT NULL, PRIMARY KEY(`characterId`), FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
// Add columns to existing tables
try {
db.execSQL("ALTER TABLE `UserCharacter` ADD COLUMN `characterType` TEXT NOT NULL DEFAULT 'BEDevice'")
} catch (e: Exception) {}
try {
db.execSQL("ALTER TABLE `Card` ADD COLUMN `isBEm` INTEGER NOT NULL DEFAULT 0")
} catch (e: Exception) {}
}
}
}
}
@@ -3,9 +3,13 @@ package com.github.nacabaro.vbhelper.di
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.source.CurrencyRepository
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
interface AppContainer {
val db: AppDatabase
val dataStoreSecretsRepository: DataStoreSecretsRepository
val currencyRepository: CurrencyRepository
val validatedCardManager: ValidatedCardManager
val companionLogService: CompanionLogService
}
@@ -8,6 +8,8 @@ import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.AppContainer
import com.github.nacabaro.vbhelper.source.CurrencyRepository
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
import com.github.nacabaro.vbhelper.source.SecretsSerializer
import com.github.nacabaro.vbhelper.source.proto.Secrets
@@ -30,6 +32,7 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
klass = AppDatabase::class.java,
"internalDb"
)
.addMigrations(AppDatabase.MIGRATION_1_2)
.createFromAsset("items.db")
.build()
}
@@ -37,5 +40,11 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
override val dataStoreSecretsRepository = DataStoreSecretsRepository(context.secretsStore)
override val currencyRepository = CurrencyRepository(context.currencyStore)
override val validatedCardManager by lazy {
ValidatedCardManager(db.validatedCardDao())
}
override val companionLogService = CompanionLogService()
}
@@ -2,10 +2,18 @@ package com.github.nacabaro.vbhelper.di
import DefaultAppContainer
import android.app.Application
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
class VBHelper : Application() {
lateinit var container: DefaultAppContainer
val validatedCardManager: ValidatedCardManager
get() = container.validatedCardManager
val companionLogService: CompanionLogService
get() = container.companionLogService
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer(applicationContext)
@@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.screens.scanScreen
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.nfc.tech.NfcA
import android.os.Bundle
import android.provider.Settings
@@ -15,12 +16,16 @@ import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.screens.scanScreen.converters.FromNfcConverter
import com.github.nacabaro.vbhelper.screens.scanScreen.converters.ToNfcConverter
import com.github.nacabaro.vbhelper.source.VitalWearCharacterExporter
import com.github.nacabaro.vbhelper.source.VitalWearCharacterImporter
import com.github.nacabaro.vbhelper.source.getCryptographicTransformerMap
import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
import com.github.nacabaro.vbhelper.transfer.hce.VitalWearHceReaderClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.stateIn
@@ -32,8 +37,10 @@ class ScanScreenControllerImpl(
private val componentActivity: ComponentActivity,
private val registerActivityLifecycleListener: (String, ActivityLifecycleListener)->Unit,
private val unregisterActivityLifecycleListener: (String)->Unit,
private val database: AppDatabase,
): ScanScreenController {
private var lastScannedCharacter: NfcCharacter? = null
private var pendingExportCharacterId: Long? = null
private val nfcAdapter: NfcAdapter
init {
@@ -46,7 +53,9 @@ class ScanScreenControllerImpl(
}
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit, onMultipleCards: (List<Card>) -> Unit) {
handleTag(secrets) { tagCommunicator ->
handleTag(
secrets,
handlerFunc = { tagCommunicator ->
try {
val character = tagCommunicator.receiveCharacter()
val resultMessage = characterFromNfc(character) { cards, nfcCharacter ->
@@ -63,7 +72,27 @@ class ScanScreenControllerImpl(
onComplete.invoke()
componentActivity.getString(R.string.scan_error_generic)
}
},
hceHandler = { isoDep ->
try {
val client = VitalWearHceReaderClient(isoDep)
val character = client.receiveCharacterFromWatch()
val importer = VitalWearCharacterImporter(database)
val result = importer.importCharacter(character)
Log.i("NFC_READ", "VitalWear HCE import: ${result.message}")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, result.message, Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Log.e("NFC_READ", "Error reading character from VitalWear HCE", e)
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic) + ": " + (e.message ?: e.javaClass.simpleName), Toast.LENGTH_LONG).show()
}
} finally {
onComplete()
}
}
)
}
override fun cancelRead() {
@@ -83,29 +112,45 @@ class ScanScreenControllerImpl(
unregisterActivityLifecycleListener.invoke(key)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun handleTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String) {
private fun handleTag(
secrets: Secrets,
handlerFunc: (TagCommunicator) -> String,
hceHandler: ((IsoDep) -> Unit)? = null,
) {
if (!nfcAdapter.isEnabled) {
showWirelessSettings()
} else {
val options = Bundle()
// Work around for some broken Nfc firmware implementations that poll the card too fast
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
nfcAdapter.enableReaderMode(componentActivity, buildOnReadTag(secrets, handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
nfcAdapter.enableReaderMode(
componentActivity,
buildOnReadTag(secrets, handlerFunc, hceHandler),
NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
options
)
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun buildOnReadTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
private fun buildOnReadTag(
secrets: Secrets,
handlerFunc: (TagCommunicator) -> String,
hceHandler: ((IsoDep) -> Unit)? = null,
): (Tag) -> Unit {
return { tag ->
val isoDep = IsoDep.get(tag)
if (isoDep != null && hceHandler != null) {
// VitalWear watch via HCE / ISO-DEP
isoDep.connect()
isoDep.use { hceHandler(isoDep) }
} else {
// Physical Vital Bracelet via raw NFC-A
val nfcData = NfcA.get(tag)
if (nfcData == null) {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_tag_not_vb), Toast.LENGTH_SHORT).show()
}
}
} else {
nfcData.connect()
nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
@@ -116,6 +161,8 @@ class ScanScreenControllerImpl(
}
}
}
}
}
private fun checkSecrets() {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
@@ -132,16 +179,16 @@ class ScanScreenControllerImpl(
nfcCharacter: NfcCharacter,
onComplete: () -> Unit
) {
handleTag(secrets) { tagCommunicator ->
handleTag(
secrets,
handlerFunc = { tagCommunicator ->
try {
if (nfcCharacter is VBNfcCharacter) {
Log.d("SendCharacter", "VBNfcCharacter")
val castNfcCharacter: VBNfcCharacter = nfcCharacter
tagCommunicator.sendCharacter(castNfcCharacter)
tagCommunicator.sendCharacter(nfcCharacter)
} else if (nfcCharacter is BENfcCharacter) {
Log.d("SendCharacter", "BENfcCharacter")
val castNfcCharacter: BENfcCharacter = nfcCharacter
tagCommunicator.sendCharacter(castNfcCharacter)
tagCommunicator.sendCharacter(nfcCharacter)
}
onComplete.invoke()
componentActivity.getString(R.string.scan_sent_character_success)
@@ -149,7 +196,12 @@ class ScanScreenControllerImpl(
Log.e("TAG", e.stackTraceToString())
componentActivity.getString(R.string.scan_error_generic)
}
},
hceHandler = { _ ->
// Character was already sent in onClickCheckCard's HCE handler.
onComplete()
}
)
}
override fun onClickCheckCard(
@@ -157,11 +209,42 @@ class ScanScreenControllerImpl(
nfcCharacter: NfcCharacter,
onComplete: () -> Unit
) {
handleTag(secrets) { tagCommunicator ->
handleTag(
secrets,
handlerFunc = { tagCommunicator ->
tagCommunicator.prepareDIMForCharacter(nfcCharacter.dimId)
onComplete.invoke()
componentActivity.getString(R.string.scan_sent_dim_success)
},
hceHandler = { isoDep ->
// VitalWear watch: send the character proto directly via HCE.
val characterId = pendingExportCharacterId
if (characterId == null) {
Log.e("NFC_WRITE", "No pending character to send to VitalWear")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic), Toast.LENGTH_SHORT).show()
}
onComplete()
return@handleTag
}
try {
val proto = VitalWearCharacterExporter(componentActivity, database).buildCharacterProto(characterId)
val client = VitalWearHceReaderClient(isoDep)
client.sendCharacterToWatch(proto)
Log.i("NFC_WRITE", "Character sent to VitalWear via HCE")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_sent_character_success), Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Log.e("NFC_WRITE", "Error sending character to VitalWear HCE", e)
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic) + ": " + (e.message ?: e.javaClass.simpleName), Toast.LENGTH_LONG).show()
}
} finally {
onComplete()
}
}
)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
@@ -181,10 +264,8 @@ class ScanScreenControllerImpl(
}
override suspend fun characterToNfc(characterId: Long): NfcCharacter {
val nfcGenerator = ToNfcConverter(
componentActivity = componentActivity
)
pendingExportCharacterId = characterId
val nfcGenerator = ToNfcConverter(componentActivity = componentActivity)
val character = nfcGenerator.characterToNfc(characterId)
Log.d("CharacterType", character.toString())
return character
@@ -5,6 +5,7 @@ import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* Utility for converting character device types.
@@ -31,7 +32,7 @@ class DeviceTypeConverter(private val database: AppDatabase) {
true
} catch (e: Exception) {
// Log but don't crash - character can still be sent as-is
android.util.Log.e("DeviceTypeConverter", "Failed to convert character $characterId to VB type", e)
Timber.e(e, "Failed to convert character $characterId to VB type")
false
}
}
@@ -42,7 +43,7 @@ class DeviceTypeConverter(private val database: AppDatabase) {
val vbCharacterData = VBCharacterData(
id = characterId,
generation = 0,
totalTrophies = 0
totalTrophies = 0,
)
database.userCharacterDao().insertVBCharacterData(vbCharacterData)
}
@@ -15,14 +15,13 @@ class VitalWearCharacterExporter(
private val context: Context,
private val database: AppDatabase
) {
fun buildShareIntent(characterId: Long): Intent {
return runBlocking {
fun buildCharacterProto(characterId: Long): Character = runBlocking {
val characterWithSprites = database.userCharacterDao().getCharacterWithSprites(characterId)
val userCharacter = database.userCharacterDao().getCharacter(characterId)
val card = database.cardDao().getCardByCharacterIdSync(characterId)
?: error("Card not found for character $characterId")
val cardProgress = database.cardProgressDao().getCardProgressSync(card.id) ?: 0
val proto = Character.newBuilder()
Character.newBuilder()
.setCardId(card.cardId)
.setCardName(card.name)
.setCharacterStats(
@@ -61,6 +60,11 @@ class VitalWearCharacterExporter(
}
)
.build()
}
fun buildShareIntent(characterId: Long): Intent {
return runBlocking {
val proto = buildCharacterProto(characterId)
val exportDir = File(context.cacheDir, "exports").apply { mkdirs() }
val exportFile = File(exportDir, "vbhelper_character_$characterId.vitalwear")
@@ -23,6 +23,13 @@ message Character {
int32 total_wins = 13;
int32 current_phase_wins = 14;
int32 mood = 15;
enum TransferDeviceType {
TRANSFER_DEVICE_TYPE_UNSPECIFIED = 0;
TRANSFER_DEVICE_TYPE_VB = 1;
TRANSFER_DEVICE_TYPE_BE = 2;
}
TransferDeviceType transfer_device_type = 16;
}
CharacterStats character_stats = 3;
+44 -32
View File
@@ -17,21 +17,21 @@
<string name="nav_adventure">アドベンチャー</string>
<string name="nav_credits">クレジット</string>
<string name="adventure_title">アドベンチャー</string>
<string name="adventure_empty_state">何もありません。</string>
<string name="adventure_empty_state">No Data</string>
<string name="home_title">VB Helper</string>
<string name="scan_secrets_not_imported">"APKの読み込みがされていません。設定から読み込をして下さい。 "</string>
<string name="scan_title">デバイスをスキャン</string>
<string name="scan_sent_dim_success">DIMの転送に成功しました。</string>
<string name="scan_nfc_must_be_enabled">NFC機能を有効にして下さい。</string>
<string name="settings_title">設定</string>
<string name="scan_missing_secrets">必要なデータが不足しています。設定を開き、Vital ArenaのAPKを読み込んでください。</string>
<string name="scan_missing_secrets">Vital ArenaのAPKを読み込みして下さい。</string>
<string name="scan_sent_character_success">転送が成功しました。</string>
<string name="scan_error_generic">エラー</string>
<string name="settings_about_desc">このアプリについて</string>
<string name="settings_export_data_title">データベースの書き出し</string>
<string name="settings_export_data_desc">" データベースを書き出します。"</string>
<string name="settings_import_data_title">データベースの読み込み</string>
<string name="settings_import_data_desc">" データベースを読み込みます。"</string>
<string name="settings_export_data_title">データベースのエクスポート</string>
<string name="settings_export_data_desc">データベースの書き出しをします。</string>
<string name="settings_import_data_title">データベースのインポート</string>
<string name="settings_import_data_desc">データベースの読み込みをします。</string>
<string name="credits_title">クレジット</string>
<string name="battles_coming_soon">Coming soon</string>
<string name="cards_my_cards_title">カードリスト</string>
@@ -42,46 +42,45 @@
<string name="scan_vb_to_app">デバイスからアプリへ</string>
<string name="scan_app_to_vb">アプリからデバイスへ</string>
<string name="read_character_title">キャラクターを読み込みます。</string>
<string name="read_character_prepare_device">バイタルブレスを準備してください。</string>
<string name="read_character_prepare_device">デバイスを準備してください。</string>
<string name="read_character_go_to_connect">準備が出来たら転送を押してください。</string>
<string name="read_character_confirm">転送</string>
<string name="card_adventure_missions_title">アドベンチャーミッション</string>
<string name="card_entry_characters_obtained">%1$d / %2$d のキャラクターを取得しました。</string>
<string name="card_entry_edit">編集</string>
<string name="card_entry_edit">編集</string>
<string name="card_entry_edit">編集</string>
<string name="card_entry_edit">編集</string>
<string name="card_entry_delete">削除</string>
<string name="home_vbdim_current_phase_win">フェーズ勝率 %</string>
<string name="home_vbdim_special_missions">スペシャルミッション</string>
<string name="storage_delete_character">削除</string>
<string name="storage_close">閉じる</string>
<string name="storage_character_image_description">キャラクター画像</string>
<string name="storage_send_to_watch">転送</string>
<string name="storage_send_to_watch">デバイスへ転送</string>
<string name="storage_send_to_vitalwear">VitalWearへ転送</string>
<string name="storage_send_to_vitalwear">VitalWearへ転送</string>
<string name="storage_set_active">選択</string>
<string name="storage_send_on_adventure">アドベンチャーへ出発</string>
<string name="scan_no_nfc_on_device">NFC機能が見つかりません。</string>
<string name="scan_tag_not_vb">検出されたタグはバイタルブレスでは使用出来ません。</string>
<string name="settings_section_nfc">NFC</string>
<string name="settings_section_dim_bem">DiM / BEm</string>
<string name="settings_section_nfc">NFC通信</string>
<string name="settings_section_dim_bem">DiM / BEm管理</string>
<string name="settings_section_data">データ管理</string>
<string name="settings_import_apk_title">アプリの読み込み</string>
<string name="settings_import_apk_desc">" Vital Arena 2.1.0のAPKファイルを読み込みします。"</string>
<string name="settings_import_card_title">カードの読み込み</string>
<string name="settings_import_apk_title">アプリのインポート</string>
<string name="settings_import_apk_desc">Vital Arena 2.1.0のAPKファイルを読み込みします。</string>
<string name="settings_import_card_title">カードのインポート</string>
<string name="settings_section_about">概要 / クレジット</string>
<string name="dex_chara_fusions_button">ジョグレス</string>
<string name="dex_chara_close_button">閉じる</string>
<string name="settings_import_card_desc">" Dim / BEm カードファイルを読み込みます。"</string>
<string name="settings_import_card_desc">Dim / BEm のデータファイルを読み込みします。</string>
<string name="settings_credits_title">クレジット</string>
<string name="settings_credits_desc">開発者について</string>
<string name="settings_about_title">概要</string>
<string name="beta_warning_message_main">" このアプリは現在アルファ版です。今後のアップデートにより保存されているデータの全てが削除される可能性がある為、大切なデータの保存はしないようにお願い致します。ご不便をおかけして申し訳ございません。 "</string>
<string name="beta_warning_message_main">このアプリは現在開発中です。\n今後のアップデートにより、保存されているデータ全ての削除が必要になる場合があります。\nその為、大切なデータの保存には使用しないようお願い致します。\nご不便をおかけして申し訳ございません。</string>
<string name="beta_warning_message_compatibility">"このアプリはオリジナルのVBおよびVHで動作するようになりました。 "</string>
<string name="beta_warning_message_thanks">" ご理解とご協力に感謝いたします。\n 開発チーム一同"</string>
<string name="beta_warning_message_thanks">ご理解とご協力に感謝致します。\n開発チーム一同</string>
<string name="home_vbdim_vitals">バイタル</string>
<string name="home_adventure_mission_finished">"1人のキャラクターがアドベンチャーミッションを完了しました。 "</string>
<string name="scan_secrets_not_initialized">"初期化が出来ませんでした。もう一度試して下さい。 "</string>
<string name="home_special_mission_delete_main">現在の進捗状況が失われますが、本当にこのスペシャルミッションを削除しますか?</string>
<string name="home_special_mission_delete_main">現在の進捗状況が失われます。本当にこのミッションを削除しますか?</string>
<string name="home_special_mission_delete_dismiss">閉じる</string>
<string name="home_special_mission_delete_remove">削除</string>
<string name="item_dialog_use">アイテム使用</string>
@@ -92,27 +91,26 @@
<string name="battles_online_title">オンラインバトル</string>
<string name="obtained_item_you_also_got_credits">%1$d クレジットを獲得しました。</string>
<string name="obtained_item_dismiss">閉じる</string>
<string name="obtained_item_dismiss">閉じる</string>
<string name="nav_dex">図鑑</string>
<string name="action_place_near_reader">バイタルブレス本体を近くに置いて下さい。</string>
<string name="credits_RedEyez_description">献身的なデバッグ作業を行って頂きました。</string>
<string name="action_place_near_reader">デバイスを近づけて下さい。</string>
<string name="action_cancel">キャンセル</string>
<string name="card_view_discovered_characters">解放済みキャラクター</string>
<string name="dex_chara_icon_description">キャラクターアイコン</string>
<string name="dex_chara_name_icon_description">名前</string>
<string name="credits_section_reverse_engineering">リバースエンジニアリング</string>
<string name="credits_section_reverse_engineering">リバースエンジニアリング</string>
<string name="credits_section_reverse_engineering">プログラム解析</string>
<string name="credits_section_app_development">アプリケーション開発</string>
<string name="credits_cyanic_description">" ファームウェアの解析を行い、開発をサポートしてくれました。"</string>
<string name="credits_cfogrady_description">" vb-lib-nfc開発およびアプリの開発に協力して頂きました。"</string>
<string name="credits_nacabaro_description">" アプリの開発をしました。"</string>
<string name="credits_lightheel_description">" アプリのサーバー含む対戦機能を開発して頂きました。現在も開発を継続して頂いています。"</string>
<string name="credits_shvstrz_description">" アプリのアイコンデザインをして頂きました。"</string>
<string name="credits_cyanic_description">ファームウェアの解析を行い、開発をサポートしてくれました。</string>
<string name="credits_cfogrady_description">vb-lib-nfc開発および本アプリの開発に協力して頂きました。</string>
<string name="credits_nacabaro_description">本アプリの開発をしました。</string>
<string name="credits_lightheel_description">本アプリのサーバー含む対戦機能を開発して頂きました。現在も開発を継続して頂いています。</string>
<string name="credits_shvstrz_description">本アプリのアイコンデザインをして頂きました。</string>
<string name="write_card_title">カード情報書き込み中</string>
<string name="write_card_icon_description">カードアイコン</string>
<string name="write_card_device_ready">デバイスの準備をして下さい。</string>
<string name="write_card_required_card">%1$s カードが必要です。</string>
<string name="write_card_confirm">転送</string>
<string name="write_character_success">" カードの読み込みが完了しました。"</string>
<string name="write_character_success">カードの読み込みが完了しました。</string>
<string name="write_character_wait_ready">" デバイスの準備をしてから確認を押して下さい。"</string>
<string name="write_character_confirm">確認</string>
<string name="home_vbdim_trophies">トロフィー</string>
@@ -124,10 +122,10 @@
<string name="dex_chara_stats">HP: %1$d, BP: %2$d, AP: %3$d</string>
<string name="dex_chara_stage_attribute_unknown">Stg: -, Atr: -</string>
<string name="dex_chara_stats_unknown">HP: -, BP: -, AP: -</string>
<string name="dex_chara_requirements">" Tr: %1$d; Bt: %2$d; Vr: %3$d; Wr: %4$d%%; Ct: %5$dh "</string>
<string name="dex_chara_requirements">\n Tr: %1$d; Bt: %2$d; Vr: %3$d; Wr: %4$d%%; Ct: %5$dh</string>
<string name="dex_chara_adventure_level">AdvLvl %1$d</string>
<string name="storage_my_characters_title">キャラクターリスト</string>
<string name="storage_nothing_to_see_here">表示するデータはありません。</string>
<string name="storage_nothing_to_see_here">No Data</string>
<string name="storage_in_adventure_toast">このキャラクターは冒険中です。</string>
<string name="home_vbdim_next_timer">次のタイマー</string>
<string name="home_vbdim_total_battle_win">総勝率 %</string>
@@ -148,4 +146,18 @@
<string name="special_mission_steps_progress">歩数 %1$d 歩</string>
<string name="special_mission_battles_progress">バトル %1$d 回</string>
<string name="special_mission_wins_progress">勝利 %1$d 回</string>
<string name="settings_companion_import_card_image_title">カードのインポート</string>
<string name="settings_companion_import_card_image_title">カードのインポート</string>
<string name="companion_validation_connect_vb">デバイスに接続</string>
<string name="companion_card_import_success">カードのインポートに成功しました。</string>
<string name="companion_firmware_no_watch">デバイスが見つかりません。</string>
<string name="companion_validation_insert_card">デバイスへカードを差し込んで下さい。</string>
<string name="companion_validation_success">データチェックが完了しました。</string>
<string name="companion_loading_select_firmware">ファームウェアファイルを選択</string>
<string name="companion_firmware_sent_success">ファームウェアの送信が完了しました。</string>
<string name="companion_logs_receive_failed">ログの受信に失敗しました。</string>
<string name="companion_logs_failed_request">ログの要求に失敗しました。</string>
<string name="companion_logs_warning">注意:ログの転送には時間がかかる場合があります。</string>
<string name="settings_companion_send_watch_logs_title">ログの送信</string>
<string name="companion_transfer_disabled_message">現在、転送は無効になっています。</string>
</resources>
+13
View File
@@ -219,4 +219,17 @@
<string name="home_special_mission_delete_dismiss">Dismiss</string>
<string name="home_special_mission_delete_remove">Remove</string>
<string name="settings_companion_import_card_image_title">Import Card Image</string>
<string name="companion_validation_connect_vb">Connect Vital Bracelet</string>
<string name="companion_card_import_success">Card imported successfully!</string>
<string name="companion_firmware_no_watch">No watch found!</string>
<string name="companion_validation_insert_card">Insert card into Vital Bracelet</string>
<string name="companion_validation_success">Validation successful!</string>
<string name="companion_loading_select_firmware">Select firmware file</string>
<string name="companion_firmware_sent_success">Firmware sent successfully!</string>
<string name="companion_logs_receive_failed">Failed to receive logs</string>
<string name="companion_logs_failed_request">Failed to request logs</string>
<string name="companion_logs_warning">Warning: Log transfer might take a while</string>
<string name="settings_companion_send_watch_logs_title">Send Watch Logs</string>
<string name="companion_transfer_disabled_message">Transfer is currently disabled</string>
</resources>
+2 -6
View File
@@ -3,10 +3,6 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
buildscript {
dependencies {
classpath(libs.protobuf.gradle.plugin)
}
alias(libs.plugins.vksp) apply false
alias(libs.plugins.vprotobuf) apply false
}
+10
View File
@@ -21,3 +21,13 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false
+20 -1
View File
@@ -1,5 +1,5 @@
[versions]
agp = "8.13.2"
agp = "9.2.1"
datastore = "1.2.0"
datastorePreferences = "1.2.0"
kotlin = "2.3.0"
@@ -17,6 +17,14 @@ protobufJavalite = "4.33.4"
roomRuntime = "2.8.4"
vbNfcReader = "0.2.0-SNAPSHOT"
dimReader = "2.1.0"
playServicesWearable = "20.0.1"
kotlinxCoroutinesPlayServices = "1.11.0"
timber = "5.0.1"
tinylog = "2.7.0"
retrofit = "2.9.0"
gson = "2.10.1"
okhttp = "4.11.0"
ksp = "2.3.2"
[libraries]
androidx-compose-material = { module = "androidx.compose.material:material" }
@@ -46,9 +54,20 @@ protobuf-gradle-plugin = { module = "com.google.protobuf:protobuf-gradle-plugin"
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufJavalite" }
vb-nfc-reader = { module = "com.github.cfogrady:vb-nfc-reader", version.ref = "vbNfcReader" }
dim-reader = { module = "com.github.cfogrady:vb-dim-reader", version.ref = "dimReader" }
play-services-wearable = { group = "com.google.android.gms", name = "play-services-wearable", version.ref = "playServicesWearable" }
kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesPlayServices" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tinylog-api = { module = "org.tinylog:tinylog-api", version.ref = "tinylog" }
tinylog-impl = { module = "org.tinylog:tinylog-impl", version.ref = "tinylog" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
vksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
vprotobuf = { id = "com.google.protobuf", version.ref = "protobufGradlePlugin" }
+1 -1
View File
@@ -1,6 +1,6 @@
#Tue Dec 24 10:55:57 UTC 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+1
View File
@@ -17,6 +17,7 @@ dependencyResolutionManagement {
mavenLocal()
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}