diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..94e8b87
--- /dev/null
+++ b/app/build.gradle.kts
@@ -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
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -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
diff --git a/app/src/androidTest/java/com/github/nacabaro/vbhelper/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/github/nacabaro/vbhelper/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..3170fe6
--- /dev/null
+++ b/app/src/androidTest/java/com/github/nacabaro/vbhelper/ExampleInstrumentedTest.kt
@@ -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
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2492ae6
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt
new file mode 100644
index 0000000..4c98062
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt
@@ -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
+
+ // 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 {
+ 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)
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/Dim.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/Dim.kt
new file mode 100644
index 0000000..199a248
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/Dim.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/DimProgress.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/DimProgress.kt
new file mode 100644
index 0000000..cb37abb
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/DimProgress.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/Evolutions.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/Evolutions.kt
new file mode 100644
index 0000000..58c9163
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/Evolutions.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/Mon.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/Mon.kt
new file mode 100644
index 0000000..b600adc
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/Mon.kt
@@ -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
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/User.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/User.kt
new file mode 100644
index 0000000..f3671d9
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/User.kt
@@ -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
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserHealthData.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserHealthData.kt
new file mode 100644
index 0000000..7cfd127
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserHealthData.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonsters.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonsters.kt
new file mode 100644
index 0000000..c4c2270
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonsters.kt
@@ -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
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonstersSpecialMissions.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonstersSpecialMissions.kt
new file mode 100644
index 0000000..83810ce
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserMonstersSpecialMissions.kt
@@ -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
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserStepsData.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserStepsData.kt
new file mode 100644
index 0000000..8d4f146
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/UserStepsData.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Color.kt b/app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Color.kt
new file mode 100644
index 0000000..542c355
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Color.kt
@@ -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
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Theme.kt b/app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Theme.kt
new file mode 100644
index 0000000..6673275
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Theme.kt
@@ -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
diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Type.kt b/app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Type.kt
new file mode 100644
index 0000000..9c21c4b
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/ui/theme/Type.kt
@@ -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
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..9dbd136
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ VBHelper
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..7c9d23c
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/github/nacabaro/vbhelper/ExampleUnitTest.kt b/app/src/test/java/com/github/nacabaro/vbhelper/ExampleUnitTest.kt
new file mode 100644
index 0000000..cc08726
--- /dev/null
+++ b/app/src/test/java/com/github/nacabaro/vbhelper/ExampleUnitTest.kt
@@ -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
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..952b930
--- /dev/null
+++ b/build.gradle.kts
@@ -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
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -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
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..76b8854
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -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" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..b519593
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -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" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -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
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..aa3e0ce
--- /dev/null
+++ b/settings.gradle.kts
@@ -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")
diff --git a/vb-nfc-reader/.gitignore b/vb-nfc-reader/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/vb-nfc-reader/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/vb-nfc-reader/.idea/.gitignore b/vb-nfc-reader/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/vb-nfc-reader/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/vb-nfc-reader/.idea/caches/deviceStreaming.xml b/vb-nfc-reader/.idea/caches/deviceStreaming.xml
new file mode 100644
index 0000000..406736c
--- /dev/null
+++ b/vb-nfc-reader/.idea/caches/deviceStreaming.xml
@@ -0,0 +1,340 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vb-nfc-reader/.idea/gradle.xml b/vb-nfc-reader/.idea/gradle.xml
new file mode 100644
index 0000000..d3f0572
--- /dev/null
+++ b/vb-nfc-reader/.idea/gradle.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vb-nfc-reader/.idea/misc.xml b/vb-nfc-reader/.idea/misc.xml
new file mode 100644
index 0000000..8bb1dd1
--- /dev/null
+++ b/vb-nfc-reader/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vb-nfc-reader/.idea/runConfigurations.xml b/vb-nfc-reader/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/vb-nfc-reader/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vb-nfc-reader/build.gradle.kts b/vb-nfc-reader/build.gradle.kts
new file mode 100644
index 0000000..c9539df
--- /dev/null
+++ b/vb-nfc-reader/build.gradle.kts
@@ -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
diff --git a/vb-nfc-reader/consumer-rules.pro b/vb-nfc-reader/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/vb-nfc-reader/proguard-rules.pro b/vb-nfc-reader/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/vb-nfc-reader/proguard-rules.pro
@@ -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
diff --git a/vb-nfc-reader/src/androidTest/java/com/github/cfogrady/vbnfc/ExampleInstrumentedTest.kt b/vb-nfc-reader/src/androidTest/java/com/github/cfogrady/vbnfc/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..21ef40e
--- /dev/null
+++ b/vb-nfc-reader/src/androidTest/java/com/github/cfogrady/vbnfc/ExampleInstrumentedTest.kt
@@ -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
diff --git a/vb-nfc-reader/src/main/AndroidManifest.xml b/vb-nfc-reader/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/vb-nfc-reader/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ByteManipulation.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ByteManipulation.kt
new file mode 100644
index 0000000..f7514eb
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ByteManipulation.kt
@@ -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 {
+ val result = Array(length) { 0u }
+ for (i in 0..
+ 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.. = 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
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/TagCommunicator.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/TagCommunicator.kt
new file mode 100644
index 0000000..1e9860c
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/TagCommunicator.kt
@@ -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): TagCommunicator {
+ val checksumCalculator = ChecksumCalculator()
+ val deviceToTranslator = HashMap()
+ 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.. {
+ val pages = ArrayList()
+ // setup blank header pages
+ for (i in 0..7) {
+ if (header != null) {
+ val index = i*4
+ pages.add(header.sliceArray(index..,
+ 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, // 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()}
+)"""
+ }
+
+
+}
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataFactory.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataFactory.kt
new file mode 100644
index 0000000..2fc6608
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataFactory.kt
@@ -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
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslator.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslator.kt
new file mode 100644
index 0000000..d0cd520
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslator.kt
@@ -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.., bytes: ByteArray) {
+ if (transformationHistory.size != 8) {
+ throw IllegalArgumentException("Transformation History must be exactly size 8")
+ }
+ for (phase in 0.. 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 {
+ val transformationHistory = Array(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
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDevice.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDevice.kt
new file mode 100644
index 0000000..29b2eba
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDevice.kt
@@ -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
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/FirmwareVersion.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/FirmwareVersion.kt
new file mode 100644
index 0000000..fb04c96
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/FirmwareVersion.kt
@@ -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
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceSubType.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceSubType.kt
new file mode 100644
index 0000000..f0af04b
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceSubType.kt
@@ -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
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceType.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceType.kt
new file mode 100644
index 0000000..8e1e87a
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceType.kt
@@ -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
+ }
+}
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcCharacter.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcCharacter.kt
new file mode 100644
index 0000000..203439e
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcCharacter.kt
@@ -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
+) {
+
+ 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()}
+)"""
+ }
+
+
+}
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcData.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcData.kt
new file mode 100644
index 0000000..cf31d7b
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcData.kt
@@ -0,0 +1,3 @@
+package com.github.cfogrady.vbnfc.data
+
+class NfcData(val nfcCharacter: NfcCharacter, val nfcDevice: NfcDevice)
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcDevice.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcDevice.kt
new file mode 100644
index 0000000..026bd90
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcDevice.kt
@@ -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
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcHeader.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcHeader.kt
new file mode 100644
index 0000000..8e3b34b
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcHeader.kt
@@ -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
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcDataTranslator.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcDataTranslator.kt
new file mode 100644
index 0000000..36ae719
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcDataTranslator.kt
@@ -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
diff --git a/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcHeader.kt b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcHeader.kt
new file mode 100644
index 0000000..8ca8c8b
--- /dev/null
+++ b/vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcHeader.kt
@@ -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
diff --git a/vb-nfc-reader/src/main/res/values/arrays.xml b/vb-nfc-reader/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..f46a6fe
--- /dev/null
+++ b/vb-nfc-reader/src/main/res/values/arrays.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/vb-nfc-reader/src/main/res/values/strings.xml b/vb-nfc-reader/src/main/res/values/strings.xml
new file mode 100644
index 0000000..33a89dd
--- /dev/null
+++ b/vb-nfc-reader/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerHelper.kt b/vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerHelper.kt
new file mode 100644
index 0000000..cc8c881
--- /dev/null
+++ b/vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerHelper.kt
@@ -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
diff --git a/vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerTest.kt b/vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerTest.kt
new file mode 100644
index 0000000..712c44e
--- /dev/null
+++ b/vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerTest.kt
@@ -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(), any()) } 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
diff --git a/vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslatorTest.kt b/vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslatorTest.kt
new file mode 100644
index 0000000..816117e
--- /dev/null
+++ b/vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslatorTest.kt
@@ -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