mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-07-30 00:31:54 +00:00
Overwrite with local version
This commit is contained in:
parent
895b704c8d
commit
f829efae68
@ -5,6 +5,9 @@
|
|||||||
<permission
|
<permission
|
||||||
android:name="com.github.nacabaro.vbhelper.permission.ACCESS_TRANSFER_SEEN"
|
android:name="com.github.nacabaro.vbhelper.permission.ACCESS_TRANSFER_SEEN"
|
||||||
android:protectionLevel="signature" />
|
android:protectionLevel="signature" />
|
||||||
|
<permission
|
||||||
|
android:name="com.github.nacabaro.vbhelper.permission.ACCESS_CARD_DATA"
|
||||||
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
<uses-feature android:name="android.hardware.nfc" android:required="true" />
|
<uses-feature android:name="android.hardware.nfc" android:required="true" />
|
||||||
@ -35,6 +38,12 @@
|
|||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
<provider
|
||||||
|
android:name=".transfer.CardImportProvider"
|
||||||
|
android:authorities="com.github.nacabaro.vbhelper.cardimport"
|
||||||
|
android:exported="true"
|
||||||
|
android:readPermission="com.github.nacabaro.vbhelper.permission.ACCESS_CARD_DATA"
|
||||||
|
android:writePermission="com.github.nacabaro.vbhelper.permission.ACCESS_CARD_DATA" />
|
||||||
<provider
|
<provider
|
||||||
android:name=".transfer.SharedTransferSeenProvider"
|
android:name=".transfer.SharedTransferSeenProvider"
|
||||||
android:authorities="com.github.nacabaro.vbhelper.transferseen"
|
android:authorities="com.github.nacabaro.vbhelper.transferseen"
|
||||||
|
|||||||
@ -128,9 +128,15 @@ interface UserCharacterDao {
|
|||||||
@Query("SELECT * FROM BECharacterData WHERE id = :id")
|
@Query("SELECT * FROM BECharacterData WHERE id = :id")
|
||||||
fun getBeData(id: Long): Flow<BECharacterData>
|
fun getBeData(id: Long): Flow<BECharacterData>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM BECharacterData WHERE id = :id")
|
||||||
|
suspend fun getBeDataOrNull(id: Long): BECharacterData?
|
||||||
|
|
||||||
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
|
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
|
||||||
fun getVbData(id: Long): Flow<VBCharacterData>
|
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")
|
@Query("SELECT * FROM SpecialMissions WHERE characterId = :id")
|
||||||
fun getSpecialMissions(id: Long): Flow<List<SpecialMissions>>
|
fun getSpecialMissions(id: Long): Flow<List<SpecialMissions>>
|
||||||
|
|
||||||
|
|||||||
@ -133,6 +133,7 @@ fun ScanScreenPreview() {
|
|||||||
scanScreenController = object: ScanScreenController {
|
scanScreenController = object: ScanScreenController {
|
||||||
override val secretsFlow = MutableStateFlow<Secrets>(Secrets.getDefaultInstance())
|
override val secretsFlow = MutableStateFlow<Secrets>(Secrets.getDefaultInstance())
|
||||||
override val detectedTransportFlow: StateFlow<DetectedTransport> = MutableStateFlow(DetectedTransport.UNKNOWN)
|
override val detectedTransportFlow: StateFlow<DetectedTransport> = MutableStateFlow(DetectedTransport.UNKNOWN)
|
||||||
|
override val transferStatusFlow: StateFlow<String?> = MutableStateFlow(null)
|
||||||
override fun unregisterActivityLifecycleListener(key: String) { }
|
override fun unregisterActivityLifecycleListener(key: String) { }
|
||||||
override fun registerActivityLifecycleListener(
|
override fun registerActivityLifecycleListener(
|
||||||
key: String,
|
key: String,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
interface ScanScreenController {
|
interface ScanScreenController {
|
||||||
val secretsFlow: Flow<Secrets>
|
val secretsFlow: Flow<Secrets>
|
||||||
val detectedTransportFlow: StateFlow<DetectedTransport>
|
val detectedTransportFlow: StateFlow<DetectedTransport>
|
||||||
|
val transferStatusFlow: StateFlow<String?>
|
||||||
|
|
||||||
enum class WriteResult {
|
enum class WriteResult {
|
||||||
MOVE_CONFIRMED,
|
MOVE_CONFIRMED,
|
||||||
|
|||||||
@ -47,8 +47,11 @@ class ScanScreenControllerImpl(
|
|||||||
|
|
||||||
override val detectedTransportFlow: StateFlow<DetectedTransport>
|
override val detectedTransportFlow: StateFlow<DetectedTransport>
|
||||||
get() = _detectedTransportFlow
|
get() = _detectedTransportFlow
|
||||||
|
override val transferStatusFlow: StateFlow<String?>
|
||||||
|
get() = _transferStatusFlow
|
||||||
|
|
||||||
private val _detectedTransportFlow = MutableStateFlow(DetectedTransport.UNKNOWN)
|
private val _detectedTransportFlow = MutableStateFlow(DetectedTransport.UNKNOWN)
|
||||||
|
private val _transferStatusFlow = MutableStateFlow<String?>(null)
|
||||||
private var lastScannedCharacter: NfcCharacter? = null
|
private var lastScannedCharacter: NfcCharacter? = null
|
||||||
private var lastRequestedCharacterId: Long? = null
|
private var lastRequestedCharacterId: Long? = null
|
||||||
private val nfcAdapter: NfcAdapter
|
private val nfcAdapter: NfcAdapter
|
||||||
@ -135,6 +138,7 @@ class ScanScreenControllerImpl(
|
|||||||
|
|
||||||
override fun onClickRead(secrets: Secrets, onComplete: () -> Unit, onMultipleCards: (List<Card>) -> Unit) {
|
override fun onClickRead(secrets: Secrets, onComplete: () -> Unit, onMultipleCards: (List<Card>) -> Unit) {
|
||||||
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
||||||
|
setTransferStatus(R.string.scan_transfer_waiting_tap)
|
||||||
handleTag(
|
handleTag(
|
||||||
secrets,
|
secrets,
|
||||||
nfcAHandler = { tagCommunicator ->
|
nfcAHandler = { tagCommunicator ->
|
||||||
@ -185,6 +189,7 @@ class ScanScreenControllerImpl(
|
|||||||
|
|
||||||
override fun onClickWrite(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: (ScanScreenController.WriteResult) -> Unit) {
|
override fun onClickWrite(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: (ScanScreenController.WriteResult) -> Unit) {
|
||||||
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
||||||
|
setTransferStatus(R.string.scan_transfer_waiting_tap)
|
||||||
handleTag(
|
handleTag(
|
||||||
secrets,
|
secrets,
|
||||||
nfcAHandler = { tagCommunicator ->
|
nfcAHandler = { tagCommunicator ->
|
||||||
@ -207,9 +212,17 @@ class ScanScreenControllerImpl(
|
|||||||
}
|
}
|
||||||
onComplete.invoke(ScanScreenController.WriteResult.MOVE_CONFIRMED)
|
onComplete.invoke(ScanScreenController.WriteResult.MOVE_CONFIRMED)
|
||||||
componentActivity.getString(R.string.scan_sent_character_success)
|
componentActivity.getString(R.string.scan_sent_character_success)
|
||||||
} catch (e: Throwable) {
|
} catch (writeError: Throwable) {
|
||||||
Log.e("TAG", e.stackTraceToString())
|
Log.e("NFC_WRITE_A", "NFC-A write failed; attempting opposite direction", writeError)
|
||||||
componentActivity.getString(R.string.scan_error_generic)
|
runCatching {
|
||||||
|
val receivedCharacter = tagCommunicator.receiveCharacter()
|
||||||
|
val fallbackMessage = importCharacterFromNfcAFallback(receivedCharacter)
|
||||||
|
onComplete.invoke(ScanScreenController.WriteResult.COPIED)
|
||||||
|
fallbackMessage
|
||||||
|
}.getOrElse { readFallbackError ->
|
||||||
|
Log.e("NFC_WRITE_A", "NFC-A opposite-direction fallback failed", readFallbackError)
|
||||||
|
componentActivity.getString(R.string.scan_error_generic)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isoDepHandler = { isoDep ->
|
isoDepHandler = { isoDep ->
|
||||||
@ -251,10 +264,42 @@ class ScanScreenControllerImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun importCharacterFromNfcAFallback(receivedCharacter: NfcCharacter): String {
|
||||||
|
val application = componentActivity.applicationContext as VBHelper
|
||||||
|
val fromNfcConverter = FromNfcConverter(componentActivity)
|
||||||
|
val candidateCards = application.container.db
|
||||||
|
.cardDao()
|
||||||
|
.getCardByCardId(receivedCharacter.dimId.toInt())
|
||||||
|
|
||||||
|
if (candidateCards.isEmpty()) {
|
||||||
|
return "Detected source character on bracelet, but matching card is not imported in VBHelper."
|
||||||
|
}
|
||||||
|
|
||||||
|
val preferredCardId = receivedCharacter.appReserved2
|
||||||
|
.firstOrNull()
|
||||||
|
?.toLong()
|
||||||
|
?.takeIf { it > 0L }
|
||||||
|
val preferredCard = preferredCardId?.let { id ->
|
||||||
|
candidateCards.firstOrNull { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectedCard = when {
|
||||||
|
preferredCard != null -> preferredCard
|
||||||
|
candidateCards.size == 1 -> candidateCards.single()
|
||||||
|
else -> {
|
||||||
|
lastScannedCharacter = receivedCharacter
|
||||||
|
return "Detected source character, but multiple matching cards exist. Use Read and select the card."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromNfcConverter.addCharacterUsingCard(receivedCharacter, selectedCard.id)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Check card (NFC-A only; ISO-DEP watches don't need a DIM prep) -----------
|
// ---- Check card (NFC-A only; ISO-DEP watches don't need a DIM prep) -----------
|
||||||
|
|
||||||
override fun onClickCheckCard(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit) {
|
override fun onClickCheckCard(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit) {
|
||||||
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
||||||
|
setTransferStatus(R.string.scan_transfer_waiting_tap)
|
||||||
handleTag(
|
handleTag(
|
||||||
secrets,
|
secrets,
|
||||||
nfcAHandler = { tagCommunicator ->
|
nfcAHandler = { tagCommunicator ->
|
||||||
@ -284,6 +329,7 @@ class ScanScreenControllerImpl(
|
|||||||
|
|
||||||
override fun cancelRead() {
|
override fun cancelRead() {
|
||||||
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
||||||
|
setTransferStatus(R.string.scan_transfer_cancelled)
|
||||||
activeReaderSessionId = readerSessionCounter.incrementAndGet()
|
activeReaderSessionId = readerSessionCounter.incrementAndGet()
|
||||||
isHandlingTag.set(false)
|
isHandlingTag.set(false)
|
||||||
synchronized(tagStateLock) {
|
synchronized(tagStateLock) {
|
||||||
@ -362,6 +408,7 @@ class ScanScreenControllerImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldHandleTag) {
|
if (shouldHandleTag) {
|
||||||
|
setTransferStatus(R.string.scan_transfer_detected_keep_tap)
|
||||||
|
|
||||||
// Detect transport once per tap and lock to that route for this transfer.
|
// Detect transport once per tap and lock to that route for this transfer.
|
||||||
val isoDep = IsoDep.get(tag)
|
val isoDep = IsoDep.get(tag)
|
||||||
@ -380,27 +427,32 @@ class ScanScreenControllerImpl(
|
|||||||
_detectedTransportFlow.value = DetectedTransport.ISO_DEP
|
_detectedTransportFlow.value = DetectedTransport.ISO_DEP
|
||||||
isoDepTarget.connect()
|
isoDepTarget.connect()
|
||||||
isoDepTarget.use {
|
isoDepTarget.use {
|
||||||
|
setTransferStatus(R.string.scan_transfer_in_progress)
|
||||||
val successText = isoDepAction.invoke(isoDepTarget)
|
val successText = isoDepAction.invoke(isoDepTarget)
|
||||||
componentActivity.runOnUiThread {
|
componentActivity.runOnUiThread {
|
||||||
Toast.makeText(componentActivity, successText, Toast.LENGTH_SHORT).show()
|
Toast.makeText(componentActivity, successText, Toast.LENGTH_SHORT).show()
|
||||||
|
setTransferStatus(R.string.scan_transfer_complete_remove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e("NFC_HCE", "IsoDep transfer failed", e)
|
Log.e("NFC_HCE", "IsoDep transfer failed", e)
|
||||||
componentActivity.runOnUiThread {
|
componentActivity.runOnUiThread {
|
||||||
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic), Toast.LENGTH_SHORT).show()
|
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic), Toast.LENGTH_SHORT).show()
|
||||||
|
setTransferStatus(R.string.scan_transfer_failed_try_again)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (hasNfcARoute) {
|
} else if (hasNfcARoute) {
|
||||||
if (!handleNfcATag(tag, secrets, nfcAHandler)) {
|
if (!handleNfcATag(tag, secrets, nfcAHandler)) {
|
||||||
componentActivity.runOnUiThread {
|
componentActivity.runOnUiThread {
|
||||||
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_tag_not_vb), Toast.LENGTH_SHORT).show()
|
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_tag_not_vb), Toast.LENGTH_SHORT).show()
|
||||||
|
setTransferStatus(R.string.scan_transfer_failed_try_again)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
_detectedTransportFlow.value = DetectedTransport.UNKNOWN
|
||||||
componentActivity.runOnUiThread {
|
componentActivity.runOnUiThread {
|
||||||
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_tag_not_vb), Toast.LENGTH_SHORT).show()
|
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_tag_not_vb), Toast.LENGTH_SHORT).show()
|
||||||
|
setTransferStatus(R.string.scan_transfer_failed_try_again)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -443,9 +495,11 @@ class ScanScreenControllerImpl(
|
|||||||
nfcData.connect()
|
nfcData.connect()
|
||||||
nfcData.use {
|
nfcData.use {
|
||||||
val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
|
val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
|
||||||
|
setTransferStatus(R.string.scan_transfer_in_progress)
|
||||||
val successText = nfcAHandler(tagCommunicator)
|
val successText = nfcAHandler(tagCommunicator)
|
||||||
componentActivity.runOnUiThread {
|
componentActivity.runOnUiThread {
|
||||||
Toast.makeText(componentActivity, successText, Toast.LENGTH_SHORT).show()
|
Toast.makeText(componentActivity, successText, Toast.LENGTH_SHORT).show()
|
||||||
|
setTransferStatus(R.string.scan_transfer_complete_remove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@ -455,17 +509,23 @@ class ScanScreenControllerImpl(
|
|||||||
Log.w("NFC_A", "Ignoring stale NFC-A tag session; waiting for a fresh tap", e)
|
Log.w("NFC_A", "Ignoring stale NFC-A tag session; waiting for a fresh tap", e)
|
||||||
componentActivity.runOnUiThread {
|
componentActivity.runOnUiThread {
|
||||||
Toast.makeText(componentActivity, "Tag session expired. Please tap again.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(componentActivity, "Tag session expired. Please tap again.", Toast.LENGTH_SHORT).show()
|
||||||
|
setTransferStatus(R.string.scan_transfer_waiting_tap)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Log.e("NFC_A", "NfcA transfer failed", e)
|
Log.e("NFC_A", "NfcA transfer failed", e)
|
||||||
componentActivity.runOnUiThread {
|
componentActivity.runOnUiThread {
|
||||||
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic), Toast.LENGTH_SHORT).show()
|
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_error_generic), Toast.LENGTH_SHORT).show()
|
||||||
|
setTransferStatus(R.string.scan_transfer_failed_try_again)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setTransferStatus(messageRes: Int) {
|
||||||
|
_transferStatusFlow.value = componentActivity.getString(messageRes)
|
||||||
|
}
|
||||||
|
|
||||||
private fun disableReaderModeSafely() {
|
private fun disableReaderModeSafely() {
|
||||||
if (!nfcAdapter.isEnabled) return
|
if (!nfcAdapter.isEnabled) return
|
||||||
runCatching { nfcAdapter.disableReaderMode(componentActivity) }
|
runCatching { nfcAdapter.disableReaderMode(componentActivity) }
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import com.github.cfogrady.vbnfc.vb.SpecialMission
|
|||||||
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
|
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
|
||||||
import com.github.nacabaro.vbhelper.database.AppDatabase
|
import com.github.nacabaro.vbhelper.database.AppDatabase
|
||||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||||
|
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
|
||||||
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
||||||
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
|
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
|
||||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||||
@ -22,6 +23,11 @@ import java.util.Date
|
|||||||
class ToNfcConverter(
|
class ToNfcConverter(
|
||||||
private val componentActivity: ComponentActivity
|
private val componentActivity: ComponentActivity
|
||||||
) {
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val MIN_NFC_TRANSFORMATION_YEAR = 2021
|
||||||
|
private const val MAX_NFC_TRANSFORMATION_YEAR = 2035
|
||||||
|
}
|
||||||
|
|
||||||
private val application: VBHelper = componentActivity.applicationContext as VBHelper
|
private val application: VBHelper = componentActivity.applicationContext as VBHelper
|
||||||
private val database: AppDatabase = application.container.db
|
private val database: AppDatabase = application.container.db
|
||||||
|
|
||||||
@ -41,8 +47,12 @@ class ToNfcConverter(
|
|||||||
.characterDao()
|
.characterDao()
|
||||||
.getCharacterInfo(userCharacter.charId)
|
.getCharacterInfo(userCharacter.charId)
|
||||||
|
|
||||||
val card = database.cardDao().getCardByCharacterIdSync(characterId)
|
// Use the character's own type as the authoritative signal for encoding.
|
||||||
val shouldEncodeAsBem = card?.isBEm ?: (userCharacter.characterType == DeviceType.BEDevice)
|
// The card's isBEm flag only indicates the *card* is a BEM card, not that the
|
||||||
|
// stored UserCharacter has a BECharacterData row. Mixing those two signals was
|
||||||
|
// the root cause of a NoSuchElementException when a VB-type character was
|
||||||
|
// associated with a BE-type card.
|
||||||
|
val shouldEncodeAsBem = userCharacter.characterType == DeviceType.BEDevice
|
||||||
|
|
||||||
return if (shouldEncodeAsBem)
|
return if (shouldEncodeAsBem)
|
||||||
nfcToBENfc(characterId, characterInfo, userCharacter)
|
nfcToBENfc(characterId, characterInfo, userCharacter)
|
||||||
@ -186,8 +196,32 @@ class ToNfcConverter(
|
|||||||
): BENfcCharacter {
|
): BENfcCharacter {
|
||||||
val beData = database
|
val beData = database
|
||||||
.userCharacterDao()
|
.userCharacterDao()
|
||||||
.getBeData(characterId)
|
.getBeDataOrNull(characterId)
|
||||||
.first()
|
?: BECharacterData(
|
||||||
|
id = characterId,
|
||||||
|
trainingHp = 0,
|
||||||
|
trainingAp = 0,
|
||||||
|
trainingBp = 0,
|
||||||
|
remainingTrainingTimeInMinutes = 0,
|
||||||
|
itemEffectMentalStateValue = 0,
|
||||||
|
itemEffectMentalStateMinutesRemaining = 0,
|
||||||
|
itemEffectActivityLevelValue = 0,
|
||||||
|
itemEffectActivityLevelMinutesRemaining = 0,
|
||||||
|
itemEffectVitalPointsChangeValue = 0,
|
||||||
|
itemEffectVitalPointsChangeMinutesRemaining = 0,
|
||||||
|
abilityRarity = NfcCharacter.AbilityRarity.None,
|
||||||
|
abilityType = 0,
|
||||||
|
abilityBranch = 0,
|
||||||
|
abilityReset = 0,
|
||||||
|
rank = 0,
|
||||||
|
itemType = 0,
|
||||||
|
itemMultiplier = 0,
|
||||||
|
itemRemainingTime = 0,
|
||||||
|
otp0 = "",
|
||||||
|
otp1 = "",
|
||||||
|
minorVersion = 0,
|
||||||
|
majorVersion = 0,
|
||||||
|
)
|
||||||
|
|
||||||
val paddedTransformationArray = generateTransformationHistory(characterId)
|
val paddedTransformationArray = generateTransformationHistory(characterId)
|
||||||
|
|
||||||
@ -264,11 +298,15 @@ class ToNfcConverter(
|
|||||||
"Day: ${calendar.get(Calendar.DAY_OF_MONTH)}"
|
"Day: ${calendar.get(Calendar.DAY_OF_MONTH)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val rawYear = calendar.get(Calendar.YEAR)
|
||||||
|
val normalizedYear = rawYear.coerceIn(MIN_NFC_TRANSFORMATION_YEAR, MAX_NFC_TRANSFORMATION_YEAR)
|
||||||
|
if (normalizedYear != rawYear) {
|
||||||
|
Log.w("TransformationHistory", "Normalizing out-of-range transformation year $rawYear to $normalizedYear")
|
||||||
|
}
|
||||||
|
|
||||||
NfcCharacter.Transformation(
|
NfcCharacter.Transformation(
|
||||||
toCharIndex = it.monIndex.toUByte(),
|
toCharIndex = it.monIndex.toUByte(),
|
||||||
year = calendar
|
year = normalizedYear.toUShort(),
|
||||||
.get(Calendar.YEAR)
|
|
||||||
.toUShort(),
|
|
||||||
month = (calendar
|
month = (calendar
|
||||||
.get(Calendar.MONTH) + 1)
|
.get(Calendar.MONTH) + 1)
|
||||||
.toUByte(),
|
.toUByte(),
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import com.github.nacabaro.vbhelper.R
|
|||||||
@Composable
|
@Composable
|
||||||
fun ActionScreen(
|
fun ActionScreen(
|
||||||
topBannerText: String,
|
topBannerText: String,
|
||||||
|
transferStatusMessage: String?,
|
||||||
onClickCancel: () -> Unit,
|
onClickCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold (
|
Scaffold (
|
||||||
@ -36,6 +37,12 @@ fun ActionScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.action_place_near_reader))
|
Text(stringResource(R.string.action_place_near_reader))
|
||||||
|
if (!transferStatusMessage.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = transferStatusMessage,
|
||||||
|
modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = onClickCancel,
|
onClick = onClickCancel,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
|
|||||||
@ -25,6 +25,7 @@ fun ReadingScreen(
|
|||||||
onComplete: () -> Unit
|
onComplete: () -> Unit
|
||||||
) {
|
) {
|
||||||
val secrets by scanScreenController.secretsFlow.collectAsState(null)
|
val secrets by scanScreenController.secretsFlow.collectAsState(null)
|
||||||
|
val transferStatus by scanScreenController.transferStatusFlow.collectAsState(null)
|
||||||
|
|
||||||
var cardsRead by remember { mutableStateOf<List<Card>?>(null) }
|
var cardsRead by remember { mutableStateOf<List<Card>?>(null) }
|
||||||
|
|
||||||
@ -116,7 +117,10 @@ fun ReadingScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (readingScreen) {
|
if (readingScreen) {
|
||||||
ActionScreen(topBannerText = stringResource(R.string.reading_character_title),) {
|
ActionScreen(
|
||||||
|
topBannerText = stringResource(R.string.reading_character_title),
|
||||||
|
transferStatusMessage = transferStatus,
|
||||||
|
) {
|
||||||
Log.d(TAG, "ActionScreen.onCancel: cancelRead + onCancel")
|
Log.d(TAG, "ActionScreen.onCancel: cancelRead + onCancel")
|
||||||
readingScreen = false
|
readingScreen = false
|
||||||
scanScreenController.cancelRead()
|
scanScreenController.cancelRead()
|
||||||
|
|||||||
@ -30,6 +30,7 @@ fun WritingScreen(
|
|||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val secrets by scanScreenController.secretsFlow.collectAsState(null)
|
val secrets by scanScreenController.secretsFlow.collectAsState(null)
|
||||||
|
val transferStatus by scanScreenController.transferStatusFlow.collectAsState(null)
|
||||||
|
|
||||||
val application = LocalContext.current.applicationContext as VBHelper
|
val application = LocalContext.current.applicationContext as VBHelper
|
||||||
val storageRepository = StorageRepository(application.container.db)
|
val storageRepository = StorageRepository(application.container.db)
|
||||||
@ -103,7 +104,10 @@ fun WritingScreen(
|
|||||||
)
|
)
|
||||||
} else if (!isDoneSendingCard) {
|
} else if (!isDoneSendingCard) {
|
||||||
writing = true
|
writing = true
|
||||||
ActionScreen( topBannerText = stringResource(R.string.sending_card_title)) {
|
ActionScreen(
|
||||||
|
topBannerText = stringResource(R.string.sending_card_title),
|
||||||
|
transferStatusMessage = transferStatus,
|
||||||
|
) {
|
||||||
scanScreenController.cancelRead()
|
scanScreenController.cancelRead()
|
||||||
onCancel()
|
onCancel()
|
||||||
}
|
}
|
||||||
@ -121,7 +125,10 @@ fun WritingScreen(
|
|||||||
)
|
)
|
||||||
} else if (!isDoneWritingCharacter) {
|
} else if (!isDoneWritingCharacter) {
|
||||||
writing = true
|
writing = true
|
||||||
ActionScreen(topBannerText = stringResource(R.string.writing_character_action_title)) {
|
ActionScreen(
|
||||||
|
topBannerText = stringResource(R.string.writing_character_action_title),
|
||||||
|
transferStatusMessage = transferStatus,
|
||||||
|
) {
|
||||||
isDoneSendingCard = false
|
isDoneSendingCard = false
|
||||||
scanScreenController.cancelRead()
|
scanScreenController.cancelRead()
|
||||||
onCancel()
|
onCancel()
|
||||||
|
|||||||
@ -2,9 +2,30 @@ package com.github.nacabaro.vbhelper.source
|
|||||||
|
|
||||||
import com.github.cfogrady.vitalwear.protos.Character
|
import com.github.cfogrady.vitalwear.protos.Character
|
||||||
import com.github.nacabaro.vbhelper.database.AppDatabase
|
import com.github.nacabaro.vbhelper.database.AppDatabase
|
||||||
|
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
|
||||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
internal const val DEFAULT_VITALWEAR_TRAINING_TIME_SECONDS = 100L * 60L * 60L
|
||||||
|
|
||||||
|
internal fun resolveTrainingSeconds(
|
||||||
|
deviceType: DeviceType,
|
||||||
|
beData: BECharacterData?,
|
||||||
|
): Long {
|
||||||
|
val remainingTrainingTimeInMinutes = beData?.remainingTrainingTimeInMinutes
|
||||||
|
if (remainingTrainingTimeInMinutes != null) {
|
||||||
|
return remainingTrainingTimeInMinutes.toLong() * 60L
|
||||||
|
}
|
||||||
|
|
||||||
|
// VBHelper only stores the explicit training timer on BE payloads. When exporting non-BE
|
||||||
|
// characters to VitalWear, preserve their ability to train by seeding the same default
|
||||||
|
// window VitalWear uses for freshly created partners instead of sending an immediate zero.
|
||||||
|
return if (deviceType == DeviceType.BEDevice) {
|
||||||
|
0L
|
||||||
|
} else {
|
||||||
|
DEFAULT_VITALWEAR_TRAINING_TIME_SECONDS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class VitalWearCharacterExporter(
|
class VitalWearCharacterExporter(
|
||||||
private val database: AppDatabase
|
private val database: AppDatabase
|
||||||
@ -16,22 +37,29 @@ class VitalWearCharacterExporter(
|
|||||||
suspend fun buildCharacterProto(characterId: Long): Character {
|
suspend fun buildCharacterProto(characterId: Long): Character {
|
||||||
val characterWithSprites = database.userCharacterDao().getCharacterWithSprites(characterId)
|
val characterWithSprites = database.userCharacterDao().getCharacterWithSprites(characterId)
|
||||||
val userCharacter = database.userCharacterDao().getCharacter(characterId)
|
val userCharacter = database.userCharacterDao().getCharacter(characterId)
|
||||||
|
val characterInfo = database.characterDao().getCharacterInfo(userCharacter.charId)
|
||||||
val vwSettings = database.vitalWearSettingsDao().getByCharacterId(characterId)
|
val vwSettings = database.vitalWearSettingsDao().getByCharacterId(characterId)
|
||||||
val card = database.cardDao().getCardByCharacterIdSync(characterId)
|
val card = database.cardDao().getCardByCharacterIdSync(characterId)
|
||||||
?: error("Card not found for character $characterId")
|
?: error("Card not found for character $characterId")
|
||||||
val cardProgress = database.cardProgressDao().getCardProgressSync(card.id) ?: 0
|
val cardProgress = database.cardProgressDao().getCardProgressSync(card.id) ?: 0
|
||||||
|
val beData = database.userCharacterDao().getBeDataOrNull(characterId)
|
||||||
|
val vbData = database.userCharacterDao().getVbDataOrNull(characterId)
|
||||||
|
val normalizedTransformationCountdownMinutes = normalizeTransformationCountdownMinutes(
|
||||||
|
transformationCountdownMinutes = userCharacter.transformationCountdown,
|
||||||
|
hasPossibleTransformations = hasPossibleTransformations(userCharacter.charId),
|
||||||
|
)
|
||||||
return Character.newBuilder()
|
return Character.newBuilder()
|
||||||
.setCardId(card.cardId)
|
.setCardId(card.cardId)
|
||||||
.setCardName(card.name)
|
.setCardName(card.name)
|
||||||
.setCharacterStats(
|
.setCharacterStats(
|
||||||
Character.CharacterStats.newBuilder()
|
Character.CharacterStats.newBuilder()
|
||||||
.setSlotId(database.userCharacterDao().getCharacterInfo(characterId).charaIndex)
|
.setSlotId(characterInfo.charId)
|
||||||
.setVitals(characterWithSprites.vitalPoints)
|
.setVitals(characterWithSprites.vitalPoints)
|
||||||
.setTrainingTimeRemainingInSeconds(resolveTrainingSeconds(characterId, userCharacter.characterType))
|
.setTrainingTimeRemainingInSeconds(resolveTrainingSeconds(userCharacter.characterType, beData))
|
||||||
.setTimeUntilNextTransformation(characterWithSprites.transformationCountdown.toLong() * 60L)
|
.setTimeUntilNextTransformation(normalizedTransformationCountdownMinutes.toLong() * 60L)
|
||||||
.setTrainedBp(resolveTrainedBp(characterId, userCharacter.characterType))
|
.setTrainedBp(resolveTrainedBp(beData))
|
||||||
.setTrainedHp(resolveTrainedHp(characterId, userCharacter.characterType))
|
.setTrainedHp(resolveTrainedHp(beData))
|
||||||
.setTrainedAp(resolveTrainedAp(characterId, userCharacter.characterType))
|
.setTrainedAp(resolveTrainedAp(beData))
|
||||||
.setTrainedPp(characterWithSprites.trophies)
|
.setTrainedPp(characterWithSprites.trophies)
|
||||||
.setInjured(characterWithSprites.injuryStatus.name.lowercase().contains("inj"))
|
.setInjured(characterWithSprites.injuryStatus.name.lowercase().contains("inj"))
|
||||||
.setAccumulatedDailyInjuries(vwSettings?.accumulatedDailyInjuries ?: 0)
|
.setAccumulatedDailyInjuries(vwSettings?.accumulatedDailyInjuries ?: 0)
|
||||||
@ -40,6 +68,29 @@ class VitalWearCharacterExporter(
|
|||||||
.setTotalWins(characterWithSprites.totalBattlesWon)
|
.setTotalWins(characterWithSprites.totalBattlesWon)
|
||||||
.setCurrentPhaseWins(characterWithSprites.currentPhaseBattlesWon)
|
.setCurrentPhaseWins(characterWithSprites.currentPhaseBattlesWon)
|
||||||
.setMood(characterWithSprites.mood)
|
.setMood(characterWithSprites.mood)
|
||||||
|
.setDeviceType(userCharacter.characterType.toTransferDeviceType())
|
||||||
|
.setAgeInDays(userCharacter.ageInDays.coerceAtLeast(0))
|
||||||
|
.setActivityLevel(userCharacter.activityLevel.coerceAtLeast(0))
|
||||||
|
.setHeartRateCurrent(userCharacter.heartRateCurrent.coerceAtLeast(0))
|
||||||
|
.setGeneration(vbData?.generation ?: 0)
|
||||||
|
.setTotalTrophies(vbData?.totalTrophies ?: userCharacter.trophies.coerceAtLeast(0))
|
||||||
|
.setNextAdventureMissionStage(characterInfo.currentStage.coerceAtLeast(0))
|
||||||
|
.setItemEffectMentalStateValue(beData?.itemEffectMentalStateValue ?: 0)
|
||||||
|
.setItemEffectMentalStateMinutesRemaining(beData?.itemEffectMentalStateMinutesRemaining ?: 0)
|
||||||
|
.setItemEffectActivityLevelValue(beData?.itemEffectActivityLevelValue ?: 0)
|
||||||
|
.setItemEffectActivityLevelMinutesRemaining(beData?.itemEffectActivityLevelMinutesRemaining ?: 0)
|
||||||
|
.setItemEffectVitalPointsChangeValue(beData?.itemEffectVitalPointsChangeValue ?: 0)
|
||||||
|
.setItemEffectVitalPointsChangeMinutesRemaining(beData?.itemEffectVitalPointsChangeMinutesRemaining ?: 0)
|
||||||
|
.setAbilityRarity(beData?.abilityRarity?.ordinal ?: 0)
|
||||||
|
.setAbilityType(beData?.abilityType ?: 0)
|
||||||
|
.setAbilityBranch(beData?.abilityBranch ?: 0)
|
||||||
|
.setAbilityReset(beData?.abilityReset ?: 0)
|
||||||
|
.setRank(beData?.rank ?: 0)
|
||||||
|
.setItemType(beData?.itemType ?: 0)
|
||||||
|
.setItemMultiplier(beData?.itemMultiplier ?: 0)
|
||||||
|
.setItemRemainingTime(beData?.itemRemainingTime ?: 0)
|
||||||
|
.setFirmwareMinorVersion(beData?.minorVersion ?: 0)
|
||||||
|
.setFirmwareMajorVersion(beData?.majorVersion ?: 0)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.setSettings(
|
.setSettings(
|
||||||
@ -61,32 +112,23 @@ class VitalWearCharacterExporter(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveTrainingSeconds(characterId: Long, deviceType: DeviceType): Long {
|
|
||||||
if (deviceType != DeviceType.BEDevice) return 0L
|
private fun resolveTrainedBp(beData: com.github.nacabaro.vbhelper.domain.device_data.BECharacterData?): Int {
|
||||||
return database.userCharacterDao().getBeData(characterId).valueOrNull()?.remainingTrainingTimeInMinutes?.toLong()?.times(60L) ?: 0L
|
return beData?.trainingBp ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveTrainedBp(characterId: Long, deviceType: DeviceType): Int {
|
private fun resolveTrainedHp(beData: com.github.nacabaro.vbhelper.domain.device_data.BECharacterData?): Int {
|
||||||
return if (deviceType == DeviceType.BEDevice) {
|
return beData?.trainingHp ?: 0
|
||||||
database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingBp ?: 0
|
|
||||||
} else 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveTrainedHp(characterId: Long, deviceType: DeviceType): Int {
|
private fun resolveTrainedAp(beData: com.github.nacabaro.vbhelper.domain.device_data.BECharacterData?): Int {
|
||||||
return if (deviceType == DeviceType.BEDevice) {
|
return beData?.trainingAp ?: 0
|
||||||
database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingHp ?: 0
|
|
||||||
} else 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveTrainedAp(characterId: Long, deviceType: DeviceType): Int {
|
private suspend fun hasPossibleTransformations(cardCharacterId: Long): Boolean {
|
||||||
return if (deviceType == DeviceType.BEDevice) {
|
return database.characterDao().getEvolutionRequirementsForCard(cardCharacterId).firstOrNull()?.isNotEmpty() == true
|
||||||
database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingAp ?: 0
|
|
||||||
} else 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> kotlinx.coroutines.flow.Flow<T>.valueOrNull(): T? {
|
|
||||||
return runBlocking { firstOrNull() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveAllowedBattles(rawValue: Int?): Character.Settings.AllowedBattles {
|
private fun resolveAllowedBattles(rawValue: Int?): Character.Settings.AllowedBattles {
|
||||||
if (rawValue == null) return Character.Settings.AllowedBattles.CARD_ONLY
|
if (rawValue == null) return Character.Settings.AllowedBattles.CARD_ONLY
|
||||||
|
|||||||
@ -10,6 +10,7 @@ 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.VBCharacterData
|
||||||
import com.github.nacabaro.vbhelper.domain.device_data.VitalWearCharacterSettings
|
import com.github.nacabaro.vbhelper.domain.device_data.VitalWearCharacterSettings
|
||||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@ -43,23 +44,37 @@ class VitalWearCharacterImporter(
|
|||||||
val totalWins = character.characterStats.totalWins.coerceAtMost(totalBattles)
|
val totalWins = character.characterStats.totalWins.coerceAtMost(totalBattles)
|
||||||
val currentPhaseBattles = character.characterStats.currentPhaseBattles
|
val currentPhaseBattles = character.characterStats.currentPhaseBattles
|
||||||
val currentPhaseWins = character.characterStats.currentPhaseWins.coerceAtMost(currentPhaseBattles)
|
val currentPhaseWins = character.characterStats.currentPhaseWins.coerceAtMost(currentPhaseBattles)
|
||||||
val deviceType = if (importedCard.isBEm) DeviceType.BEDevice else DeviceType.VBDevice
|
val normalizedTransformationCountdown = normalizeTransformationCountdownMinutes(
|
||||||
|
transformationCountdownMinutes = secondsToMinutes(character.characterStats.timeUntilNextTransformation),
|
||||||
|
hasPossibleTransformations = hasPossibleTransformations(cardCharacter.id),
|
||||||
|
)
|
||||||
|
val hasBePayloadStats = character.characterStats.trainedHp > 0 ||
|
||||||
|
character.characterStats.trainedAp > 0 ||
|
||||||
|
character.characterStats.trainedBp > 0 ||
|
||||||
|
character.characterStats.trainingTimeRemainingInSeconds > 0L
|
||||||
|
val fallbackIsBeCharacter = importedCard.isBEm || hasBePayloadStats
|
||||||
|
val deviceType = resolveDeviceType(
|
||||||
|
transferDeviceType = character.characterStats.deviceType,
|
||||||
|
fallbackIsBeCharacter = fallbackIsBeCharacter,
|
||||||
|
)
|
||||||
|
val isBeCharacter = deviceType == DeviceType.BEDevice
|
||||||
|
|
||||||
val userCharacterId = database.userCharacterDao().insertCharacterData(
|
val userCharacterId = database.userCharacterDao().insertCharacterData(
|
||||||
UserCharacter(
|
UserCharacter(
|
||||||
charId = cardCharacter.id,
|
charId = cardCharacter.id,
|
||||||
ageInDays = max(character.transformationHistoryCount - 1, 0),
|
ageInDays = character.characterStats.ageInDays.takeIf { it > 0 }
|
||||||
|
?: max(character.transformationHistoryCount - 1, 0),
|
||||||
mood = character.characterStats.mood,
|
mood = character.characterStats.mood,
|
||||||
vitalPoints = character.characterStats.vitals,
|
vitalPoints = character.characterStats.vitals,
|
||||||
transformationCountdown = secondsToMinutes(character.characterStats.timeUntilNextTransformation),
|
transformationCountdown = normalizedTransformationCountdown,
|
||||||
injuryStatus = resolveInjuryStatus(character.characterStats.injured),
|
injuryStatus = resolveInjuryStatus(character.characterStats.injured),
|
||||||
trophies = character.characterStats.trainedPp,
|
trophies = character.characterStats.trainedPp,
|
||||||
currentPhaseBattlesWon = currentPhaseWins,
|
currentPhaseBattlesWon = currentPhaseWins,
|
||||||
currentPhaseBattlesLost = (currentPhaseBattles - currentPhaseWins).coerceAtLeast(0),
|
currentPhaseBattlesLost = (currentPhaseBattles - currentPhaseWins).coerceAtLeast(0),
|
||||||
totalBattlesWon = totalWins,
|
totalBattlesWon = totalWins,
|
||||||
totalBattlesLost = (totalBattles - totalWins).coerceAtLeast(0),
|
totalBattlesLost = (totalBattles - totalWins).coerceAtLeast(0),
|
||||||
activityLevel = 0,
|
activityLevel = character.characterStats.activityLevel.coerceAtLeast(0),
|
||||||
heartRateCurrent = 0,
|
heartRateCurrent = character.characterStats.heartRateCurrent.coerceAtLeast(0),
|
||||||
characterType = deviceType,
|
characterType = deviceType,
|
||||||
isActive = true
|
isActive = true
|
||||||
)
|
)
|
||||||
@ -76,7 +91,8 @@ class VitalWearCharacterImporter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (importedCard.isBEm) {
|
if (isBeCharacter) {
|
||||||
|
val abilityRarity = resolveAbilityRarity(character.characterStats.abilityRarity)
|
||||||
database.userCharacterDao().insertBECharacterData(
|
database.userCharacterDao().insertBECharacterData(
|
||||||
BECharacterData(
|
BECharacterData(
|
||||||
id = userCharacterId,
|
id = userCharacterId,
|
||||||
@ -84,32 +100,40 @@ class VitalWearCharacterImporter(
|
|||||||
trainingAp = character.characterStats.trainedAp,
|
trainingAp = character.characterStats.trainedAp,
|
||||||
trainingBp = character.characterStats.trainedBp,
|
trainingBp = character.characterStats.trainedBp,
|
||||||
remainingTrainingTimeInMinutes = secondsToMinutes(character.characterStats.trainingTimeRemainingInSeconds),
|
remainingTrainingTimeInMinutes = secondsToMinutes(character.characterStats.trainingTimeRemainingInSeconds),
|
||||||
itemEffectMentalStateValue = 0,
|
itemEffectMentalStateValue = character.characterStats.itemEffectMentalStateValue,
|
||||||
itemEffectMentalStateMinutesRemaining = 0,
|
itemEffectMentalStateMinutesRemaining = character.characterStats.itemEffectMentalStateMinutesRemaining,
|
||||||
itemEffectActivityLevelValue = 0,
|
itemEffectActivityLevelValue = character.characterStats.itemEffectActivityLevelValue,
|
||||||
itemEffectActivityLevelMinutesRemaining = 0,
|
itemEffectActivityLevelMinutesRemaining = character.characterStats.itemEffectActivityLevelMinutesRemaining,
|
||||||
itemEffectVitalPointsChangeValue = 0,
|
itemEffectVitalPointsChangeValue = character.characterStats.itemEffectVitalPointsChangeValue,
|
||||||
itemEffectVitalPointsChangeMinutesRemaining = 0,
|
itemEffectVitalPointsChangeMinutesRemaining = character.characterStats.itemEffectVitalPointsChangeMinutesRemaining,
|
||||||
abilityRarity = resolveDefaultAbilityRarity(),
|
abilityRarity = abilityRarity,
|
||||||
abilityType = 0,
|
abilityType = character.characterStats.abilityType,
|
||||||
abilityBranch = 0,
|
abilityBranch = character.characterStats.abilityBranch,
|
||||||
abilityReset = 0,
|
abilityReset = character.characterStats.abilityReset,
|
||||||
rank = 0,
|
rank = character.characterStats.rank,
|
||||||
itemType = 0,
|
itemType = character.characterStats.itemType,
|
||||||
itemMultiplier = 0,
|
itemMultiplier = character.characterStats.itemMultiplier,
|
||||||
itemRemainingTime = 0,
|
itemRemainingTime = character.characterStats.itemRemainingTime,
|
||||||
otp0 = "",
|
otp0 = "",
|
||||||
otp1 = "",
|
otp1 = "",
|
||||||
minorVersion = 0,
|
minorVersion = character.characterStats.firmwareMinorVersion,
|
||||||
majorVersion = 0
|
majorVersion = character.characterStats.firmwareMajorVersion
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
database.userCharacterDao().insertVBCharacterData(
|
database.userCharacterDao().insertVBCharacterData(
|
||||||
VBCharacterData(
|
VBCharacterData(
|
||||||
id = userCharacterId,
|
id = userCharacterId,
|
||||||
generation = max(character.transformationHistoryCount - 1, 0),
|
generation = if (character.characterStats.generation > 0) {
|
||||||
totalTrophies = character.characterStats.trainedPp.coerceAtLeast(0)
|
character.characterStats.generation
|
||||||
|
} else {
|
||||||
|
max(character.transformationHistoryCount - 1, 0)
|
||||||
|
},
|
||||||
|
totalTrophies = if (character.characterStats.totalTrophies > 0) {
|
||||||
|
character.characterStats.totalTrophies
|
||||||
|
} else {
|
||||||
|
character.characterStats.trainedPp.coerceAtLeast(0)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -162,6 +186,15 @@ class VitalWearCharacterImporter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val nextAdventureMissionStage = character.characterStats.nextAdventureMissionStage.coerceAtLeast(0)
|
||||||
|
if (nextAdventureMissionStage > 0) {
|
||||||
|
database.cardProgressDao().updateCardProgress(
|
||||||
|
currentStage = nextAdventureMissionStage,
|
||||||
|
cardId = importedCard.id,
|
||||||
|
unlocked = nextAdventureMissionStage > importedCard.stageCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return ImportResult(
|
return ImportResult(
|
||||||
success = true,
|
success = true,
|
||||||
message = "Imported ${importedCard.name} slot $slotId from VitalWear."
|
message = "Imported ${importedCard.name} slot $slotId from VitalWear."
|
||||||
@ -203,7 +236,14 @@ class VitalWearCharacterImporter(
|
|||||||
if (seconds <= 0L) {
|
if (seconds <= 0L) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return (seconds / 60L).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
|
// Preserve non-zero timers from HCE payloads instead of flooring 1..59s to 0.
|
||||||
|
return ((seconds + 59L) / 60L).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasPossibleTransformations(cardCharacterId: Long): Boolean {
|
||||||
|
return runBlocking {
|
||||||
|
database.characterDao().getEvolutionRequirementsForCard(cardCharacterId).firstOrNull()?.isNotEmpty() == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveInjuryStatus(injured: Boolean): NfcCharacter.InjuryStatus {
|
private fun resolveInjuryStatus(injured: Boolean): NfcCharacter.InjuryStatus {
|
||||||
@ -225,6 +265,11 @@ class VitalWearCharacterImporter(
|
|||||||
return enumValues<NfcCharacter.AbilityRarity>().first()
|
return enumValues<NfcCharacter.AbilityRarity>().first()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveAbilityRarity(rawValue: Int): NfcCharacter.AbilityRarity {
|
||||||
|
val rarities = enumValues<NfcCharacter.AbilityRarity>()
|
||||||
|
return rarities.getOrElse(rawValue) { resolveDefaultAbilityRarity() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun markSeen(cardName: String, slotId: Int, cardId: Long, timestamp: Long) {
|
private fun markSeen(cardName: String, slotId: Int, cardId: Long, timestamp: Long) {
|
||||||
database.dexDao().insertCharacter(slotId, cardId, timestamp)
|
database.dexDao().insertCharacter(slotId, cardId, timestamp)
|
||||||
transferSeenDao.markSeen(cardName, slotId, timestamp)
|
transferSeenDao.markSeen(cardName, slotId, timestamp)
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
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<out String>?, selection: String?,
|
||||||
|
selectionArgs: Array<out String>?, 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<out String>?): Int = 0
|
||||||
|
override fun update(uri: Uri, values: ContentValues?, selection: String?,
|
||||||
|
selectionArgs: Array<out String>?): Int = 0
|
||||||
|
}
|
||||||
|
|
||||||
@ -8,6 +8,12 @@ message Character {
|
|||||||
int32 card_id = 2;
|
int32 card_id = 2;
|
||||||
|
|
||||||
message CharacterStats {
|
message CharacterStats {
|
||||||
|
enum TransferDeviceType {
|
||||||
|
TRANSFER_DEVICE_TYPE_UNSPECIFIED = 0;
|
||||||
|
TRANSFER_DEVICE_TYPE_VB = 1;
|
||||||
|
TRANSFER_DEVICE_TYPE_BE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
int32 slot_id = 1;
|
int32 slot_id = 1;
|
||||||
int32 vitals = 2;
|
int32 vitals = 2;
|
||||||
int64 training_time_remaining_in_seconds = 3;
|
int64 training_time_remaining_in_seconds = 3;
|
||||||
@ -23,6 +29,29 @@ message Character {
|
|||||||
int32 total_wins = 13;
|
int32 total_wins = 13;
|
||||||
int32 current_phase_wins = 14;
|
int32 current_phase_wins = 14;
|
||||||
int32 mood = 15;
|
int32 mood = 15;
|
||||||
|
TransferDeviceType device_type = 16;
|
||||||
|
int32 age_in_days = 17;
|
||||||
|
int32 activity_level = 18;
|
||||||
|
int32 heart_rate_current = 19;
|
||||||
|
int32 generation = 20;
|
||||||
|
int32 total_trophies = 21;
|
||||||
|
int32 next_adventure_mission_stage = 22;
|
||||||
|
int32 item_effect_mental_state_value = 23;
|
||||||
|
int32 item_effect_mental_state_minutes_remaining = 24;
|
||||||
|
int32 item_effect_activity_level_value = 25;
|
||||||
|
int32 item_effect_activity_level_minutes_remaining = 26;
|
||||||
|
int32 item_effect_vital_points_change_value = 27;
|
||||||
|
int32 item_effect_vital_points_change_minutes_remaining = 28;
|
||||||
|
int32 ability_rarity = 29;
|
||||||
|
int32 ability_type = 30;
|
||||||
|
int32 ability_branch = 31;
|
||||||
|
int32 ability_reset = 32;
|
||||||
|
int32 rank = 33;
|
||||||
|
int32 item_type = 34;
|
||||||
|
int32 item_multiplier = 35;
|
||||||
|
int32 item_remaining_time = 36;
|
||||||
|
int32 firmware_minor_version = 37;
|
||||||
|
int32 firmware_major_version = 38;
|
||||||
}
|
}
|
||||||
CharacterStats character_stats = 3;
|
CharacterStats character_stats = 3;
|
||||||
|
|
||||||
|
|||||||
@ -58,6 +58,12 @@
|
|||||||
<string name="scan_nfc_must_be_enabled">O NFC precisa estar ativado.</string>
|
<string name="scan_nfc_must_be_enabled">O NFC precisa estar ativado.</string>
|
||||||
<string name="scan_target_device_full">O dispositivo de destino está cheio (ativo + backup). Remova um personagem antes de enviar.</string>
|
<string name="scan_target_device_full">O dispositivo de destino está cheio (ativo + backup). Remova um personagem antes de enviar.</string>
|
||||||
<string name="scan_transfer_skipped_storage_state">Transferência ignorada. O envio só é permitido quando o brinquedo tem um personagem ativo e o slot de backup está vazio.</string>
|
<string name="scan_transfer_skipped_storage_state">Transferência ignorada. O envio só é permitido quando o brinquedo tem um personagem ativo e o slot de backup está vazio.</string>
|
||||||
|
<string name="scan_transfer_waiting_tap">Pronto. Encoste e mantenha o celular e o relógio juntos até concluir.</string>
|
||||||
|
<string name="scan_transfer_detected_keep_tap">Conexão detectada. Mantenha os dispositivos juntos...</string>
|
||||||
|
<string name="scan_transfer_in_progress">Transferindo agora. Não afaste o celular ainda.</string>
|
||||||
|
<string name="scan_transfer_complete_remove">Transferência concluída. Agora você pode afastar o celular.</string>
|
||||||
|
<string name="scan_transfer_failed_try_again">Falha na transferência. Reposicione os dispositivos e tente novamente.</string>
|
||||||
|
<string name="scan_transfer_cancelled">Transferência cancelada.</string>
|
||||||
|
|
||||||
<string name="settings_title">Configurações</string>
|
<string name="settings_title">Configurações</string>
|
||||||
|
|
||||||
|
|||||||
@ -62,6 +62,12 @@
|
|||||||
<string name="scan_nfc_must_be_enabled">NFC must be enabled</string>
|
<string name="scan_nfc_must_be_enabled">NFC must be enabled</string>
|
||||||
<string name="scan_target_device_full">Target device is full (active + backup). Remove one character before sending.</string>
|
<string name="scan_target_device_full">Target device is full (active + backup). Remove one character before sending.</string>
|
||||||
<string name="scan_transfer_skipped_storage_state">Transfer skipped. Send is allowed only when the toy has an active character and an empty backup slot.</string>
|
<string name="scan_transfer_skipped_storage_state">Transfer skipped. Send is allowed only when the toy has an active character and an empty backup slot.</string>
|
||||||
|
<string name="scan_transfer_waiting_tap">Ready. Tap and hold phone/watch together until complete.</string>
|
||||||
|
<string name="scan_transfer_detected_keep_tap">Connection detected. Keep devices together...</string>
|
||||||
|
<string name="scan_transfer_in_progress">Transferring now. Do not remove phone yet.</string>
|
||||||
|
<string name="scan_transfer_complete_remove">Transfer complete. You can remove the phone now.</string>
|
||||||
|
<string name="scan_transfer_failed_try_again">Transfer failed. Reposition devices and try again.</string>
|
||||||
|
<string name="scan_transfer_cancelled">Transfer cancelled.</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="settings_title">Settings</string>
|
<string name="settings_title">Settings</string>
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,11 +1,61 @@
|
|||||||
package com.github.nacabaro.vbhelper.source
|
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.domain.card.Card
|
||||||
|
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class VitalWearCharacterImporterTest {
|
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
|
@Test
|
||||||
fun selectImportedCard_prefersUniqueCardIdWhenNameWasRenamed() {
|
fun selectImportedCard_prefersUniqueCardIdWhenNameWasRenamed() {
|
||||||
val selected = selectImportedCard(
|
val selected = selectImportedCard(
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -16,7 +16,7 @@ protobufGradlePlugin = "0.9.6"
|
|||||||
protobufJavalite = "4.33.4"
|
protobufJavalite = "4.33.4"
|
||||||
roomRuntime = "2.8.4"
|
roomRuntime = "2.8.4"
|
||||||
vbNfcReader = "0.2.0-SNAPSHOT"
|
vbNfcReader = "0.2.0-SNAPSHOT"
|
||||||
dimReader = "2.1.0"
|
dimReader = "2.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-compose-material = { module = "androidx.compose.material:material" }
|
androidx-compose-material = { module = "androidx.compose.material:material" }
|
||||||
|
|||||||
@ -22,3 +22,15 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
rootProject.name = "VBHelper"
|
rootProject.name = "VBHelper"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
||||||
|
includeBuild("../VB-DIM-Reader-2.0.0") {
|
||||||
|
dependencySubstitution {
|
||||||
|
substitute(module("com.github.cfogrady:vb-dim-reader")).using(project(":"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("../vb-nfc-reader") {
|
||||||
|
dependencySubstitution {
|
||||||
|
substitute(module("com.github.cfogrady:vb-nfc-reader")).using(project(":"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user