~cytrogen/vbhelper

84a1c1af72cae5cd4bb3ec95cc49fbbe8ce62c89 — Nacho 1 year, 3 months ago 4e3ebd4
Initial commit
82 files changed, 3219 insertions(+), 0 deletions(-)

A app/.gitignore
A app/build.gradle.kts
A app/proguard-rules.pro
A app/src/androidTest/java/com/github/nacabaro/vbhelper/ExampleInstrumentedTest.kt
A app/src/main/AndroidManifest.xml
A app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt
A app/src/main/java/com/github/nacabaro/vbhelper/domain/Dim.kt
A app/src/main/java/com/github/nacabaro/vbhelper/domain/DimProgress.kt
A app/src/main/java/com/github/nacabaro/vbhelper/domain/Evolutions.kt
A app/src/main/java/com/github/nacabaro/vbhelper/domain/Mon.kt
A app/src/main/java/com/github/nacabaro/vbhelper/domain/User.kt
A app/src/main/java/com/github/nacabaro/vbhelper/domain/UserHealthData.kt
A app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonsters.kt
A app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonstersSpecialMissions.kt
A app/src/main/java/com/github/nacabaro/vbhelper/domain/UserStepsData.kt
A app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Color.kt
A app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Theme.kt
A app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Type.kt
A app/src/main/res/drawable/ic_launcher_background.xml
A app/src/main/res/drawable/ic_launcher_foreground.xml
A app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
A app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
A app/src/main/res/mipmap-hdpi/ic_launcher.webp
A app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
A app/src/main/res/mipmap-mdpi/ic_launcher.webp
A app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
A app/src/main/res/mipmap-xhdpi/ic_launcher.webp
A app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
A app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
A app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
A app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
A app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
A app/src/main/res/values/colors.xml
A app/src/main/res/values/strings.xml
A app/src/main/res/values/themes.xml
A app/src/main/res/xml/backup_rules.xml
A app/src/main/res/xml/data_extraction_rules.xml
A app/src/test/java/com/github/nacabaro/vbhelper/ExampleUnitTest.kt
A build.gradle.kts
A gradle.properties
A gradle/libs.versions.toml
A gradle/wrapper/gradle-wrapper.jar
A gradle/wrapper/gradle-wrapper.properties
A gradlew
A gradlew.bat
A settings.gradle.kts
A vb-nfc-reader/.gitignore
A vb-nfc-reader/.idea/.gitignore
A vb-nfc-reader/.idea/caches/deviceStreaming.xml
A vb-nfc-reader/.idea/gradle.xml
A vb-nfc-reader/.idea/misc.xml
A vb-nfc-reader/.idea/runConfigurations.xml
A vb-nfc-reader/build.gradle.kts
A vb-nfc-reader/consumer-rules.pro
A vb-nfc-reader/proguard-rules.pro
A vb-nfc-reader/src/androidTest/java/com/github/cfogrady/vbnfc/ExampleInstrumentedTest.kt
A vb-nfc-reader/src/main/AndroidManifest.xml
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ByteManipulation.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ChecksumCalculator.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/CryptographicTransformer.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslator.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslatorFactory.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/TagCommunicator.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/Utils.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcCharacter.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataFactory.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslator.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDevice.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/FirmwareVersion.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceSubType.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceType.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcCharacter.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcData.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcDevice.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcHeader.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcDataTranslator.kt
A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcHeader.kt
A vb-nfc-reader/src/main/res/values/arrays.xml
A vb-nfc-reader/src/main/res/values/strings.xml
A vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerHelper.kt
A vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerTest.kt
A vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslatorTest.kt
A app/.gitignore => app/.gitignore +1 -0
@@ 0,0 1,1 @@
/build
\ No newline at end of file

A app/build.gradle.kts => app/build.gradle.kts +63 -0
@@ 0,0 1,63 @@
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    id("com.google.devtools.ksp") version "2.0.21-1.0.27"
}

android {
    namespace = "com.github.nacabaro.vbhelper"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.github.nacabaro.vbhelper"
        minSdk = 28
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
    buildFeatures {
        compose = true
    }
}

dependencies {
    implementation(libs.androidx.room.runtime)
    implementation(project(":vb-nfc-reader"))
    ksp(libs.androidx.room.compiler)
    annotationProcessor(libs.androidx.room.compiler)
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)
    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
}
\ No newline at end of file

A app/proguard-rules.pro => app/proguard-rules.pro +21 -0
@@ 0,0 1,21 @@
# 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
\ No newline at end of file

A app/src/androidTest/java/com/github/nacabaro/vbhelper/ExampleInstrumentedTest.kt => app/src/androidTest/java/com/github/nacabaro/vbhelper/ExampleInstrumentedTest.kt +24 -0
@@ 0,0 1,24 @@
package com.github.nacabaro.vbhelper

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.nacabaro.vbhelper", appContext.packageName)
    }
}
\ No newline at end of file

A app/src/main/AndroidManifest.xml => app/src/main/AndroidManifest.xml +31 -0
@@ 0,0 1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.NFC" />
    <uses-feature android:name="android.hardware.nfc" android:required="true" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.VBHelper"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.VBHelper">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
\ No newline at end of file

A app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt => app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt +121 -0
@@ 0,0 1,121 @@
package com.github.nacabaro.vbhelper

import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.NfcA
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
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.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.github.nacabaro.vbhelper.R
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme

class MainActivity : ComponentActivity() {
    private lateinit var nfcAdapter: NfcAdapter
    private lateinit var deviceToCryptographicTransformers: Map<UShort, CryptographicTransformer>

    // EXTRACTED DIRECTLY FROM EXAMPLE APP
    override fun onCreate(savedInstanceState: Bundle?) {
        deviceToCryptographicTransformers = getMapOfCryptographicTransformers()

        val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
        if (maybeNfcAdapter == null) {
            Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show()
            finish()
            return
        }
        nfcAdapter = maybeNfcAdapter


        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            VBHelperTheme {

            }
        }
    }

    // EXTRACTED DIRECTLY FROM EXAMPLE APP
    private fun getMapOfCryptographicTransformers(): Map<UShort, CryptographicTransformer> {
        return mapOf(
            Pair(DeviceType.VitalBraceletBEDeviceType,
                CryptographicTransformer(readableHmacKey1 = resources.getString(com.github.cfogrady.vbnfc.R.string.password1),
                    readableHmacKey2 = resources.getString(com.github.cfogrady.vbnfc.R.string.password2),
                    aesKey = resources.getString(com.github.cfogrady.vbnfc.R.string.decryptionKey),
                    substitutionCipher = resources.getIntArray(com.github.cfogrady.vbnfc.R.array.substitutionArray))),
//            Pair(DeviceType.VitalSeriesDeviceType,
//                CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
//                    hmacKey2 = resources.getString(R.string.password2),
//                    decryptionKey = resources.getString(R.string.decryptionKey),
//                    substitutionCipher = resources.getIntArray(R.array.substitutionArray))),
//            Pair(DeviceType.VitalCharactersDeviceType,
//                CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
//                    hmacKey2 = resources.getString(R.string.password2),
//                    decryptionKey = resources.getString(R.string.decryptionKey),
//                    substitutionCipher = resources.getIntArray(R.array.substitutionArray)))
        )
    }

    // EXTRACTED DIRECTLY FROM EXAMPLE APP
    private fun showWirelessSettings() {
        Toast.makeText(this, "NFC must be enabled", Toast.LENGTH_SHORT).show()
        startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
    }

    // EXTRACTED DIRECTLY FROM EXAMPLE APP
    private fun buildOnReadTag(handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
        return { tag->
            val nfcData = NfcA.get(tag)
            if (nfcData == null) {
                runOnUiThread {
                    Toast.makeText(this, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
                }
            }
            nfcData.connect()
            nfcData.use {
                val tagCommunicator = TagCommunicator.getInstance(nfcData, deviceToCryptographicTransformers)
                val successText = handlerFunc(tagCommunicator)
                runOnUiThread {
                    Toast.makeText(this, successText, Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    // EXTRACTED DIRECTLY FROM EXAMPLE APP
    private fun handleTag(handlerFunc: (TagCommunicator)->String) {
        if (!nfcAdapter.isEnabled) {
            showWirelessSettings()
        } else {
            val options = Bundle()
            // Work around for some broken Nfc firmware implementations that poll the card too fast
            options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
            nfcAdapter.enableReaderMode(this, buildOnReadTag(handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
                options
            )
        }
    }

    // EXTRACTED DIRECTLY FROM EXAMPLE APP
    override fun onPause() {
        super.onPause()
        if (nfcAdapter.isEnabled) {
            nfcAdapter.disableReaderMode(this)
        }
    }
}

A app/src/main/java/com/github/nacabaro/vbhelper/domain/Dim.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/Dim.kt +12 -0
@@ 0,0 1,12 @@
package com.github.nacabaro.vbhelper.domain

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class Dim(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val name: String,
    val stageCount: Int
)

A app/src/main/java/com/github/nacabaro/vbhelper/domain/DimProgress.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/DimProgress.kt +28 -0
@@ 0,0 1,28 @@
package com.github.nacabaro.vbhelper.domain

import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = User::class,
            parentColumns = ["id"],
            childColumns = ["userId"],
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Dim::class,
            parentColumns = ["id"],
            childColumns = ["dimId"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class DimProgress(
    @PrimaryKey val dimId: Int,
    @PrimaryKey val userId: Int,
    val currentStage: Int,
    val unlocked: Boolean
)

A app/src/main/java/com/github/nacabaro/vbhelper/domain/Evolutions.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/Evolutions.kt +30 -0
@@ 0,0 1,30 @@
package com.github.nacabaro.vbhelper.domain

import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = Mon::class,
            parentColumns = ["id"],
            childColumns = ["monId"],
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Mon::class,
            parentColumns = ["id"],
            childColumns = ["nextMon"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class Evolutions(
    @PrimaryKey val monId: Int,
    @PrimaryKey val nextMon: Int,
    val trophies: Int,
    val vitals: Int,
    val totalBattles: Int,
    val winRate: Int // Does not need to be a floating point
)

A app/src/main/java/com/github/nacabaro/vbhelper/domain/Mon.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/Mon.kt +28 -0
@@ 0,0 1,28 @@
package com.github.nacabaro.vbhelper.domain

import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.ForeignKey

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = Dim::class,
            parentColumns = ["id"],
            childColumns = ["dimId"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class Mon (
    @PrimaryKey val id: Int,
    val dimId: Int,
    val monIndex: Int,
    val name: String,
    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
)
\ No newline at end of file

A app/src/main/java/com/github/nacabaro/vbhelper/domain/User.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/User.kt +11 -0
@@ 0,0 1,11 @@
package com.github.nacabaro.vbhelper.domain

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class User (
    @PrimaryKey(autoGenerate = true) val id: Int,
    val username: String,
    val gender: Boolean
)
\ No newline at end of file

A app/src/main/java/com/github/nacabaro/vbhelper/domain/UserHealthData.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/UserHealthData.kt +21 -0
@@ 0,0 1,21 @@
package com.github.nacabaro.vbhelper.domain

import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = User::class,
            parentColumns = ["id"],
            childColumns = ["userId"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class UserHealthData(
    @PrimaryKey val userId: Int,
    val tamerRank: Int, // Old VB thingy, will probably go unused at first
    val totalSteps: Int
)

A app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonsters.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonsters.kt +45 -0
@@ 0,0 1,45 @@
package com.github.nacabaro.vbhelper.domain

import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = User::class,
            parentColumns = ["id"],
            childColumns = ["userId"],
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Mon::class,
            parentColumns = ["id"],
            childColumns = ["monId"],
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = UserMonsters::class,
            parentColumns = ["id"],
            childColumns = ["previousStage"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class UserMonsters (
    @PrimaryKey(autoGenerate = true) val id: Int,
    val userId: Int,
    val monId: Int,
    val previousStage: Int?,
    val vitals: Int,
    val trophies: Int,
    val trainingAp: Int,
    val trainingBp: Int,
    val trainingHp: Int,
    val rank: Int, // Maybe use another enum (?)
    val ability: Int,  // Another enum (???)
    val evoTimerLeft: Int,  // Minutes!!
    val limitTimerLeft: Int,  // Minutes!!!!!
    val totalBattles: Int,
    val totalWins: Int
)
\ No newline at end of file

A app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonstersSpecialMissions.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonstersSpecialMissions.kt +33 -0
@@ 0,0 1,33 @@
package com.github.nacabaro.vbhelper.domain

import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = UserMonsters::class,
            parentColumns = ["id"],
            childColumns = ["monId"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class UserMonstersSpecialMissions(
    @PrimaryKey val monId: Int,
    val slot1: Int,
    val timeLeft1: Int,
    val progression1: Int,
    val slot2: Int,
    val timeLeft2: Int,
    val progression2: Int,
    val slot3: Int,
    val timeLeft3: Int,
    val progression3: Int,
    val slot4: Int,
    val timeLeft4: Int,
    val progression4: Int
)

// Not really proud of this one boss

A app/src/main/java/com/github/nacabaro/vbhelper/domain/UserStepsData.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/UserStepsData.kt +21 -0
@@ 0,0 1,21 @@
package com.github.nacabaro.vbhelper.domain

import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = User::class,
            parentColumns = ["id"],
            childColumns = ["userId"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class UserStepsData(
    @PrimaryKey val userId: Int,
    val day: Int, // Unix?
    val stepCount: Int
)

A app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Color.kt => app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Color.kt +11 -0
@@ 0,0 1,11 @@
package com.github.nacabaro.vbhelper.ui.theme

import androidx.compose.ui.graphics.Color

val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)

val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
\ No newline at end of file

A app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Theme.kt => app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Theme.kt +58 -0
@@ 0,0 1,58 @@
package com.github.nacabaro.vbhelper.ui.theme

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext

private val DarkColorScheme = darkColorScheme(
    primary = Purple80,
    secondary = PurpleGrey80,
    tertiary = Pink80
)

private val LightColorScheme = lightColorScheme(
    primary = Purple40,
    secondary = PurpleGrey40,
    tertiary = Pink40

    /* Other default colors to override
    background = Color(0xFFFFFBFE),
    surface = Color(0xFFFFFBFE),
    onPrimary = Color.White,
    onSecondary = Color.White,
    onTertiary = Color.White,
    onBackground = Color(0xFF1C1B1F),
    onSurface = Color(0xFF1C1B1F),
    */
)

@Composable
fun VBHelperTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}
\ No newline at end of file

A app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Type.kt => app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Type.kt +34 -0
@@ 0,0 1,34 @@
package com.github.nacabaro.vbhelper.ui.theme

import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

// Set of Material typography styles to start with
val Typography = Typography(
    bodyLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    )
    /* Other default text styles to override
    titleLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 22.sp,
        lineHeight = 28.sp,
        letterSpacing = 0.sp
    ),
    labelSmall = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 11.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.5.sp
    )
    */
)
\ No newline at end of file

A app/src/main/res/drawable/ic_launcher_background.xml => app/src/main/res/drawable/ic_launcher_background.xml +170 -0
@@ 0,0 1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillColor="#3DDC84"
        android:pathData="M0,0h108v108h-108z" />
    <path
        android:fillColor="#00000000"
        android:pathData="M9,0L9,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,0L19,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,0L29,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,0L39,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,0L49,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,0L59,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,0L69,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,0L79,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M89,0L89,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M99,0L99,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,9L108,9"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,19L108,19"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,29L108,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,39L108,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,49L108,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,59L108,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,69L108,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,79L108,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,89L108,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,99L108,99"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,29L89,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,39L89,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,49L89,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,59L89,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,69L89,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,79L89,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,19L29,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,19L39,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,19L49,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,19L59,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,19L69,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,19L79,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
</vector>

A app/src/main/res/drawable/ic_launcher_foreground.xml => app/src/main/res/drawable/ic_launcher_foreground.xml +30 -0
@@ 0,0 1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
        <aapt:attr name="android:fillColor">
            <gradient
                android:endX="85.84757"
                android:endY="92.4963"
                android:startX="42.9492"
                android:startY="49.59793"
                android:type="linear">
                <item
                    android:color="#44000000"
                    android:offset="0.0" />
                <item
                    android:color="#00000000"
                    android:offset="1.0" />
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:fillColor="#FFFFFF"
        android:fillType="nonZero"
        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
        android:strokeWidth="1"
        android:strokeColor="#00000000" />
</vector>
\ No newline at end of file

A app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml => app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
@@ 0,0 1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file

A app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml => app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
@@ 0,0 1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file

A app/src/main/res/mipmap-hdpi/ic_launcher.webp => app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
A app/src/main/res/mipmap-hdpi/ic_launcher_round.webp => app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
A app/src/main/res/mipmap-mdpi/ic_launcher.webp => app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
A app/src/main/res/mipmap-mdpi/ic_launcher_round.webp => app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
A app/src/main/res/mipmap-xhdpi/ic_launcher.webp => app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
A app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp => app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
A app/src/main/res/mipmap-xxhdpi/ic_launcher.webp => app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
A app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp => app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
A app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp => app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
A app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp => app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
A app/src/main/res/values/colors.xml => app/src/main/res/values/colors.xml +10 -0
@@ 0,0 1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
</resources>
\ No newline at end of file

A app/src/main/res/values/strings.xml => app/src/main/res/values/strings.xml +3 -0
@@ 0,0 1,3 @@
<resources>
    <string name="app_name">VBHelper</string>
</resources>
\ No newline at end of file

A app/src/main/res/values/themes.xml => app/src/main/res/values/themes.xml +5 -0
@@ 0,0 1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="Theme.VBHelper" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
\ No newline at end of file

A app/src/main/res/xml/backup_rules.xml => app/src/main/res/xml/backup_rules.xml +13 -0
@@ 0,0 1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
   Sample backup rules file; uncomment and customize as necessary.
   See https://developer.android.com/guide/topics/data/autobackup
   for details.
   Note: This file is ignored for devices older that API 31
   See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
    <!--
   <include domain="sharedpref" path="."/>
   <exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
\ No newline at end of file

A app/src/main/res/xml/data_extraction_rules.xml => app/src/main/res/xml/data_extraction_rules.xml +19 -0
@@ 0,0 1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
   Sample data extraction rules file; uncomment and customize as necessary.
   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
   for details.
-->
<data-extraction-rules>
    <cloud-backup>
        <!-- TODO: Use <include> and <exclude> to control what is backed up.
        <include .../>
        <exclude .../>
        -->
    </cloud-backup>
    <!--
    <device-transfer>
        <include .../>
        <exclude .../>
    </device-transfer>
    -->
</data-extraction-rules>
\ No newline at end of file

A app/src/test/java/com/github/nacabaro/vbhelper/ExampleUnitTest.kt => app/src/test/java/com/github/nacabaro/vbhelper/ExampleUnitTest.kt +17 -0
@@ 0,0 1,17 @@
package com.github.nacabaro.vbhelper

import org.junit.Test

import org.junit.Assert.*

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}
\ No newline at end of file

A build.gradle.kts => build.gradle.kts +6 -0
@@ 0,0 1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.kotlin.compose) apply false
}
\ No newline at end of file

A gradle.properties => gradle.properties +23 -0
@@ 0,0 1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
\ No newline at end of file

A gradle/libs.versions.toml => gradle/libs.versions.toml +35 -0
@@ 0,0 1,35 @@
[versions]
agp = "8.7.3"
kotlin = "2.0.0"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
composeBom = "2024.04.01"
roomRuntime = "2.6.1"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
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" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }


A gradle/wrapper/gradle-wrapper.jar => gradle/wrapper/gradle-wrapper.jar +0 -0
A gradle/wrapper/gradle-wrapper.properties => gradle/wrapper/gradle-wrapper.properties +6 -0
@@ 0,0 1,6 @@
#Tue Dec 24 10:55:57 UTC 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

A gradlew => gradlew +185 -0
@@ 0,0 1,185 @@
#!/usr/bin/env sh

#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn () {
    echo "$*"
}

die () {
    echo
    echo "$*"
    echo
    exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MINGW* )
    msys=true
    ;;
  NONSTOP* )
    nonstop=true
    ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`

    JAVACMD=`cygpath --unix "$JAVACMD"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=`expr $i + 1`
    done
    case $i in
        0) set -- ;;
        1) set -- "$args0" ;;
        2) set -- "$args0" "$args1" ;;
        3) set -- "$args0" "$args1" "$args2" ;;
        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

# Escape application args
save () {
    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
    echo " "
}
APP_ARGS=`save "$@"`

# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"

exec "$JAVACMD" "$@"

A gradlew.bat => gradlew.bat +89 -0
@@ 0,0 1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega

A settings.gradle.kts => settings.gradle.kts +24 -0
@@ 0,0 1,24 @@
pluginManagement {
    repositories {
        google {
            content {
                includeGroupByRegex("com\\.android.*")
                includeGroupByRegex("com\\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "VBHelper"
include(":app")
include(":vb-nfc-reader")

A vb-nfc-reader/.gitignore => vb-nfc-reader/.gitignore +1 -0
@@ 0,0 1,1 @@
/build
\ No newline at end of file

A vb-nfc-reader/.idea/.gitignore => vb-nfc-reader/.idea/.gitignore +3 -0
@@ 0,0 1,3 @@
# Default ignored files
/shelf/
/workspace.xml

A vb-nfc-reader/.idea/caches/deviceStreaming.xml => vb-nfc-reader/.idea/caches/deviceStreaming.xml +340 -0
@@ 0,0 1,340 @@
<?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>
\ No newline at end of file

A vb-nfc-reader/.idea/gradle.xml => vb-nfc-reader/.idea/gradle.xml +13 -0
@@ 0,0 1,13 @@
<?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>
\ No newline at end of file

A vb-nfc-reader/.idea/misc.xml => vb-nfc-reader/.idea/misc.xml +9 -0
@@ 0,0 1,9 @@
<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>
\ No newline at end of file

A vb-nfc-reader/.idea/runConfigurations.xml => vb-nfc-reader/.idea/runConfigurations.xml +17 -0
@@ 0,0 1,17 @@
<?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>
\ No newline at end of file

A vb-nfc-reader/build.gradle.kts => vb-nfc-reader/build.gradle.kts +44 -0
@@ 0,0 1,44 @@
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")
}
\ No newline at end of file

A vb-nfc-reader/consumer-rules.pro => vb-nfc-reader/consumer-rules.pro +0 -0
A vb-nfc-reader/proguard-rules.pro => vb-nfc-reader/proguard-rules.pro +21 -0
@@ 0,0 1,21 @@
# 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
\ No newline at end of file

A vb-nfc-reader/src/androidTest/java/com/github/cfogrady/vbnfc/ExampleInstrumentedTest.kt => vb-nfc-reader/src/androidTest/java/com/github/cfogrady/vbnfc/ExampleInstrumentedTest.kt +24 -0
@@ 0,0 1,24 @@
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)
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/AndroidManifest.xml => vb-nfc-reader/src/main/AndroidManifest.xml +4 -0
@@ 0,0 1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ByteManipulation.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ByteManipulation.kt +78 -0
@@ 0,0 1,78 @@
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
}

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ChecksumCalculator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ChecksumCalculator.kt +41 -0
@@ 0,0 1,41 @@
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)
            }
        }
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/CryptographicTransformer.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/CryptographicTransformer.kt +110 -0
@@ 0,0 1,110 @@
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
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslator.kt +30 -0
@@ 0,0 1,30 @@
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

}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslatorFactory.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslatorFactory.kt +22 -0
@@ 0,0 1,22 @@
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
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/TagCommunicator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/TagCommunicator.kt +192 -0
@@ 0,0 1,192 @@
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")
        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)
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/Utils.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/Utils.kt +20 -0
@@ 0,0 1,20 @@
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
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcCharacter.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcCharacter.kt +183 -0
@@ 0,0 1,183 @@
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()}
)"""
    }


}

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataFactory.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataFactory.kt +68 -0
@@ 0,0 1,68 @@
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)
//
    }

}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslator.kt +271 -0
@@ 0,0 1,271 @@
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
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDevice.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDevice.kt +20 -0
@@ 0,0 1,20 @@
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
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/FirmwareVersion.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/FirmwareVersion.kt +3 -0
@@ 0,0 1,3 @@
package com.github.cfogrady.vbnfc.be

data class FirmwareVersion(val majorVersion: Byte, val minorVersion: Byte)
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceSubType.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceSubType.kt +12 -0
@@ 0,0 1,12 @@
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
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceType.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceType.kt +9 -0
@@ 0,0 1,9 @@
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
    }
}

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcCharacter.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcCharacter.kt +147 -0
@@ 0,0 1,147 @@
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()}
)"""
    }


}

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcData.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcData.kt +3 -0
@@ 0,0 1,3 @@
package com.github.cfogrady.vbnfc.data

class NfcData(val nfcCharacter: NfcCharacter, val nfcDevice: NfcDevice)

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcDevice.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcDevice.kt +9 -0
@@ 0,0 1,9 @@
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()]
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcHeader.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcHeader.kt +26 -0
@@ 0,0 1,26 @@
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)
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcDataTranslator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcDataTranslator.kt +45 -0
@@ 0,0 1,45 @@
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
    }
}
\ No newline at end of file

A vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcHeader.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcHeader.kt +25 -0
@@ 0,0 1,25 @@
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,
)
\ No newline at end of file

A vb-nfc-reader/src/main/res/values/arrays.xml => vb-nfc-reader/src/main/res/values/arrays.xml +4 -0
@@ 0,0 1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="substitutionArray" />
</resources>
\ No newline at end of file

A vb-nfc-reader/src/main/res/values/strings.xml => vb-nfc-reader/src/main/res/values/strings.xml +6 -0
@@ 0,0 1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="password1" />
    <string name="password2" />
    <string name="decryptionKey" />
</resources>
\ No newline at end of file

A vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerHelper.kt => vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerHelper.kt +50 -0
@@ 0,0 1,50 @@
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)
        }
    }
}
\ No newline at end of file

A vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerTest.kt => vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerTest.kt +56 -0
@@ 0,0 1,56 @@
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())
        }
    }
}
\ No newline at end of file

A vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslatorTest.kt => vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslatorTest.kt +73 -0
@@ 0,0 1,73 @@
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)
    }
}
\ No newline at end of file