Compare commits

..
Author SHA1 Message Date
Nacho 5691f998f2 Cleaned settings screen 2025-01-05 22:18:39 +01:00
Nacho 4e2b9eb541 Fixed flickering and banner overlapping 2025-01-05 18:13:41 +01:00
Nacho 19fbed0ef2 More things!
- Started slowly implementing the original model.
- Names are bitmaps, so we use that.
- Sprites are stored in RoomDB as bitmap.
- Can now import DiMs, need to implement BEms.
- Can also view all the sprites for all the DiMs and now you can view DiMs individually (these two are temporary).

Things to do next
- Use the new model when uploading characters from the watch.
- Figure out why my implementation of ABGR to BGRA isn't working (or something like that)
- Improve data retrieval from the database, maybe using flows and properly implemented view models. As of now the interface is quite flickery.
- Improve code that handles DiMs and BEms.

As for bugs
- Interface sometimes likes flickering a lot, need to figure out why
- I cannot for the life of me create transparent bitmaps... help!
- Something else I might be forgetting...

Going to continue working in this branch.
2025-01-05 17:38:12 +01:00
nacabaroandGitHub 82292823d3 Merge pull request #6 from cfogrady/dim-reader-library
Import vb-dim-reader library
2025-01-05 10:50:04 +01:00
nacabaroandGitHub 01e82a6a44 Merge pull request #5 from cfogrady/nfc-reader-from-maven
Pull Nfc Reader Library from dependency repo instead of as second module.
2025-01-05 10:49:39 +01:00
Christopher O'Grady 1e73f52da5 Add dim-reader library
Update readme to reflect
Add commented out sample code.
2025-01-04 23:46:13 -05:00
Christopher O'Grady d9e492eb0e Add readme describing how to setup
Fix readme
2025-01-04 23:07:07 -05:00
Christopher O'Grady 5bd5fc3f74 Use nfc library from maven local instead of copied module. 2025-01-04 23:03:01 -05:00
Christopher O'Grady 3019560a26 Ignore keys file for now. 2025-01-04 22:47:13 -05:00
Christopher O'Grady a01d0eba47 Ignore generated project files. 2025-01-04 22:45:35 -05:00
nacabaroandGitHub bc02689850 Merge pull request #4 from nacabaro/ui/get_character_details
Few things, again!
2025-01-05 02:12:44 +01:00
Nacho e8f441ba2b Few things, again!
- From "Battles" to "Battle"
- Added a settings screen, to import keys and cards
- Added a way to access the settings from home
- Get character details
2025-01-05 01:58:08 +01:00
Nacho 59033e6459 Forgot to update some foreign key references... sorry! 2025-01-04 19:02:13 +01:00
nacabaroandGitHub 4736202838 Merge pull request #2 from nacabaro/database/initialise
Database/initialise
2025-01-04 18:51:17 +01:00
nacabaroandGitHub aa0f4241ea Merge pull request #1 from nacabaro/ui/main_screens
Added dummy screens plus icons
2025-01-04 16:12:07 +01:00
71 changed files with 900 additions and 2057 deletions
+8
View File
@@ -0,0 +1,8 @@
.gradle/
.idea/
local.properties
app/src/main/res/values/keys.xml
+38
View File
@@ -0,0 +1,38 @@
# VBHelper
## Developer Setup
1. Clone vb-nfc-reader (https://github.com/cfogrady/lib-vb-nfc)
2. Run vb-nfc-reader/publishToMavenLocal gradle task in the lib-vb-nfc project.
3. Clone vb-dim-reader (https://github.com/cfogrady/vb-dim-reader)
4. Run publishToMavenLocal gradle task in the vb-dim-reader project.
5. Create res/values/keys.xml within the app module.
6. Populate with:
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="password1">beHmacKey1</string>
<string name="password2">beHmacKey2</string>
<string name="decryptionKey">aesKey</string>
<integer-array name="substitutionArray">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
<item>14</item>
<item>15</item>
</integer-array>
</resources>
```
7. Replace the values in the keys.xml file with those extracted from the original app.
8. Run
+2 -1
View File
@@ -42,7 +42,8 @@ android {
dependencies {
implementation(libs.androidx.room.runtime)
implementation(project(":vb-nfc-reader"))
implementation(libs.vb.nfc.reader)
implementation(libs.dim.reader)
ksp(libs.androidx.room.compiler)
annotationProcessor(libs.androidx.room.compiler)
implementation(libs.androidx.core.ktx)
@@ -10,25 +10,31 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vb.dim.card.DimReader
import com.github.nacabaro.vbhelper.navigation.AppNavigation
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.R
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.Dim
import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.domain.Character
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryBECharacterData
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryCharacterData
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHistory
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private lateinit var nfcAdapter: NfcAdapter
@@ -36,10 +42,14 @@ class MainActivity : ComponentActivity() {
private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null)
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
// EXTRACTED DIRECTLY FROM EXAMPLE APP
override fun onCreate(savedInstanceState: Bundle?) {
deviceToCryptographicTransformers = getMapOfCryptographicTransformers()
registerFileActivityResult()
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (maybeNfcAdapter == null) {
Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show()
@@ -57,11 +67,109 @@ class MainActivity : ComponentActivity() {
}
}
private fun registerFileActivityResult() {
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
lifecycleScope.launch {
if (it.resultCode != RESULT_OK) {
Toast.makeText(applicationContext, "Import operation cancelled.", Toast.LENGTH_SHORT).show()
}
val contentResolver = applicationContext.contentResolver
val inputStream = contentResolver.openInputStream(it.data!!.data!!)
inputStream.use { fileReader ->
val dimReader = DimReader()
val card = dimReader.readCard(fileReader, false)
val dimModel = Dim(
id = card.header.dimId,
logo = card.spriteData.sprites[0].pixelData,
name = card.spriteData.text, // TODO Make user write card name
stageCount = card.adventureLevels.levels.size,
logoHeight = card.spriteData.sprites[0].height,
logoWidth = card.spriteData.sprites[0].width
)
val application = applicationContext as VBHelper
val storageRepository = application.container.db
storageRepository.dimDao().insertNewDim(dimModel)
val characters = card.characterStats.characterEntries
/*
Confusing math for me ahead
sprite[0] logo
sprite[10] name
sprite[10 + 1] character_1
sprite[10 + 2] character_2
sprite[16] name 1
sprite[17] character_1
sprite[18] character_2
sprite[23] name 2
sprite[24] character_1
sprite[25] character_2
sprite[23 + 12 + 1] name 3
sprite[23 + 12 + 2] character_1
sprite[23 + 12 + 3] character_2
*/
var spriteCounter = 10
var domainCharacters = mutableListOf<Character>()
for (index in 0 until characters.size) {
domainCharacters.add(
Character(
id = 0,
dimId = card.header.dimId,
monIndex = index,
name = card.spriteData.sprites[spriteCounter].pixelData,
stage = characters[index].stage,
attribute = characters[index].attribute,
baseHp = characters[index].hp,
baseBp = characters[index].dp,
baseAp = characters[index].ap,
sprite1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
sprite2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
nameWidth = card.spriteData.sprites[spriteCounter].width,
nameHeight = card.spriteData.sprites[spriteCounter].height,
spritesWidth = card.spriteData.sprites[spriteCounter + 1].width,
spritesHeight = card.spriteData.sprites[spriteCounter + 1].height
)
)
if (index == 0) {
spriteCounter += 6
} else if (index == 1) {
spriteCounter += 7
} else {
spriteCounter += 14
}
}
storageRepository
.characterDao()
.insertCharacter(*domainCharacters.toTypedArray())
val sprites = card.spriteData.sprites.map { sprite ->
Sprites(
id = 0,
sprite = sprite.pixelData,
width = sprite.width,
height = sprite.height
)
}
storageRepository
.characterDao()
.insertSprite(*sprites.toTypedArray())
}
inputStream?.close()
Toast.makeText(applicationContext, "Import successful!", Toast.LENGTH_SHORT).show()
}
}
}
@Composable
private fun MainApplication() {
var isDoneReadingCharacter by remember { mutableStateOf(false) }
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = application.container.db
AppNavigation(
isDoneReadingCharacter = isDoneReadingCharacter,
onClickRead = {
@@ -75,6 +183,13 @@ class MainActivity : ComponentActivity() {
},
onClickScan = {
isDoneReadingCharacter = false
},
onClickImportCard = {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
activityResultLauncher.launch(intent)
}
)
}
@@ -160,6 +275,7 @@ class MainActivity : ComponentActivity() {
*/
private fun addCharacterScannedIntoDatabase() {
val beCharacter = nfcCharacter as MutableStateFlow<BENfcCharacter?>
val temporaryCharacterData = TemporaryCharacterData(
dimId = nfcCharacter.value!!.dimId.toInt(),
charIndex = nfcCharacter.value!!.charIndex.toInt(),
@@ -182,7 +298,7 @@ class MainActivity : ComponentActivity() {
val application = applicationContext as VBHelper
val storageRepository = application.container.db
val characterId = storageRepository
val characterId: Long = storageRepository
.temporaryMonsterDao()
.insertCharacterData(temporaryCharacterData)
@@ -0,0 +1,37 @@
package com.github.nacabaro.vbhelper.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.dp
@Composable
fun CharacterEntry(
icon: ImageBitmap,
modifier: Modifier = Modifier,
onClick: () -> Unit = { }
) {
Card(
shape = MaterialTheme.shapes.medium,
onClick = onClick,
modifier = modifier
.padding(8.dp)
.size(96.dp)
) {
Image(
bitmap = icon,
contentDescription = "Icon",
filterQuality = FilterQuality.None,
modifier = Modifier
.padding(8.dp)
.fillMaxSize()
)
}
}
@@ -0,0 +1,64 @@
package com.github.nacabaro.vbhelper.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun StorageEntry(
name: String,
nameBitmap: ImageBitmap? = null,
modifier: Modifier = Modifier,
icon: Int? = null,
bitmap: ImageBitmap? = null,
onClick: () -> Unit = { }
) {
Card(
shape = MaterialTheme.shapes.medium,
onClick = onClick,
modifier = modifier
.padding(8.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
) {
if (bitmap != null) {
Image(
bitmap = bitmap,
contentDescription = name,
modifier = Modifier
.padding(8.dp)
.size(64.dp)
)
} else if (icon != null) {
Image(
painter = painterResource(id = icon),
contentDescription = name,
modifier = Modifier
.padding(8.dp)
.size(64.dp)
)
}
Text(
text = name,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(8.dp)
)
}
}
}
@@ -1,25 +1,66 @@
package com.github.nacabaro.vbhelper.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.nacabaro.vbhelper.R
@Composable
fun TopBanner(
text: String,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
onGearClick: (() -> Unit)? = null,
onBackClick: (() -> Unit)? = null,
) {
Text(
text = text,
textAlign = TextAlign.Center,
fontSize = 24.sp,
Box( // Use Box to overlay elements
modifier = modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth()
.padding(16.dp)
)
) {
Text(
text = text,
textAlign = TextAlign.Center,
fontSize = 24.sp,
modifier = Modifier
.align(Alignment.Center) // Center the text
)
if (onGearClick != null) {
IconButton(
onClick = onGearClick,
modifier = Modifier
.align(Alignment.CenterEnd) // Place gear icon at the end
) {
Icon(
painter = painterResource(R.drawable.baseline_settings_24), // Use a gear icon
contentDescription = "Settings"
)
}
}
if (onBackClick != null) {
IconButton(
onClick = onBackClick,
modifier = Modifier
.align(Alignment.CenterStart) // Place gear icon at the end
) {
Icon(
painter = painterResource(R.drawable.baseline_arrow_back_24), // Use a gear icon
contentDescription = "Settings"
)
}
}
}
}
@@ -0,0 +1,26 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.Character
import com.github.nacabaro.vbhelper.domain.Sprites
import org.w3c.dom.CharacterData
@Dao
interface CharacterDao {
@Insert
suspend fun insertCharacter(vararg characterData: Character)
@Query("SELECT * FROM Character")
suspend fun getAllCharacters(): List<Character>
@Query("SELECT * FROM Character WHERE dimId = :dimId")
suspend fun getCharacterByDimId(dimId: Int): List<Character>
@Insert
suspend fun insertSprite(vararg sprite: Sprites)
@Query("SELECT * FROM Sprites")
suspend fun getAllSprites(): List<Sprites>
}
@@ -2,15 +2,15 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.Dim
import kotlinx.coroutines.flow.Flow
@Dao
interface DiMDao {
@Insert
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertNewDim(dim: Dim)
@Query("SELECT * FROM Dim")
fun getAllDims(): Flow<List<Dim>>
suspend fun getAllDims(): List<Dim>
}
@@ -2,6 +2,11 @@ package com.github.nacabaro.vbhelper.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.nacabaro.vbhelper.daos.CharacterDao
import com.github.nacabaro.vbhelper.daos.DiMDao
import com.github.nacabaro.vbhelper.domain.Character
import com.github.nacabaro.vbhelper.domain.Dim
import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.temporary_daos.TemporaryMonsterDao
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryBECharacterData
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryCharacterData
@@ -12,10 +17,14 @@ import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHist
entities = [
TemporaryCharacterData::class,
TemporaryBECharacterData::class,
TemporaryTransformationHistory::class
TemporaryTransformationHistory::class,
Dim::class,
Character::class,
Sprites::class
]
)
abstract class AppDatabase : RoomDatabase() {
abstract fun temporaryMonsterDao(): TemporaryMonsterDao
abstract fun dimDao(): DiMDao
abstract fun characterDao(): CharacterDao
}
@@ -14,15 +14,20 @@ import androidx.room.ForeignKey
)
]
)
data class Mon (
@PrimaryKey val id: Int,
data class Character (
@PrimaryKey(autoGenerate = true) val id: Long,
val dimId: Int,
val monIndex: Int,
val name: String,
val name: ByteArray,
val stage: Int, // These should be replaced with enums
val attribute: Int, // This one too
val baseHp: Int,
val baseBp: Int,
val baseAp: Int,
val evoTime: Int, // In minutes
val sprite1: ByteArray,
val sprite2: ByteArray,
val nameWidth: Int,
val nameHeight: Int,
val spritesWidth: Int,
val spritesHeight: Int
)
@@ -1,5 +1,6 @@
package com.github.nacabaro.vbhelper.domain
import android.icu.text.ListFormatter.Width
import androidx.room.Entity
import androidx.room.PrimaryKey
@@ -7,6 +8,9 @@ import androidx.room.PrimaryKey
data class Dim(
@PrimaryKey(autoGenerate = true)
val id: Int,
val logo: ByteArray,
val logoWidth: Int,
val logoHeight: Int,
val name: String,
val stageCount: Int
)
@@ -7,13 +7,13 @@ import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = Mon::class,
entity = Character::class,
parentColumns = ["id"],
childColumns = ["monId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Mon::class,
entity = Character::class,
parentColumns = ["id"],
childColumns = ["nextMon"],
onDelete = ForeignKey.CASCADE
@@ -0,0 +1,12 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Sprites(
@PrimaryKey(autoGenerate = true) val id : Int,
val sprite: ByteArray,
val width: Int,
val height: Int
)
@@ -13,7 +13,7 @@ import androidx.room.PrimaryKey
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Mon::class,
entity = Character::class,
parentColumns = ["id"],
childColumns = ["monId"],
onDelete = ForeignKey.CASCADE
@@ -1,5 +1,6 @@
package com.github.nacabaro.vbhelper.navigation
import android.util.Log
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
@@ -9,14 +10,18 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.github.nacabaro.vbhelper.screens.BattlesScreen
import com.github.nacabaro.vbhelper.screens.DexScreen
import com.github.nacabaro.vbhelper.screens.DiMScreen
import com.github.nacabaro.vbhelper.screens.HomeScreen
import com.github.nacabaro.vbhelper.screens.ScanScreen
import com.github.nacabaro.vbhelper.screens.SettingsScreen
import com.github.nacabaro.vbhelper.screens.SpriteViewer
import com.github.nacabaro.vbhelper.screens.StorageScreen
@Composable
fun AppNavigation(
onClickRead: () -> Unit,
onClickScan: () -> Unit,
onClickImportCard: () -> Unit,
isDoneReadingCharacter: Boolean
) {
val navController = rememberNavController()
@@ -36,7 +41,9 @@ fun AppNavigation(
BattlesScreen()
}
composable(BottomNavItem.Home.route) {
HomeScreen()
HomeScreen(
navController = navController
)
}
composable(BottomNavItem.Storage.route) {
StorageScreen()
@@ -50,7 +57,30 @@ fun AppNavigation(
)
}
composable(BottomNavItem.Dex.route) {
DexScreen()
DexScreen(
navController = navController
)
}
composable(BottomNavItem.Settings.route) {
SettingsScreen(
navController = navController,
onClickImportCard = onClickImportCard
)
}
composable(BottomNavItem.Viewer.route) {
SpriteViewer(
navController = navController
)
}
composable(BottomNavItem.CardView.route) {
val dimId = it.arguments?.getString("dimId")
Log.d("dimId", dimId.toString())
if (dimId != null) {
DiMScreen(
navController = navController,
dimId = dimId.toInt()
)
}
}
}
}
@@ -8,8 +8,11 @@ sealed class BottomNavItem (
var label: String
) {
object Scan : BottomNavItem("Scan", R.drawable.baseline_nfc_24, "Scan")
object Battles : BottomNavItem("Battles", R.drawable.baseline_swords_24, "Battles")
object Battles : BottomNavItem("Battle", R.drawable.baseline_swords_24, "Battle")
object Home : BottomNavItem("Home", R.drawable.baseline_cottage_24, "Home")
object Dex : BottomNavItem("Dex", R.drawable.baseline_menu_book_24, "Dex")
object Storage : BottomNavItem("Storage", R.drawable.baseline_catching_pokemon_24, "Storage")
object Settings : BottomNavItem("Settings", R.drawable.baseline_settings_24, "Settings")
object Viewer : BottomNavItem("Viewer", R.drawable.baseline_image_24, "Viewer")
object CardView : BottomNavItem("Card/{dimId}", R.drawable.baseline_image_24, "Card")
}
@@ -1,5 +1,6 @@
package com.github.nacabaro.vbhelper.screens
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -7,38 +8,85 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.R
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.Dim
import com.github.nacabaro.vbhelper.navigation.BottomNavItem
import com.github.nacabaro.vbhelper.source.DexRepository
import kotlinx.coroutines.launch
import java.nio.ByteBuffer
@Composable
fun DexScreen() {
fun DexScreen(
navController: NavController
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val dexRepository = DexRepository(application.container.db)
val dimList = remember { mutableStateOf<List<Dim>>(emptyList()) }
LaunchedEffect(dexRepository) {
coroutineScope.launch {
val newDimList = dexRepository.getAllDims()
dimList.value = newDimList // Replace the entire list atomically
}
}
Scaffold (
topBar = { TopBanner("Discovered Digimon") }
topBar = {
TopBanner(
text = "Discovered Digimon",
onGearClick = {
navController.navigate(BottomNavItem.Viewer.route)
}
)
}
) { contentPadding ->
LazyColumn (
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
items(100) { i ->
items(dimList.value) {
val bitmap = remember (it.logo) {
Bitmap.createBitmap(it.logoWidth, it.logoHeight, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(it.logo))
}
}
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
DexDiMEntry(
name = "Digimon $i",
icon = R.drawable.baseline_egg_24,
onClick = {},
name = it.name,
logo = imageBitmap,
onClick = {
navController
.navigate(
BottomNavItem
.CardView.route
.replace("{dimId}", "${it.id}")
)
},
modifier = Modifier
.fillMaxWidth()
.padding(
vertical = 8.dp,
horizontal = 16.dp
)
.padding(8.dp)
)
}
}
@@ -48,7 +96,7 @@ fun DexScreen() {
@Composable
fun DexDiMEntry(
name: String,
icon: Int,
logo: ImageBitmap,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
@@ -64,8 +112,9 @@ fun DexDiMEntry(
.padding(8.dp)
) {
Image (
painter = painterResource(id = icon),
bitmap = logo,
contentDescription = name,
filterQuality = FilterQuality.None,
modifier = Modifier
.padding(8.dp)
.size(64.dp)
@@ -0,0 +1,70 @@
package com.github.nacabaro.vbhelper.screens
import android.graphics.Bitmap
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.domain.Character
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.source.DexRepository
import kotlinx.coroutines.launch
import java.nio.ByteBuffer
@Composable
fun DiMScreen(
navController: NavController,
dimId: Int
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val dexRepository = DexRepository(application.container.db)
val characterList = remember { mutableStateOf<List<Character>>(emptyList()) }
LaunchedEffect(dexRepository) {
coroutineScope.launch {
val newCharacterList = dexRepository.getCharactersByDimId(dimId)
characterList.value = newCharacterList
}
}
Scaffold (
topBar = {
TopBanner(
text = "Discovered Digimon",
onBackClick = {
navController.popBackStack()
}
)
}
) { contentPadding ->
LazyVerticalGrid(
columns = GridCells.Fixed(3),
contentPadding = contentPadding
) {
items(characterList.value) { character ->
val bitmapCharacter = remember (character.sprite1) {
Bitmap.createBitmap(character.spritesWidth, character.spritesHeight, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(character.sprite1))
}
}
val imageBitmapCharacter = remember(bitmapCharacter) { bitmapCharacter.asImageBitmap() }
CharacterEntry(
icon = imageBitmapCharacter,
onClick = { }
)
}
}
}
}
@@ -1,9 +1,34 @@
package com.github.nacabaro.vbhelper.screens
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.navigation.BottomNavItem
@Composable
fun HomeScreen() {
Text("Home Screen")
fun HomeScreen(
navController: NavController
) {
Scaffold (
topBar = {
TopBanner(
text = "VB Helper",
onGearClick = {
navController.navigate(BottomNavItem.Settings.route)
}
)
}
) { contentPadding ->
Box (
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
Text("Home Screen")
}
}
}
@@ -0,0 +1,97 @@
package com.github.nacabaro.vbhelper.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
@Composable
fun SettingsScreen(
navController: NavController,
onClickImportCard: () -> Unit
) {
Scaffold (
topBar = {
TopBanner(
text = "Settings",
onBackClick = {
navController.popBackStack()
}
)
},
modifier = Modifier
.fillMaxSize()
) { contentPadding ->
Column (
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
SettingsSection("General")
SettingsEntry(title = "Import VB key", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import VB Characters key", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import VB BE key", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import transform functions", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import decryption key", description = "Import standard vital bracelet keys") { }
SettingsSection("DiM/BEm management")
SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
SettingsEntry(title = "Rename DiM/BEm", description = "Set card name") { }
SettingsSection("About and credits")
SettingsEntry(title = "Credits", description = "Credits") { }
SettingsEntry(title = "About", description = "About") { }
}
}
}
@Composable
fun SettingsEntry(
title: String,
description: String,
onClick: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(16.dp)
) {
Text(text = title)
Text(
text = description,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline
)
}
}
@Composable
fun SettingsSection(
title: String
) {
Box (
modifier = Modifier
.padding(start = 16.dp)
) {
Text(
text = title,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
}
@@ -0,0 +1,89 @@
package com.github.nacabaro.vbhelper.screens
import android.graphics.Bitmap
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.source.SpriteRepo
import kotlinx.coroutines.launch
import java.nio.ByteBuffer
@Composable
fun SpriteViewer(
navController: NavController
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val db = application.container.db
val spriteRepo = SpriteRepo(db)
val spriteList = remember { mutableStateListOf<Sprites>() }
Log.d("SpriteViewer", "spriteList: $spriteList")
LaunchedEffect(spriteRepo) {
coroutineScope.launch {
spriteList.clear()
spriteList.addAll(spriteRepo.getAllSprites())
}
}
Scaffold (
topBar = {
TopBanner(
text = "Sprite viewer",
onBackClick = {
navController.popBackStack()
}
)
},
modifier = Modifier
.fillMaxSize()
) { contentPadding ->
LazyColumn (
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
items(spriteList) { sprite ->
val bitmap = remember (sprite.sprite) {
Log.d("SpriteViewer", "sprite: $sprite")
Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.sprite))
}
}
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
Image(
bitmap = imageBitmap,
contentDescription = "Sprite",
modifier = Modifier
.size(256.dp)
)
}
}
}
}
@@ -4,6 +4,7 @@ import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -12,7 +13,10 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardElevation
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@@ -23,13 +27,18 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.components.StorageEntry
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.source.StorageRepository
@@ -45,6 +54,7 @@ fun StorageScreen() {
val storageRepository = StorageRepository(application.container.db)
val monList = remember { mutableStateListOf<TemporaryCharacterData>() }
var selectedCharacter by remember { mutableStateOf<Long?>(null) }
LaunchedEffect(storageRepository) {
coroutineScope.launch {
@@ -60,11 +70,19 @@ fun StorageScreen() {
topBar = { TopBanner(text = "My Digimon") }
) { contentPadding ->
if (monList.isEmpty()) {
Text(
text = "Nothing to see here",
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(8.dp)
)
.padding(contentPadding)
.fillMaxSize()
) {
Text(
text = "Nothing to see here",
textAlign = TextAlign.Center,
modifier = Modifier
)
}
}
LazyVerticalGrid(
@@ -74,46 +92,71 @@ fun StorageScreen() {
.padding(top = contentPadding.calculateTopPadding())
) {
items(monList) { index ->
var showDialog by rememberSaveable { mutableStateOf(false) }
StorageEntry(
name = index.dimId.toString() + " - " + index.charIndex.toString(),
icon = R.drawable.ic_launcher_foreground,
onClick = { selectedCharacter = index.id },
modifier = Modifier
.padding(8.dp)
)
if (selectedCharacter != null) {
StorageDialog(
characterId = selectedCharacter!!,
onDismissRequest = { selectedCharacter = null }
)
}
}
}
}
}
@Composable
fun StorageEntry(
name: String,
icon: Int,
modifier: Modifier = Modifier
fun StorageDialog(
characterId: Long,
onDismissRequest: () -> Unit
) {
Card (
shape = MaterialTheme.shapes.medium,
modifier = modifier
.padding(8.dp)
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
val character = remember { mutableStateOf<TemporaryCharacterData?>(null) }
LaunchedEffect(storageRepository) {
coroutineScope.launch {
character.value = storageRepository.getSingleCharacter(characterId)
}
}
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Column (
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
Card(
shape = RoundedCornerShape(16.dp)
) {
Image(
painter = painterResource(id = icon),
contentDescription = name,
Column (
modifier = Modifier
.padding(8.dp)
.size(64.dp)
)
Text(
text = name,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(8.dp)
)
.padding(16.dp)
) {
if (character.value != null) {
Text(
text = character.value?.toString() ?: "Loading...",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(8.dp)
)
}
Button(
onClick = onDismissRequest
) {
Text(text = "Close")
}
}
}
}
}
@@ -0,0 +1,18 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.Character
import com.github.nacabaro.vbhelper.domain.Dim
import kotlinx.coroutines.flow.Flow
class DexRepository (
private val db: AppDatabase
) {
suspend fun getAllDims(): List<Dim> {
return db.dimDao().getAllDims()
}
suspend fun getCharactersByDimId(dimId: Int): List<Character> {
return db.characterDao().getCharacterByDimId(dimId)
}
}
@@ -0,0 +1,12 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.Sprites
class SpriteRepo (
private val db: AppDatabase
) {
suspend fun getAllSprites(): List<Sprites> {
return db.characterDao().getAllSprites()
}
}
@@ -9,4 +9,8 @@ class StorageRepository (
suspend fun getAllCharacters(): List<TemporaryCharacterData> {
return db.temporaryMonsterDao().getAllCharacters()
}
suspend fun getSingleCharacter(id: Long): TemporaryCharacterData {
return db.temporaryMonsterDao().getCharacter(id)
}
}
@@ -11,7 +11,7 @@ import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHist
@Dao
interface TemporaryMonsterDao {
@Insert
fun insertCharacterData(temporaryCharacterData: TemporaryCharacterData): Int
fun insertCharacterData(temporaryCharacterData: TemporaryCharacterData): Long
@Insert
fun insertBECharacterData(temporaryBECharacterData: TemporaryBECharacterData)
@@ -24,4 +24,7 @@ interface TemporaryMonsterDao {
@Query("SELECT * FROM TemporaryCharacterData")
suspend fun getAllCharacters(): List<TemporaryCharacterData>
@Query("SELECT * FROM TemporaryCharacterData WHERE id = :id")
suspend fun getCharacter(id: Long): TemporaryCharacterData
}
@@ -18,7 +18,7 @@ import com.github.cfogrady.vbnfc.data.NfcCharacter
]
)
data class TemporaryBECharacterData (
@PrimaryKey(autoGenerate = true) val id: Int,
@PrimaryKey(autoGenerate = true) val id: Long,
val trainingHp: Int,
val trainingAp: Int,
val trainingBp: Int,
@@ -6,7 +6,7 @@ import com.github.cfogrady.vbnfc.data.NfcCharacter
@Entity
data class TemporaryCharacterData (
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val dimId: Int,
var charIndex: Int,
var stage: Int,
@@ -16,8 +16,8 @@ import androidx.room.PrimaryKey
)
// Bit lazy, will correct later...
data class TemporaryTransformationHistory (
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val monId: Int,
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val monId: Long,
val toCharIndex: Int,
val yearsSince1988: Int,
val month: Int,
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>
+4
View File
@@ -9,6 +9,8 @@ lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
composeBom = "2024.04.01"
roomRuntime = "2.6.1"
vbNfcReader = "0.1.0"
dimReader = "2.1.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -27,6 +29,8 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
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" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+1 -1
View File
@@ -14,6 +14,7 @@ pluginManagement {
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenLocal()
google()
mavenCentral()
}
@@ -21,4 +22,3 @@ dependencyResolutionManagement {
rootProject.name = "VBHelper"
include(":app")
include(":vb-nfc-reader")
-1
View File
@@ -1 +0,0 @@
/build
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
-340
View File
@@ -1,340 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceStreaming">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
<option name="codename" value="OP573DL1" />
<option name="id" value="OP573DL1" />
<option name="manufacturer" value="OPPO" />
<option name="name" value="CPH2557" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-21" />
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
-44
View File
@@ -1,44 +0,0 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.github.cfogrady.vbnfc"
compileSdk = 34
defaultConfig {
minSdk = 28
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
testImplementation("io.mockk:mockk-android:1.13.14")
testImplementation("io.mockk:mockk-agent:1.13.14")
}
View File
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -1,24 +0,0 @@
package com.github.cfogrady.vbnfc
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.github.cfogrady.vbnfc.test", appContext.packageName)
}
}
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -1,78 +0,0 @@
package com.github.cfogrady.vbnfc
import java.lang.IllegalArgumentException
import java.nio.ByteOrder
fun ByteArray.getUInt32(index: Int = 0, byteOrder: ByteOrder = ByteOrder.nativeOrder()): UInt {
if (this.size < index + 4) {
throw IllegalArgumentException("Must be 4 bytes from index to get a UInt")
}
var result: UInt = 0u
for (i in 0 until 4) {
result = if (byteOrder == ByteOrder.BIG_ENDIAN) {
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(3 - i))
} else {
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(i))
}
}
return result
}
fun UInt.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray {
val byteArray = byteArrayOf(0, 0, 0, 0)
for(i in 0 until 4) {
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
byteArray[i] = ((this shr 8*i) and 255u).toByte()
} else {
byteArray[3-i] = ((this shr 8*i) and 255u).toByte()
}
}
return byteArray
}
fun ByteArray.getUInt16(index: Int = 0, byteOrder: ByteOrder = ByteOrder.nativeOrder()): UShort {
if (this.size < index + 2) {
throw IllegalArgumentException("Must be 2 bytes from index to get a UInt")
}
var result: UInt = 0u
for (i in 0 until 2) {
result = if (byteOrder == ByteOrder.BIG_ENDIAN) {
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(1 - i))
} else {
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(i))
}
}
return result.toUShort()
}
fun UShort.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray {
val byteArray = byteArrayOf(0, 0)
val asUInt = this.toUInt()
for(i in 0 until 2) {
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
byteArray[i] = ((asUInt shr 8*i) and 255u).toByte()
} else {
byteArray[1-i] = ((asUInt shr 8*i) and 255u).toByte()
}
}
return byteArray
}
fun UShort.toByteArray(bytes: ByteArray, dstIndex: Int, byteOrder: ByteOrder = ByteOrder.nativeOrder()) {
val asUInt = this.toUInt()
for(i in 0 until 2) {
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
bytes[i+dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
} else {
bytes[(1-i) + dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
}
}
}
fun ByteArray.copyIntoUShortArray(offset: Int, length: Int): Array<UShort> {
val result = Array<UShort>(length) { 0u }
for (i in 0..<length) {
result[i] = this.getUInt16(offset + i * 2, ByteOrder.BIG_ENDIAN)
}
return result
}
@@ -1,41 +0,0 @@
package com.github.cfogrady.vbnfc
import java.lang.IllegalStateException
class ChecksumCalculator {
companion object {
private val PagesWithChecksum = hashSetOf(8, 16, 24, 32, 40, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 104, 192, 200, 208, 216)
}
@OptIn(ExperimentalStdlibApi::class)
fun checkChecksums(data: ByteArray) {
operateOnChecksums(data) { checksumByte, checksumIdx ->
if (checksumByte != data[checksumIdx]) {
throw IllegalStateException("Checksum ${checksumByte.toHexString()} doesn't match expected ${data[checksumIdx].toHexString()}")
}
}
}
@OptIn(ExperimentalStdlibApi::class)
fun recalculateChecksums(data: ByteArray) {
operateOnChecksums(data) { checksumByte, checksumIdx ->
data[checksumIdx] = checksumByte
}
}
private fun operateOnChecksums(data: ByteArray, operator: (Byte, Int)->Unit) {
// loop through all data
for(i in data.indices step 16) {
val page = i/4 + 8 // first 8 pages are header data and not part of the character data
if (PagesWithChecksum.contains(page)) {
var sum = 0
val checksumIndex = i + 15
for(j in i..<checksumIndex) {
sum += data[j]
}
val checksumByte = (sum and 0xff).toByte()
operator(checksumByte, checksumIndex)
}
}
}
}
@@ -1,110 +0,0 @@
package com.github.cfogrady.vbnfc
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.xor
class CryptographicTransformer(private val readableHmacKey1: String, private val readableHmacKey2: String, private val aesKey: String, private val substitutionCipher: IntArray) {
companion object {
const val HMAC256 = "HmacSHA256"
}
// Creates a 4 byte password by hashing the current data using HMAC256 with the first key. Then
// applying a 4-bit substitution cypher on the result, and hashing again with the second key.
// The password are the 4 bytes starting at index 28 of that result.
fun createNfcPassword(inputData: ByteArray): ByteArray {
val hmacKey1 = decryptHmacKey(readableHmacKey1)
val hmacKey2 = decryptHmacKey(readableHmacKey2)
val hashedInput = generateHMacSHA256Hash(hmacKey1, inputData)
val substitutedBytes = apply4BitSubstitutionCipher(hashedInput)
val secondHash = generateHMacSHA256Hash(hmacKey2, substitutedBytes)
return secondHash.sliceArray(28..<32)
}
fun decryptData(data: ByteArray, tagId: ByteArray): ByteArray {
val hmacKey1 = decryptHmacKey(readableHmacKey1)
val hmacKey2 = decryptHmacKey(readableHmacKey2)
return cryptoTransformation(Cipher.DECRYPT_MODE, data, tagId, hmacKey1, hmacKey2)
}
fun encryptData(data: ByteArray, tagId: ByteArray): ByteArray {
val hmacKey1 = decryptHmacKey(readableHmacKey1)
val hmacKey2 = decryptHmacKey(readableHmacKey2)
return cryptoTransformation(Cipher.ENCRYPT_MODE, data, tagId, hmacKey1, hmacKey2)
}
private fun decryptHmacKey(str: String): String {
val decoded = Base64.getDecoder().decode(str)
val decrypt = decryptAesCbcPkcs5Padding(aesKey, decoded)
return String(decrypt, StandardCharsets.UTF_8)
}
private fun decryptAesCbcPkcs5Padding(key: String, data: ByteArray): ByteArray {
val keyBytes = key.toByteArray(StandardCharsets.UTF_8)
val rightSizedKey = keyBytes.copyOf(32)
val ivBytes = keyBytes.copyOfRange(key.length - 16, key.length)
val secretKeySpec = SecretKeySpec(rightSizedKey, "AES")
val ivParameterSpec = IvParameterSpec(ivBytes)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
return cipher.doFinal(data)
}
private fun generateHMacSHA256Hash(hmacKey: String, data: ByteArray): ByteArray {
val hmacKeyBytes = hmacKey.toByteArray(StandardCharsets.US_ASCII)
val secretKeySpec = SecretKeySpec(hmacKeyBytes, HMAC256)
val mac = Mac.getInstance(HMAC256)
mac.init(secretKeySpec)
return mac.doFinal(data)
}
// This is a 4 bit substitution cipher, where each 4 bits act as an index to another 4 bits
private fun apply4BitSubstitutionCipher(data: ByteArray): ByteArray {
val result = ByteArray(data.size)
for (idx in data.indices) {
val byte: Int = data[idx].toInt()
var newByte = 0
for (fourBitShifts in 0..<2) { // perform one OR without shift, and one OR shifted 4 bits
val shift = fourBitShifts * 4
val permutationIndex = (byte shr shift) and 0xF
newByte = newByte or (substitutionCipher[permutationIndex] shl shift)
}
result[idx] = newByte.toByte()
}
return result
}
// Hashes the tagId once and applies substitution cipher. Then hashes again.
// Splits hash and original tagId into key and initialization vector
private fun cryptoTransformation(cipherMode: Int, data: ByteArray, tagId: ByteArray, hmacKey1: String, hmacKey2: String): ByteArray {
var hashedTagId = apply4BitSubstitutionCipher(generateHMacSHA256Hash(hmacKey1, tagId))
hashedTagId = generateHMacSHA256Hash(hmacKey2, hashedTagId) // second hash
// generate actual key and initializing vector from tagIdGeneratedKey
val iv1 = ByteArray(15)
hashedTagId.copyInto(iv1, 0, 24, 32)
tagId.copyInto(iv1, 8, 0, 7)
val iv2 = hashedTagId.copyOf(15)
val ivParameterSpec = IvParameterSpec(xorBytes(iv1, iv2, 16))
val secretKeySpec = SecretKeySpec(hashedTagId.copyOf(16), "AES")
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
cipher.init(cipherMode, secretKeySpec, ivParameterSpec)
return cipher.doFinal(data)
}
private fun xorBytes(data1: ByteArray, data2: ByteArray, resultSize: Int): ByteArray {
val results = ByteArray(resultSize)
results.fill(0)
for (i in data1.indices) {
results[i] = data1[i] xor data2[i]
}
return results
}
}
@@ -1,30 +0,0 @@
package com.github.cfogrady.vbnfc
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.data.NfcHeader
interface NfcDataTranslator {
// setCharacterInByteArray takes the NfcCharacter and modifies the byte array with character
// data. At the time of writing this is used to write a parsed character into fresh unparsed
// device data when sending a character back to the device.
fun setCharacterInByteArray(character: NfcCharacter, bytes: ByteArray) {
}
// finalizeByteArrayFormat finalizes the byte array for NFC format by setting all the
// checksums, and duplicating the duplicate memory pages.
fun finalizeByteArrayFormat(bytes: ByteArray)
// getOperationCommandBytes gets an operation command corresponding to the existing header and
// the input operation
fun getOperationCommandBytes(header: NfcHeader, operation: Byte): ByteArray
// parseNfcCharacter parses the nfc data byte array into an instance of a NfcCharacter object
fun parseNfcCharacter(bytes: ByteArray): NfcCharacter
// parseHeader parses the nfc header byte array into an instance of NfcHeader
fun parseHeader(headerBytes: ByteArray): NfcHeader
val cryptographicTransformer: CryptographicTransformer
}
@@ -1,22 +0,0 @@
package com.github.cfogrady.vbnfc
import com.github.cfogrady.vbnfc.be.BENfcDataTranslator
import com.github.cfogrady.vbnfc.data.DeviceType
class NfcDataTranslatorFactory(
private val translators: MutableMap<UShort,NfcDataTranslator> = HashMap()
) {
fun getNfcDataTranslator(deviceTypeId: UShort): NfcDataTranslator {
val dataTranslator = translators[deviceTypeId]
if(dataTranslator != null) {
return dataTranslator
}
throw UnsupportedOperationException("Device type ${deviceTypeId} is not yet supported")
}
fun addNfcDataTranslator(nfcDataTranslator: NfcDataTranslator, deviceTypeId: UShort) {
translators[deviceTypeId] = nfcDataTranslator
}
}
@@ -1,193 +0,0 @@
package com.github.cfogrady.vbnfc
import android.nfc.tech.NfcA
import android.util.Log
import com.github.cfogrady.vbnfc.be.BENfcDataTranslator
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.data.NfcHeader
import java.nio.ByteOrder
class TagCommunicator(
private val nfcData: NfcA,
private val checksumCalculator: ChecksumCalculator,
private val nfcDataTranslatorFactory: NfcDataTranslatorFactory,
) {
companion object {
const val TAG = "VBNfcHandler"
const val HEADER_PAGE: Byte = 0x04
const val NFC_PASSWORD_COMMAND: Byte = 0x1b
const val NFC_READ_COMMAND: Byte = 0x30
const val NFC_WRITE_COMMAND: Byte = 0xA2.toByte()
const val STATUS_IDLE: Byte = 0
const val STATUS_READY: Byte = 1
const val OPERATION_IDLE: Byte = 0
const val OPERATION_READY: Byte = 1
const val OPERATION_TRANSFERRED_TO_APP: Byte = 2
const val OPERATION_CHECK_DIM: Byte = 3
const val OPERATION_TRANSFERED_TO_DEVICE: Byte = 4
const val START_DATA_PAGE = 8
const val LAST_DATA_PAGE = 220 // technically 223, but we read 4 pages at a time.
fun getInstance(nfcData: NfcA, deviceTypeIdSecrets: Map<UShort, CryptographicTransformer>): TagCommunicator {
val checksumCalculator = ChecksumCalculator()
val deviceToTranslator = HashMap<UShort, NfcDataTranslator>()
for (keyValue in deviceTypeIdSecrets) {
when(keyValue.key) {
DeviceType.VitalBraceletBEDeviceType -> {
deviceToTranslator[keyValue.key] = BENfcDataTranslator(keyValue.value, checksumCalculator)
}
else -> {
throw IllegalArgumentException("DeviceId ${keyValue.key} Provided Without Known Parser")
}
}
}
return TagCommunicator(nfcData, checksumCalculator, NfcDataTranslatorFactory(deviceToTranslator))
}
}
data class DeviceTranslatorAndHeader(val nfcHeader: NfcHeader, val translator: NfcDataTranslator)
@OptIn(ExperimentalStdlibApi::class)
fun receiveCharacter(): NfcCharacter {
val translatorAndHeader = fetchDeviceTranslatorAndHeader()
val header = translatorAndHeader.nfcHeader
val translator = translatorAndHeader.translator
Log.i(TAG, "Writing to make ready for operation")
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
Log.i(TAG, "Authenticating")
passwordAuth(translator.cryptographicTransformer)
Log.i(TAG, "Reading Character")
val encryptedCharacterData = readNfcData()
Log.i(TAG, "Raw NFC Data Received: ${encryptedCharacterData.toHexString()}")
val decryptedCharacterData = translator.cryptographicTransformer.decryptData(encryptedCharacterData, nfcData.tag.id)
checksumCalculator.checkChecksums(decryptedCharacterData)
val nfcCharacter = translator.parseNfcCharacter(decryptedCharacterData)
Log.i(TAG, "Known Character Stats: $nfcCharacter")
// Not ready to lose any of my mons in this...
//Log.i(TAG, "Signaling operation complete")
//nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_TRANSFERRED_TO_APP))
return nfcCharacter
}
@OptIn(ExperimentalStdlibApi::class)
private fun fetchDeviceTranslatorAndHeader(): DeviceTranslatorAndHeader {
val readData = nfcData.transceive(byteArrayOf(NFC_READ_COMMAND, HEADER_PAGE))
Log.i("TagCommunicator", "First 4 Pages: ${readData.toHexString()}")
val deviceTypeId = readData.getUInt16(4, ByteOrder.BIG_ENDIAN)
val translator = nfcDataTranslatorFactory.getNfcDataTranslator(deviceTypeId)
val header = translator.parseHeader(readData)
return DeviceTranslatorAndHeader(header, translator)
}
private fun readNfcData(): ByteArray {
val result = ByteArray(((LAST_DATA_PAGE +4)- START_DATA_PAGE) * 4)
for (page in START_DATA_PAGE..LAST_DATA_PAGE step 4) {
val pages = nfcData.transceive(byteArrayOf(NFC_READ_COMMAND, page.toByte()))
if (pages.size < 16) {
throw Exception("Failed to read page: $page")
}
System.arraycopy(pages, 0, result, (page - START_DATA_PAGE)*4, pages.size)
}
return result
}
fun prepareDIMForCharacter(dimId: UShort) {
val translatorAndHeader = fetchDeviceTranslatorAndHeader()
val header = translatorAndHeader.nfcHeader
val translator = translatorAndHeader.translator
// set app nonce to device ensure when we send back the character that we are preparing
// the same device we send to
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
// app authenticates, and reads everything, and checks the version
// The version check is only for the BE when transfering from DIM=0 (pulsemon).
// This was from the bug when the BE first came out.
// Check (page 103 [0:1] != 1, 0)
header.setDimId(dimId)
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_CHECK_DIM))
}
private fun defaultNfcDataGenerator(translator: NfcDataTranslator, character: NfcCharacter): ByteArray {
val currentNfcData = readNfcData()
val newNfcData = translator.cryptographicTransformer.decryptData(currentNfcData, nfcData.tag.id)
translator.setCharacterInByteArray(character, newNfcData)
checksumCalculator.recalculateChecksums(newNfcData)
translator.finalizeByteArrayFormat(newNfcData)
return newNfcData
}
// sendCharacter sends a character to the device using the nfcDataGenerator function. The
// default nfcDataGenerator reads the current data of the device and applies the new character
// data to the read data and prepares that to be sent back to the device. The nfcDataGenerator
// is a functor which takes in the NfcDataTranslator for the device and the NfcCharacter
// provided to the sendCharacter method and returns the decrypted byte array data to be sent
// back to the device.
@OptIn(ExperimentalStdlibApi::class)
fun sendCharacter(character: NfcCharacter, nfcDataGenerator: (NfcDataTranslator, NfcCharacter) -> ByteArray = this::defaultNfcDataGenerator) {
Log.i(TAG, "Sending Character: $character")
val deviceTranslatorAndHeader = fetchDeviceTranslatorAndHeader()
val translator = deviceTranslatorAndHeader.translator
val header = deviceTranslatorAndHeader.nfcHeader
// check the nonce
// if it's not expected, then the bracelet isn't ready
// Check the product and device ids that they match the target.
// This check relies on the app categories of BE, Vital Hero, and Vital Series.
// ensure the dim id matches the expected
if (character.dimId != header.getDimId()) {
throw IllegalArgumentException("Device is ready for DIM ${header.getDimId()}, but attempted to send ${character.dimId}")
}
// update the memory data
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
passwordAuth(translator.cryptographicTransformer)
var newNfcData = nfcDataGenerator(translator, character)
newNfcData = translator.cryptographicTransformer.encryptData(newNfcData, nfcData.tag.id)
Log.i(TAG, "Sending Character: ${newNfcData.toHexString()}")
// write nfc data
val pagedData = ConvertToPages(newNfcData)
for(pageToWriteIdx in 8..<pagedData.size) {
val pageToWrite = pagedData[pageToWriteIdx]
nfcData.transceive(byteArrayOf(NFC_WRITE_COMMAND, pageToWriteIdx.toByte(), pageToWrite[0], pageToWrite[1], pageToWrite[2], pageToWrite[3]))
}
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_TRANSFERED_TO_DEVICE))
}
@OptIn(ExperimentalStdlibApi::class)
fun passwordAuth(cryptographicTransformer: CryptographicTransformer) {
val tagId = nfcData.tag.id
Log.i(TAG, "TagId: ${tagId.toHexString()}")
val password = cryptographicTransformer.createNfcPassword(tagId)
try {
val result = nfcData.transceive(byteArrayOf(NFC_PASSWORD_COMMAND, password[0], password[1], password[2], password[3]))
Log.i(TAG, "PasswordAuth Result: ${result.toHexString()}")
if (result.size == 1) {
throw AuthenticationException("Authentication failed. Result: ${result.toHexString()}")
}
} catch (e: Exception) {
Log.e(TAG, "Exception: ${e.message}")
}
}
// addDataTranslator adds a new data translator to be used with the specified deviceTypeId.
// This can be used to keep the same general communication protocol, but allows for a different
// parsing of the data.
fun addDataTranslator(nfcDataTranslator: NfcDataTranslator, deviceTypeId: UShort) {
nfcDataTranslatorFactory.addNfcDataTranslator(nfcDataTranslator, deviceTypeId)
}
class AuthenticationException(message: String): Exception(message)
}
@@ -1,20 +0,0 @@
package com.github.cfogrady.vbnfc
// ConverToPages converts the byte array into the paged structure used in NFC communication
// If data for the header isn't included, the first 8 pages will be 0 filled.
fun ConvertToPages(data: ByteArray, header: ByteArray? = null) : List<ByteArray> {
val pages = ArrayList<ByteArray>()
// setup blank header pages
for (i in 0..7) {
if (header != null) {
val index = i*4
pages.add(header.sliceArray(index..<index+4))
} else {
pages.add(byteArrayOf(0, 0, 0, 0))
}
}
for(i in data.indices step 4) {
pages.add(data.sliceArray(i..<i+4))
}
return pages
}
@@ -1,183 +0,0 @@
package com.github.cfogrady.vbnfc.be
import com.github.cfogrady.vbnfc.data.NfcCharacter
import java.util.Arrays
import java.util.Objects
class BENfcCharacter(
dimId: UShort,
charIndex: UShort,
stage: Byte,
attribute: Attribute,
ageInDays: Byte,
nextAdventureMissionStage: Byte,
mood: Byte,
vitalPoints: UShort,
transformationCountdownInMinutes: UShort,
injuryStatus: InjuryStatus,
trainingPp: UShort,
currentPhaseBattlesWon: UShort,
currentPhaseBattlesLost: UShort,
totalBattlesWon: UShort,
totalBattlesLost: UShort,
activityLevel: Byte,
heartRateCurrent: UByte,
transformationHistory: Array<Transformation>,
var trainingHp: UShort,
var trainingAp: UShort,
var trainingBp: UShort,
var remainingTrainingTimeInMinutes: UShort,
var itemEffectMentalStateValue: Byte,
var itemEffectMentalStateMinutesRemaining: Byte,
var itemEffectActivityLevelValue: Byte,
var itemEffectActivityLevelMinutesRemaining: Byte,
var itemEffectVitalPointsChangeValue: Byte,
var itemEffectVitalPointsChangeMinutesRemaining: Byte,
var abilityRarity: AbilityRarity,
var abilityType: UShort,
var abilityBranch: UShort,
var abilityReset: Byte,
var rank: Byte,
var itemType: Byte,
var itemMultiplier: Byte,
var itemRemainingTime: Byte,
internal val otp0: ByteArray, // OTP matches the character to the dim
internal val otp1: ByteArray, // OTP matches the character to the dim
var characterCreationFirmwareVersion: FirmwareVersion,
var appReserved1: ByteArray, // this is a 12 byte array reserved for new app features, a custom app should be able to safely use this for custom features
var appReserved2: Array<UShort>, // this is a 3 element array reserved for new app features, a custom app should be able to safely use this for custom features
) :
NfcCharacter(
dimId = dimId,
charIndex = charIndex,
stage = stage,
attribute = attribute,
ageInDays = ageInDays,
nextAdventureMissionStage = nextAdventureMissionStage,
mood = mood,
vitalPoints = vitalPoints,
transformationCountdown = transformationCountdownInMinutes,
injuryStatus = injuryStatus,
trophies = trainingPp,
currentPhaseBattlesWon = currentPhaseBattlesWon,
currentPhaseBattlesLost = currentPhaseBattlesLost,
totalBattlesWon = totalBattlesWon,
totalBattlesLost = totalBattlesLost,
activityLevel = activityLevel,
heartRateCurrent = heartRateCurrent,
transformationHistory = transformationHistory,
)
{
fun getTrainingPp(): UShort {
return trophies
}
fun setTrainingPp(trainingPp: UShort) {
trophies = trainingPp
}
fun getWinPercentage(): Byte {
val totalBatles = currentPhaseBattlesWon + currentPhaseBattlesLost
if (totalBatles == 0u) {
return 0
}
return ((100u * currentPhaseBattlesWon) / totalBatles).toByte()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BENfcCharacter
if(!super.equals(other)) return false
if (trainingHp != other.trainingHp) return false
if (trainingAp != other.trainingAp) return false
if (trainingBp != other.trainingBp) return false
if (remainingTrainingTimeInMinutes != other.remainingTrainingTimeInMinutes) return false
if (itemEffectMentalStateValue != other.itemEffectMentalStateValue) return false
if (itemEffectMentalStateMinutesRemaining != other.itemEffectMentalStateMinutesRemaining) return false
if (itemEffectActivityLevelValue != other.itemEffectActivityLevelValue) return false
if (itemEffectActivityLevelMinutesRemaining != other.itemEffectActivityLevelMinutesRemaining) return false
if (itemEffectVitalPointsChangeValue != other.itemEffectVitalPointsChangeValue) return false
if (itemEffectVitalPointsChangeMinutesRemaining != other.itemEffectVitalPointsChangeMinutesRemaining) return false
if (abilityRarity != other.abilityRarity) return false
if (abilityType != other.abilityType) return false
if (abilityBranch != other.abilityBranch) return false
if (abilityReset != other.abilityReset) return false
if (rank != other.rank) return false
if (itemType != other.itemType) return false
if (itemMultiplier != other.itemMultiplier) return false
if (itemRemainingTime != other.itemRemainingTime) return false
if (!otp0.contentEquals(other.otp0)) return false
if (!otp1.contentEquals(other.otp1)) return false
if (characterCreationFirmwareVersion != other.characterCreationFirmwareVersion) return false
if (!appReserved1.contentEquals(other.appReserved1)) return false
if (!appReserved2.contentEquals(other.appReserved2)) return false
return true
}
override fun hashCode(): Int {
return Objects.hash(
super.hashCode(),
trainingHp,
trainingAp,
trainingBp,
remainingTrainingTimeInMinutes,
itemEffectMentalStateValue,
itemEffectMentalStateMinutesRemaining,
itemEffectActivityLevelValue,
itemEffectActivityLevelMinutesRemaining,
itemEffectVitalPointsChangeValue,
itemEffectVitalPointsChangeMinutesRemaining,
abilityRarity,
abilityType,
abilityBranch,
abilityReset,
rank,
itemType,
itemMultiplier,
itemRemainingTime,
otp0.contentHashCode(),
otp1.contentHashCode(),
characterCreationFirmwareVersion,
appReserved1.contentHashCode(),
appReserved2.contentHashCode())
}
@OptIn(ExperimentalStdlibApi::class)
override fun toString(): String {
return """
${super.toString()}
BENfcCharacter(
trainingHp=$trainingHp,
trainingAp=$trainingAp,
trainingBp=$trainingBp,
remainingTrainingTimeInMinutes=$remainingTrainingTimeInMinutes,
itemEffectMentalStateValue=$itemEffectMentalStateValue,
itemEffectMentalStateMinutesRemaining=$itemEffectMentalStateMinutesRemaining,
itemEffectActivityLevelValue=$itemEffectActivityLevelValue,
itemEffectActivityLevelMinutesRemaining=$itemEffectActivityLevelMinutesRemaining,
itemEffectVitalPointsChangeValue=$itemEffectVitalPointsChangeValue,
itemEffectVitalPointsChangeMinutesRemaining=$itemEffectVitalPointsChangeMinutesRemaining,
abilityRarity=$abilityRarity,
abilityType=$abilityType,
abilityBranch=$abilityBranch,
abilityReset=$abilityReset,
rank=$rank,
itemType=$itemType,
itemMultiplier=$itemMultiplier,
itemRemainingTime=$itemRemainingTime,
otp0=${otp0.toHexString()},
otp1=${otp1.toHexString()},
characterCreationFirmwareVersion=$characterCreationFirmwareVersion,
appReserved1=${appReserved1.contentToString()},
appReserved2=${appReserved2.contentToString()}
)"""
}
}
@@ -1,68 +0,0 @@
package com.github.cfogrady.vbnfc.be
import com.github.cfogrady.vbnfc.ChecksumCalculator
// Obsolete... being held onto for reference to device side data.
class BENfcDataFactory(checksumCalculator: ChecksumCalculator = ChecksumCalculator()) {
fun buildBENfcDevice(bytes: ByteArray): BENfcDevice {
return BENfcDevice(
gender = BENfcDevice.Gender.entries[bytes[12].toInt()],
registedDims = bytes.sliceArray(32..<32+15),
currDays = bytes[78]
)
// reserved1 = readByte(107)
// saveFirmwareVersion = readUShort(108)
// advMissionStage = readByte(128)
//
//
//
//
// reserved2 = readByte(140)
//
// reserved3 = readUShort(162)
// year = readByte(164)
// month = readByte(165)
// day = readByte(166)
// vitalPointsHistory0 = readUShort(176)
// vitalPointsHistory1 = readUShort(178)
// vitalPointsHistory2 = readUShort(180)
// vitalPointsHistory3 = readUShort(182)
// vitalPointsHistory4 = readUShort(184)
// vitalPointsHistory5 = readUShort(186)
// year0PreviousVitalPoints = readByte(188)
// month0PreviousVitalPoints = readByte(189)
// day0PreviousVitalPoints = readByte(190)
// year1PreviousVitalPoints = readByte(192)
// month1PreviousVitalPoints = readByte(193)
// day1PreviousVitalPoints = readByte(194)
// year2PreviousVitalPoints = readByte(195)
// month2PreviousVitalPoints = readByte(196)
// day2PreviousVitalPoints = readByte(197)
// year3PreviousVitalPoints = readByte(198)
// month3PreviousVitalPoints = readByte(199)
// day3PreviousVitalPoints = readByte(200)
// year4PreviousVitalPoints = readByte(201)
// month4PreviousVitalPoints = readByte(202)
// day4PreviousVitalPoints = readByte(203)
// year5PreviousVitalPoints = readByte(204)
// month5PreviousVitalPoints = readByte(205)
// day5PreviousVitalPoints = readByte(206)
//
// reserved4 = readUShort(262)
// reserved5 = readByte(264)
// questionMark = readByte(265)
// reserved6 = readByte(289)
// reserved7 = readByte(291)
//
// reserved8 = readUShort(297)
// reserved9 = readUShortArray(376, 2)
//
}
}
@@ -1,271 +0,0 @@
package com.github.cfogrady.vbnfc.be
import android.util.Log
import com.github.cfogrady.vbnfc.ChecksumCalculator
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.NfcDataTranslator
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.copyIntoUShortArray
import com.github.cfogrady.vbnfc.data.DeviceSubType
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.data.NfcHeader
import com.github.cfogrady.vbnfc.getUInt16
import com.github.cfogrady.vbnfc.toByteArray
import java.nio.ByteOrder
class BENfcDataTranslator(
override val cryptographicTransformer: CryptographicTransformer,
private val checksumCalculator: ChecksumCalculator = ChecksumCalculator()
): NfcDataTranslator {
companion object {
const val OPERATION_PAGE: Byte = 0x6
// CHARACTER
const val APP_RESERVED_START = 0
const val APP_RESERVED_SIZE = 12
const val INJURY_STATUS_IDX = 64
const val APP_RESERVED_2_START = 66
const val APP_RESERVED_2_SIZE = 3 //3 ushorts
const val CHARACTER_INDEX_IDX = 72
const val DIM_ID_IDX = 74
const val PHASE_IDX = 76
const val ATTRIBUTE_IDX = 77
const val AGE_IN_DAYS_IDX = 78 // always 0 on BE :(
const val TRAINING_PP_IDX = 96
const val CURRENT_BATTLES_WON_IDX = 98
const val CURRENT_BATTLES_LOST_IDX = 100
const val TOTAL_BATTLES_WON_IDX = 102
const val TOTAL_BATTLES_LOST_IDX = 104
const val WIN_PCT_IDX = 106 // unused
const val CHARACTER_CREATION_FIRMWARE_VERSION_IDX = 108
const val NEXT_ADVENTURE_MISSION_STAGE_IDX = 128
const val MOOD_IDX = 129
const val ACTIVITY_LEVEL_IDX = 130
const val HEART_RATE_CURRENT_IDX = 131
const val VITAL_POINTS_IDX = 132
const val ITEM_EFFECT_MENTAL_STATE_VALUE_IDX = 134
const val ITEM_EFFECT_MENTAL_STATE_MINUTES_REMAINING_IDX = 135
const val ITEM_EFFECT_ACTIVITY_LEVEL_VALUE_IDX = 136
const val ITEM_EFFECT_ACTIVITY_LEVEL_MINUTES_REMAINING_IDX = 137
const val ITEM_EFFECT_VITAL_POINTS_CHANGE_VALUE_IDX = 138
const val ITEM_EFFECT_VITAL_POINTS_CHANGE_MINUTES_REMAINING_IDX = 139
// 140 reserved
const val TRANSFORMATION_COUNT_DOWN_IDX = 141
const val TRANSFORMATION_HISTORY_START = 208
const val TRAINING_HP_IDX = 256
const val TRAINING_AP_IDX = 258
const val TRAINING_BP_IDX = 260
const val TRAINING_TIME_IDX = 266
const val RANK_IDX = 288
const val ABILITY_RARITY_IDX = 290
const val ABILITY_TYPE_IDX = 292
const val ABILITY_BRANCH_IDX = 294
const val ABILITY_RESET_IDX = 296
const val ITEM_TYPE_IDX = 299
const val ITEM_MULTIPLIER_IDX = 300
const val ITEM_REMAINING_TIME_IDX = 301
const val OTP_START_IDX = 352
const val OTP_END_IDX = 359
const val OTP2_START_IDX = 368
const val OTP2_END_IDX = 375
// DEVICE
const val VITAL_POINTS_CURRENT_IDX = 160
const val FIMRWARE_VERSION_IDX = 380
}
// setCharacterInByteArray takes the BENfcCharacter and modifies the byte array with character
// data. At the time of writing this is used to write a parsed character into fresh unparsed
// device data when sending a character back to the device.
override fun setCharacterInByteArray(
character: NfcCharacter,
bytes: ByteArray
) {
val beCharacter = character as BENfcCharacter
beCharacter.appReserved1.copyInto(bytes,
APP_RESERVED_START, 0,
APP_RESERVED_SIZE
)
beCharacter.injuryStatus.ordinal.toUShort().toByteArray(bytes,
INJURY_STATUS_IDX, ByteOrder.BIG_ENDIAN)
for(i in 0..<APP_RESERVED_2_SIZE) {
val index = APP_RESERVED_2_START + 2*i
beCharacter.appReserved2[i].toByteArray(bytes, index, ByteOrder.BIG_ENDIAN)
}
beCharacter.charIndex.toByteArray(bytes, CHARACTER_INDEX_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.dimId.toByteArray(bytes, DIM_ID_IDX, ByteOrder.BIG_ENDIAN)
bytes[PHASE_IDX] = beCharacter.stage
bytes[ATTRIBUTE_IDX] = beCharacter.attribute.ordinal.toByte()
bytes[AGE_IN_DAYS_IDX] = beCharacter.ageInDays
beCharacter.getTrainingPp().toByteArray(bytes, TRAINING_PP_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.currentPhaseBattlesWon.toByteArray(bytes,
CURRENT_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.currentPhaseBattlesLost.toByteArray(bytes,
CURRENT_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.totalBattlesWon.toByteArray(bytes,
TOTAL_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.totalBattlesLost.toByteArray(bytes,
TOTAL_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN)
bytes[WIN_PCT_IDX] = beCharacter.getWinPercentage()
bytes[NEXT_ADVENTURE_MISSION_STAGE_IDX] = beCharacter.nextAdventureMissionStage
bytes[MOOD_IDX] = beCharacter.mood
bytes[ACTIVITY_LEVEL_IDX] = beCharacter.activityLevel
bytes[HEART_RATE_CURRENT_IDX] = beCharacter.heartRateCurrent.toByte()
beCharacter.vitalPoints.toByteArray(bytes, VITAL_POINTS_IDX, ByteOrder.BIG_ENDIAN)
bytes[ITEM_EFFECT_MENTAL_STATE_VALUE_IDX] = beCharacter.itemEffectMentalStateValue
bytes[ITEM_EFFECT_MENTAL_STATE_MINUTES_REMAINING_IDX] = beCharacter.itemEffectMentalStateMinutesRemaining
bytes[ITEM_EFFECT_ACTIVITY_LEVEL_VALUE_IDX] = beCharacter.itemEffectActivityLevelValue
bytes[ITEM_EFFECT_ACTIVITY_LEVEL_MINUTES_REMAINING_IDX] = beCharacter.itemEffectActivityLevelMinutesRemaining
bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_VALUE_IDX] = beCharacter.itemEffectVitalPointsChangeValue
bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_MINUTES_REMAINING_IDX] = beCharacter.itemEffectVitalPointsChangeMinutesRemaining
beCharacter.transformationCountdown.toByteArray(bytes,
TRANSFORMATION_COUNT_DOWN_IDX, ByteOrder.BIG_ENDIAN)
transformationHistoryToByteArray(beCharacter.transformationHistory, bytes)
beCharacter.trainingHp.toByteArray(bytes, TRAINING_HP_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.trainingAp.toByteArray(bytes, TRAINING_AP_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.trainingBp.toByteArray(bytes, TRAINING_BP_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.remainingTrainingTimeInMinutes.toByteArray(bytes,
TRAINING_TIME_IDX, ByteOrder.BIG_ENDIAN)
bytes[ABILITY_RARITY_IDX] = beCharacter.abilityRarity.ordinal.toByte()
beCharacter.abilityType.toByteArray(bytes, ABILITY_TYPE_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.abilityBranch.toByteArray(bytes, ABILITY_BRANCH_IDX, ByteOrder.BIG_ENDIAN)
bytes[ABILITY_RESET_IDX] = beCharacter.abilityReset
bytes[RANK_IDX] = beCharacter.rank
bytes[ITEM_TYPE_IDX] = beCharacter.itemType
bytes[ITEM_MULTIPLIER_IDX] = beCharacter.itemMultiplier
bytes[ITEM_REMAINING_TIME_IDX] = beCharacter.itemRemainingTime
beCharacter.otp0.copyInto(bytes, OTP_START_IDX, 0, beCharacter.otp0.size)
beCharacter.otp1.copyInto(bytes, OTP2_START_IDX, 0, beCharacter.otp1.size)
bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX] = beCharacter.characterCreationFirmwareVersion.majorVersion
bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX+1] = beCharacter.characterCreationFirmwareVersion.minorVersion
}
// finalizeByteArrayFormat finalizes the byte array for BE NFC format by setting all the
// checksums, and duplicating the duplicate memory pages.
override fun finalizeByteArrayFormat(bytes: ByteArray) {
checksumCalculator.recalculateChecksums(bytes)
performPageBlockDuplications(bytes)
}
override fun getOperationCommandBytes(header: NfcHeader, operation: Byte): ByteArray {
return byteArrayOf(TagCommunicator.NFC_WRITE_COMMAND, OPERATION_PAGE, header.status, operation, header.dimIdBytes[0], header.dimIdBytes[1])
}
// parses a BENfcCharacter from a ByteArray produced by the TagCommunicator
override fun parseNfcCharacter(bytes: ByteArray): BENfcCharacter {
return BENfcCharacter(
appReserved1 = bytes.sliceArray(APP_RESERVED_START..<(APP_RESERVED_START + APP_RESERVED_SIZE)),
injuryStatus = NfcCharacter.InjuryStatus.entries[bytes.getUInt16(INJURY_STATUS_IDX, ByteOrder.BIG_ENDIAN).toInt()],
appReserved2 = bytes.copyIntoUShortArray(APP_RESERVED_2_START, APP_RESERVED_2_SIZE),
charIndex = bytes.getUInt16(CHARACTER_INDEX_IDX, ByteOrder.BIG_ENDIAN),
dimId = bytes.getUInt16(DIM_ID_IDX, ByteOrder.BIG_ENDIAN),
stage = bytes[PHASE_IDX],
attribute = NfcCharacter.Attribute.entries[bytes[ATTRIBUTE_IDX].toInt()],
ageInDays = bytes[AGE_IN_DAYS_IDX],
trainingPp = bytes.getUInt16(TRAINING_PP_IDX, ByteOrder.BIG_ENDIAN),
currentPhaseBattlesWon = bytes.getUInt16(CURRENT_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN),
currentPhaseBattlesLost = bytes.getUInt16(CURRENT_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN),
totalBattlesWon = bytes.getUInt16(TOTAL_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN),
totalBattlesLost = bytes.getUInt16(TOTAL_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN),
nextAdventureMissionStage = bytes[NEXT_ADVENTURE_MISSION_STAGE_IDX],
mood = bytes[MOOD_IDX],
activityLevel = bytes[ACTIVITY_LEVEL_IDX],
heartRateCurrent = bytes[HEART_RATE_CURRENT_IDX].toUByte(),
vitalPoints = bytes.getUInt16(VITAL_POINTS_IDX, ByteOrder.BIG_ENDIAN),
itemEffectMentalStateValue = bytes[ITEM_EFFECT_MENTAL_STATE_VALUE_IDX],
itemEffectMentalStateMinutesRemaining = bytes[ITEM_EFFECT_MENTAL_STATE_MINUTES_REMAINING_IDX],
itemEffectActivityLevelValue = bytes[ITEM_EFFECT_ACTIVITY_LEVEL_VALUE_IDX],
itemEffectActivityLevelMinutesRemaining = bytes[ITEM_EFFECT_ACTIVITY_LEVEL_MINUTES_REMAINING_IDX],
itemEffectVitalPointsChangeValue = bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_VALUE_IDX],
itemEffectVitalPointsChangeMinutesRemaining = bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_MINUTES_REMAINING_IDX],
transformationCountdownInMinutes = bytes.getUInt16(TRANSFORMATION_COUNT_DOWN_IDX, ByteOrder.BIG_ENDIAN),
transformationHistory = buildTransformationHistory(bytes),
trainingHp = bytes.getUInt16(TRAINING_HP_IDX, ByteOrder.BIG_ENDIAN),
trainingAp = bytes.getUInt16(TRAINING_AP_IDX, ByteOrder.BIG_ENDIAN),
trainingBp = bytes.getUInt16(TRAINING_BP_IDX, ByteOrder.BIG_ENDIAN),
remainingTrainingTimeInMinutes = bytes.getUInt16(TRAINING_TIME_IDX, ByteOrder.BIG_ENDIAN),
abilityRarity = NfcCharacter.AbilityRarity.entries[bytes[ABILITY_RARITY_IDX].toInt()],
abilityType = bytes.getUInt16(ABILITY_TYPE_IDX, ByteOrder.BIG_ENDIAN),
abilityBranch = bytes.getUInt16(ABILITY_BRANCH_IDX, ByteOrder.BIG_ENDIAN),
abilityReset = bytes[ABILITY_RESET_IDX],
rank = bytes[RANK_IDX],
itemType = bytes[ITEM_TYPE_IDX],
itemMultiplier = bytes[ITEM_MULTIPLIER_IDX],
itemRemainingTime = bytes[ITEM_REMAINING_TIME_IDX],
otp0 = bytes.sliceArray(OTP_START_IDX..OTP_END_IDX),
otp1 = bytes.sliceArray(OTP2_START_IDX..OTP2_END_IDX),
characterCreationFirmwareVersion = FirmwareVersion(
majorVersion = bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX],
minorVersion = bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX+1]),
)
}
override fun parseHeader(headerBytes: ByteArray): NfcHeader {
Log.i(TagCommunicator.TAG, "Bytes in header: ${headerBytes.size}")
val header = NfcHeader(
deviceId = DeviceType.VitalBraceletBEDeviceType,
deviceSubType = DeviceSubType.Original,
vbCompatibleTagIdentifier = headerBytes.sliceArray(0..3), // this is a magic number used to verify that the tag is a VB.
status = headerBytes[8],
operation = headerBytes[9],
dimIdBytes = headerBytes.sliceArray(10..11),
appFlag = headerBytes[12],
nonce = headerBytes.sliceArray(13..15)
)
Log.i(TagCommunicator.TAG, "Header: $header")
return header
}
// a block being 4 pages
private val firstIndicesOfBlocksToCopy = intArrayOf(32, 64, 96, 128, 256, 416)
private fun performPageBlockDuplications(data: ByteArray) {
for (firstIndex in firstIndicesOfBlocksToCopy) {
for (i in firstIndex..firstIndex + 15) {
data[i+16] = data[i]
}
}
}
private fun transformationHistoryToByteArray(transformationHistory: Array<NfcCharacter.Transformation>, bytes: ByteArray) {
if (transformationHistory.size != 8) {
throw IllegalArgumentException("Transformation History must be exactly size 8")
}
for (phase in 0..<transformationHistory.size) {
var rootIdx = phase*4 + TRANSFORMATION_HISTORY_START
if (phase > 2) {
rootIdx += 4 // we skip 220-223 for some reason
}
if (phase > 5) {
rootIdx += 4 // we skip 236-239 for some reason
}
bytes[rootIdx] = transformationHistory[phase].toCharIndex
bytes[rootIdx+1] = transformationHistory[phase].yearsSince1988
bytes[rootIdx+2] = transformationHistory[phase].month
bytes[rootIdx+3] = transformationHistory[phase].day
}
}
private fun buildTransformationHistory(data: ByteArray): Array<NfcCharacter.Transformation> {
val transformationHistory = Array<NfcCharacter.Transformation>(8) { phase ->
var rootIdx = phase*4 + TRANSFORMATION_HISTORY_START
if (phase > 2) {
rootIdx += 4 // we skip 220-223 for some reason
}
if (phase > 5) {
rootIdx += 4 // we skip 236-239 for some reason
}
NfcCharacter.Transformation(
toCharIndex = data[rootIdx],
yearsSince1988 = data[rootIdx+1],
month = data[rootIdx+2],
day = data[rootIdx+3]
)
}
return transformationHistory
}
}
@@ -1,20 +0,0 @@
package com.github.cfogrady.vbnfc.be
import com.github.cfogrady.vbnfc.data.NfcDevice
import java.util.BitSet
class BENfcDevice(
val gender: Gender,
val registedDims: ByteArray,
val currDays: Byte,
):
NfcDevice(
BitSet(),
) {
enum class Gender {
Male,
Female
}
}
@@ -1,3 +0,0 @@
package com.github.cfogrady.vbnfc.be
data class FirmwareVersion(val majorVersion: Byte, val minorVersion: Byte)
@@ -1,12 +0,0 @@
package com.github.cfogrady.vbnfc.data
class DeviceSubType {
companion object {
const val Original: UShort = 1u
const val FirstRevision: UShort = 2u
const val SecondRevision: UShort = 3u
const val DigiviceV: UShort = 4u
const val DigiviceVSecondRevision: UShort = 5u
const val VitalHero: UShort = 6u
}
}
@@ -1,9 +0,0 @@
package com.github.cfogrady.vbnfc.data
class DeviceType {
companion object {
const val VitalSeriesDeviceType: UShort = 2u
const val VitalCharactersDeviceType: UShort = 3u
const val VitalBraceletBEDeviceType: UShort = 4u
}
}
@@ -1,147 +0,0 @@
package com.github.cfogrady.vbnfc.data
import java.util.Objects
open class NfcCharacter(
val dimId: UShort,
var charIndex: UShort,
var stage: Byte,
var attribute: Attribute,
var ageInDays: Byte,
var nextAdventureMissionStage: Byte, // next adventure mission stage on the character's dim
var mood: Byte,
var vitalPoints: UShort,
var transformationCountdown: UShort,
var injuryStatus: InjuryStatus,
var trophies: UShort,
var currentPhaseBattlesWon: UShort,
var currentPhaseBattlesLost: UShort,
var totalBattlesWon: UShort,
var totalBattlesLost: UShort,
var activityLevel: Byte,
var heartRateCurrent: UByte,
var transformationHistory: Array<Transformation>
) {
data class Transformation(
val toCharIndex: Byte,
val yearsSince1988: Byte,
val month: Byte,
val day: Byte)
enum class AbilityRarity {
None,
Common,
Rare,
SuperRare,
SuperSuperRare,
UltraRare,
}
enum class Attribute {
None,
Virus,
Data,
Vaccine,
Free
}
enum class InjuryStatus {
None,
Injury,
InjuryHealed,
InjuryTwo,
InjuryTwoHealed,
InjuryThree,
InjuryThreeHealed,
InjuryFour,
}
fun getTransformationHistoryString(separator: String = System.lineSeparator()): String {
val builder = StringBuilder()
for(i in transformationHistory.indices) {
builder.append(transformationHistory[i])
if(i != transformationHistory.size-1) {
builder.append(separator)
}
}
return builder.toString()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as NfcCharacter
if (dimId != other.dimId) return false
if (charIndex != other.charIndex) return false
if (stage != other.stage) return false
if (attribute != other.attribute) return false
if (ageInDays != other.ageInDays) return false
if (nextAdventureMissionStage != other.nextAdventureMissionStage) return false
if (mood != other.mood) return false
if (vitalPoints != other.vitalPoints) return false
if (transformationCountdown != other.transformationCountdown) return false
if (injuryStatus != other.injuryStatus) return false
if (trophies != other.trophies) return false
if (currentPhaseBattlesWon != other.currentPhaseBattlesWon) return false
if (currentPhaseBattlesLost != other.currentPhaseBattlesLost) return false
if (totalBattlesWon != other.totalBattlesWon) return false
if (totalBattlesLost != other.totalBattlesLost) return false
if (activityLevel != other.activityLevel) return false
if (heartRateCurrent != other.heartRateCurrent) return false
if (!transformationHistory.contentEquals(other.transformationHistory)) return false
return true
}
override fun hashCode(): Int {
return Objects.hash(
dimId,
charIndex,
stage,
attribute,
ageInDays,
nextAdventureMissionStage,
mood,
vitalPoints,
transformationCountdown,
injuryStatus,
trophies,
currentPhaseBattlesWon,
currentPhaseBattlesLost,
totalBattlesWon,
totalBattlesLost,
activityLevel,
heartRateCurrent,
transformationHistory.contentHashCode()
)
}
override fun toString(): String {
return """NfcCharacter(
dimId=$dimId,
charIndex=$charIndex,
stage=$stage,
attribute=$attribute,
ageInDays=$ageInDays,
nextAdventureMissionStage=$nextAdventureMissionStage,
mood=$mood,
vitalPoints=$vitalPoints,
transformationCountdown=$transformationCountdown,
injuryStatus=$injuryStatus,
trophies=$trophies,
currentPhaseBattlesWon=$currentPhaseBattlesWon,
currentPhaseBattlesLost=$currentPhaseBattlesLost,
totalBattlesWon=$totalBattlesWon,
totalBattlesLost=$totalBattlesLost,
activityLevel=$activityLevel,
heartRateCurrent=$heartRateCurrent,
transformationHistory=${transformationHistory.contentToString()}
)"""
}
}
@@ -1,3 +0,0 @@
package com.github.cfogrady.vbnfc.data
class NfcData(val nfcCharacter: NfcCharacter, val nfcDevice: NfcDevice)
@@ -1,9 +0,0 @@
package com.github.cfogrady.vbnfc.data
import java.util.BitSet
open class NfcDevice(private val registeredDims: BitSet) {
fun isDimRegistered(dimId: UShort): Boolean {
return registeredDims[dimId.toInt()]
}
}
@@ -1,26 +0,0 @@
package com.github.cfogrady.vbnfc.data
import com.github.cfogrady.vbnfc.NfcDataTranslator
import com.github.cfogrady.vbnfc.getUInt16
import com.github.cfogrady.vbnfc.toByteArray
import java.nio.ByteOrder
open class NfcHeader (
val deviceId: UShort,
val deviceSubType: UShort,
val vbCompatibleTagIdentifier: ByteArray, // this is a magic number used to verify that the tag is a VB.
val status: Byte,
val operation: Byte,
var dimIdBytes: ByteArray,
val appFlag: Byte,
val nonce: ByteArray,
) {
fun getDimId(): UShort {
return dimIdBytes.getUInt16(0, ByteOrder.BIG_ENDIAN)
}
fun setDimId(dimId: UShort) {
dimIdBytes = dimId.toByteArray(ByteOrder.BIG_ENDIAN)
}
}
@@ -1,45 +0,0 @@
package com.github.cfogrady.vbnfc.vb
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.NfcDataTranslator
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.data.DeviceSubType
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.data.NfcHeader
import com.github.cfogrady.vbnfc.getUInt16
class VBNfcDataTranslator(override val cryptographicTransformer: CryptographicTransformer) : NfcDataTranslator {
companion object {
const val OPERATION_PAGE: Byte = 0x6
}
override fun finalizeByteArrayFormat(bytes: ByteArray) {
TODO("Not yet implemented")
}
override fun getOperationCommandBytes(header: NfcHeader, operation: Byte): ByteArray {
val vbHeader = header as VBNfcHeader
return byteArrayOf(TagCommunicator.NFC_WRITE_COMMAND, OPERATION_PAGE, header.status, header.dimIdBytes[1], operation, vbHeader.reserved)
}
override fun parseNfcCharacter(bytes: ByteArray): NfcCharacter {
TODO("Not yet implemented")
}
override fun parseHeader(headerBytes: ByteArray): NfcHeader {
val header = VBNfcHeader(
deviceType = DeviceType.VitalSeriesDeviceType,
deviceSubType = headerBytes.getUInt16(6),
vbCompatibleTagIdentifier = headerBytes.sliceArray(0..3), // this is a magic number used to verify that the tag is a VB.
status = headerBytes[8],
dimId = headerBytes[9],
operation = headerBytes[10],
reserved = headerBytes[11],
appFlag = headerBytes[12],
nonce = headerBytes.sliceArray(13..15)
)
return header
}
}
@@ -1,25 +0,0 @@
package com.github.cfogrady.vbnfc.vb
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcHeader
class VBNfcHeader(
deviceType: UShort,
deviceSubType: UShort,
vbCompatibleTagIdentifier: ByteArray,
status: Byte,
operation: Byte,
dimId: Byte,
val reserved: Byte,
appFlag: Byte,
nonce: ByteArray
) : NfcHeader(
deviceId = deviceType,
deviceSubType = deviceSubType,
vbCompatibleTagIdentifier = vbCompatibleTagIdentifier,
status = status,
operation = operation,
dimIdBytes = byteArrayOf(0, dimId),
appFlag = appFlag,
nonce = nonce,
)
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="substitutionArray" />
</resources>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="password1" />
<string name="password2" />
<string name="decryptionKey" />
</resources>
@@ -1,50 +0,0 @@
package com.github.cfogrady.vbnfc
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
// This class allows for creating new test keys
class CryptographicTransformerHelper {
companion object {
fun generateAesKey(): String {
val combinedKey = ByteArray(24)
for (i in combinedKey.indices) {
combinedKey[i] = Random.nextInt(48, 58 + 26).toByte()
if(combinedKey[i] > 57) {
combinedKey[i] = (combinedKey[i] + 7).toByte()
}
}
return combinedKey.toString(StandardCharsets.UTF_8)
}
fun generateHMacKey(aesKey: String, hmacKey: String = generateRandomPlainTextHmacKey()): String {
val hmacKeyData = hmacKey.toByteArray(StandardCharsets.UTF_8)
val encryptedHmacKey = encryptAesCbcPkcs5Padding(aesKey, hmacKeyData)
return Base64.getEncoder().encodeToString(encryptedHmacKey)
}
private fun generateRandomPlainTextHmacKey(): String {
val key = ByteArray(4)
for (i in key.indices) {
key[i] = Random.nextInt(33, 126).toByte()
}
return key.toString(StandardCharsets.UTF_8)
}
private fun encryptAesCbcPkcs5Padding(key: String, data: ByteArray): ByteArray {
val keyBytes = key.toByteArray(StandardCharsets.UTF_8)
val rightSizedKey = keyBytes.copyOf(32)
val ivBytes = keyBytes.copyOfRange(key.length - 16, key.length)
val secretKeySpec = SecretKeySpec(rightSizedKey, "AES")
val ivParameterSpec = IvParameterSpec(ivBytes)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
return cipher.doFinal(data)
}
}
}
@@ -1,56 +0,0 @@
package com.github.cfogrady.vbnfc
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Assert
import org.junit.Test
class CryptographicTransformerTest {
val testTagId = byteArrayOf(0x04, 0x40, 0xaf.toByte(), 0xa2.toByte(), 0xee.toByte(), 0x0f, 0x90.toByte())
val testAesKey = "8A4PEGIXJS454EFRTX9F5PCT"
val testHmacKey1 = "40nz2LdPI99D+x748XmQmw=="
val testHmacKey2 = "5Jz9lWtNg28qxqIBoR5kLw=="
val testSubstitutionCipher = intArrayOf(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
@OptIn(ExperimentalStdlibApi::class)
@Test
fun createPasswordCreatesExpectedPassword() {
mockkStatic(android.util.Log::class)
every { android.util.Log.i(any<String>(), any<String>()) } answers {
val message = it.invocation.args[1] as String
println(message)
1
}
val cryptographicTransformer = CryptographicTransformer(testHmacKey1, testHmacKey2, testAesKey, testSubstitutionCipher)
val result = cryptographicTransformer.createNfcPassword(testTagId)
val expected = "a3c83dd7"
Assert.assertEquals(expected, result.toHexString())
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun dataEncryptionAndDecryptionWorks() {
val cryptographicTransformer = CryptographicTransformer(testHmacKey1, testHmacKey2, testAesKey, testSubstitutionCipher)
val characters = listOf(
"000000000000000000000000000000000000000000000000000000000000000010400010040010001000000000001094104000100400100010000000000010940000000000000000000400840203008d0000000000000000000400840203008d00000006000300060003000001010014000000060003000600030000010100140156025309850000000000000000d6100156025309850000000000000000d610000000000000280000000000000000280000000000000000030c08302402279424022624022524022424022324020151002402290124022904240229000000f2ffffffffffffffffffffffff000000f4ffffffffffffffff00000000000000f80000000000000000000014860000009a0000000000000000000014860000009a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010101000000000000008702020202020202020000000001010033000000000000000000000000000000000000000000000000000000000000000014c5400000000000000000000000001914c540000000000000000000000000190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"000000000000000000000000000000000000000000000000000000000000000010400010040010001000000000001094104000100400100010000000000010940000000000000000000500820302008c0000000000000000000500820302008c000b00030002000b000b000001010028000b00030002000b000b0000010100280464028b04b4000000000000000308b80464028b04b4000000000000000308b80474000000001500000000000000008d0002000000000000000000002401062d240105240104240103240102240101c80024010601240106042401060000008605240116ffffffffffffffff00000038ffffffffffffffff00000000000000f80005000f000a00000000046b0000008d0005000f000a00000000046b0000008d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030303030303030300000000000000210404040404040404000000000101003a000000000000000000000000000000000000000000000000000000000000000014c5400000000000000000000000001914c540000000000000000000000000190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
)
val expectedEncryptions = listOf(
"74b46e2d78292ce251a84e644f5451dd4aa2c6d782a9aeef2c3229103c7a26e5d9c2c53d2f63077a1cf9ae18efc382edca9e50d7d861e2927cd9df2494a0772ff1b8791bae7883a862d218559277db6a9665f18da0f1caa92ccc903dbc80e66367ee39b2f243cc0b5bfcc2131dfeb9d361179d4033517c4ebbceea15ab9bfb27a9a05cb45e5dfe37287a010db3bcfd9a6f1cc54a31ea7da193dbd341e83997ad80713d5fd19c7d76a6dbedfb25049ab5472e4094d0949f19c853b2ac6c6827412904fd1683fae61ea1a64508e9243e04bdfdee63ff98321204fd4fd2a17112c0bd5aa7c11986ea7bc00110dedea2d0644ea5ea3f6e928f8c6c98107eeedbfd1b774d07df237dc294f3b8fccaa8ab2e1c59de2c695f367214f55accc2e43ca0b2c021161d3c8b9ea2405717d6884d089d848d82b37a9ed711a8e9809336d05bab0091face97671e9260d4ae741e94ef1f8d950ff390dd05bfa28adb6b72a8214192be530af26db6abda69dce0707d3d92f44beb1f01f24d7c1123ad25d37349bf7d5777723f1acb56d2f96ba435ffa505c3ae3d270c85bcb64aabac0d15d8128f7fe34cc303a74f8d42871a25d452a1ef033b824b17e51b808b5363f2f1da5a57c3fcc881b57028e76155f6d49e35e43bd142747c3a4a14217fa8600a6e0d3cdbc6b5598510023ad0897d38ca04e78a78e198ee76e784d6337ad8fa65e4b617ea11433fb3d9d7d3269a7b5e1ff9a4cd723f3fc4f81768816619fadf21b5276f31d704bba1a3a9afb0363cca3ddb272bdced2d252e21824d2828b7c36cdd37202cd5a6b5224e505f87188d3c63af33c8916aa8ae0116dd1028c370236591a4413559fe40d14dcdf72959a122def9b4c6cf5d928142a66b0dcf0ffa5d447b53a8661e3578a49d632e0285d180814116a3e78fc00fc01106a248af8afa329a69054b827e41a62abdc65074554fa2f07b773d56bae73252efa374f47397c1d36e056eb9d32b6dcf18f40669a5b23723c475c50c5ba4154b550c67dd90d4a6919686c69fdcfd7d4d988a0df5e420b6240068ae8aa07c88c38bac3b6ce7a87c8df0f540cc3c5be2180b70127c523102ed9be2cfb25a032089d47d05db6dbde2758239ad54b94756906922ed7e7c4ecf6b256c658417fa8003100c72de1dee627c1a0c670dbc91596e4dc0d4393eb24884b41979e8ff3d5b0583416750d1d856a2a689cd",
"74b46e2d78292ce251a84e644f5451dd4aa2c6d782a9aeef2c3229103c7a26e5d9c2c53d2f63077a1cf9ae18efc382edca9e50d7d861e2927cd9df2494a0772ff1b8791bae7883a862d318539376db6b9665f18da0f1caa92ccd903bbd81e66267e539b7f242cc065bf4c2131dfeb9ef611c9d4533507c43bbc6ea15ab9bfb1bac925c6c536cfe37287a010db3bf23326a2ec5923cdb7da193dbd341e83a490584053d5fd19c4076a6dbedfb25049a10472c4094d0949f19cb5fba9c6c6b06f82907de1680dbe61d86a64629e9273e9dbdfded4cff98313d04fd4cfda17112b4478159281986ea7bc00110dedea2d0a84ea5ea3f6e928f8c6c98107eeedbfd1b774807d02377c294f3b8ec27a8ab2e0b59db2c665f3c7214f55adc2fe43ca0a5c021161d3c8b9ea2405717d6884d089d848d82b37a9ed711a8e9809336d05bab0091face97671e9260d4ae741e94ef1f8d950ff390dd05bfa28adb6b72a8214190bc5108f06fb4a9da69dce0707d3d34f24ded1907f44b7a1123ad25d37349b67d5777723f1acb56d2f96ba435ffa505c3ae3d270c85bcb64aabac0d15d8128f7fe34cc303a74f8d42871a25d452a1ef033b824b17e51b808b5363f2f1da5a57c3fcc881b57028e76155f6d49e35e43bd142747c3a4a14217fa8600a6e0d3cdbc6b5598510023ad0897d38ca04e78a78e198ee76e784d6337ad8fa65e4b617ea11433fb3d9d7d3269a7b5e1ff9a4cd723f3fc4f81768816619fadf21b5276f31d704bba1a3a9afb0363cca3ddb272bdced2d252e21824d2828b7c36cdd37202cd5a6b5224e505f87188d3c63af33c8916aa8ae0116dd1028c370236591a4413559fe40d14dcdf72959a122def9b4c6cf5d928142a66b0dcf0ffa5d447b53a8661e3578a49d632e0285d180814116a3e78fc00fc01106a248af8afa329a69054b827e41a62abdc65074554fa2f07b773d56bae73252efa374f47397c1d36e056eb9d32b6dcf18f40669a5b23723c475c50c5ba4154b550c67dd90d4a6919686c69fdcfd7d4d988a0df5e420b6240068ae8aa07c88c38bac3b6ce7a87c8df0f540cc3c5be2180b70127c523102ed9be2cfb25a032089d47d05db6dbde2758239ad54b94756906922ed7e7c4ecf6b256c658417fa8003100c72de1dee627c1a0c670dbc91596e4dc0d4393eb24884b41979e8ff3d5b0583416750d1d856a2a689cd",
)
for (i in characters.indices) {
val encrypted = cryptographicTransformer.encryptData(characters[i].hexToByteArray(), testTagId)
Assert.assertEquals(expectedEncryptions[i], encrypted.toHexString())
val decrypted = cryptographicTransformer.decryptData(encrypted, testTagId)
Assert.assertEquals(characters[i], decrypted.toHexString())
}
}
}
@@ -1,73 +0,0 @@
package com.github.cfogrady.vbnfc.be
import com.github.cfogrady.vbnfc.ChecksumCalculator
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.data.NfcCharacter
import io.mockk.mockkClass
import org.junit.Assert
import org.junit.Test
class BENfcDataTranslatorTest {
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testNfcCharacterParsing() {
val nfcBytes = "000000000000000000000000000000000000000000000000000000000000000010400010040010001000000000001094104000100400100010000000000010940000000000000000000500820302008c0000000000000000000500820302008c000b00030002000b000b000001010028000b00030002000b000b0000010100280464028b04b4000000000000000308b80464028b04b4000000000000000308b80474000000001500000000000000008d0002000000000000000000002401062d240105240104240103240102240101c80024010601240106042401060000008605240116ffffffffffffffff00000038ffffffffffffffff00000000000000f80005000f000a00000000046b0000008d0005000f000a00000000046b0000008d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010100000000000000210202020202020202000000000101003a000000000000000000000000000000000000000000000000000000000000000014c5400000000000000000000000001914c540000000000000000000000000190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".hexToByteArray()
val mockCryptographicTransformer = mockkClass(CryptographicTransformer::class)
val checksumCalculator = ChecksumCalculator()
val beNfcDataTranslator = BENfcDataTranslator(mockCryptographicTransformer, checksumCalculator)
val character = beNfcDataTranslator.parseNfcCharacter(nfcBytes)
val expectedCharacter = BENfcCharacter(
dimId = 130u,
charIndex = 5u,
stage = 3,
attribute = NfcCharacter.Attribute.Data,
ageInDays = 0,
mood = 100,
characterCreationFirmwareVersion = FirmwareVersion(1, 1),
nextAdventureMissionStage = 4,
vitalPoints = 1204u,
transformationCountdownInMinutes = 776u,
injuryStatus = NfcCharacter.InjuryStatus.None,
trainingPp = 11u,
currentPhaseBattlesWon = 3u,
currentPhaseBattlesLost = 2u,
totalBattlesWon = 11u,
totalBattlesLost = 11u,
activityLevel = 2,
heartRateCurrent = 139u,
transformationHistory = arrayOf(NfcCharacter.Transformation(0, 36, 1, 6),
NfcCharacter.Transformation(1, 36, 1, 6),
NfcCharacter.Transformation(4, 36, 1, 6),
NfcCharacter.Transformation(5, 36, 1, 22),
NfcCharacter.Transformation(-1, -1, -1, -1),
NfcCharacter.Transformation(-1, -1, -1, -1),
NfcCharacter.Transformation(-1, -1, -1, -1),
NfcCharacter.Transformation(-1, -1, -1, -1),
),
trainingHp = 5u,
trainingAp = 15u,
trainingBp = 10u,
remainingTrainingTimeInMinutes = 1131u,
itemEffectMentalStateValue = 0,
itemEffectMentalStateMinutesRemaining = 0,
itemEffectActivityLevelValue = 0,
itemEffectActivityLevelMinutesRemaining = 0,
itemEffectVitalPointsChangeValue = 0,
itemEffectVitalPointsChangeMinutesRemaining = 0,
abilityRarity = NfcCharacter.AbilityRarity.None,
abilityType = 0u,
abilityBranch = 0u,
abilityReset = 0,
rank = 0,
itemType = 0,
itemMultiplier = 0,
itemRemainingTime = 0,
appReserved1 = byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
appReserved2 = arrayOf(0u, 0u, 0u),
otp0 = "0101010101010101".hexToByteArray(),
otp1 = "0202020202020202".hexToByteArray()
)
Assert.assertEquals(expectedCharacter, character)
}
}