Initial commit
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
63
app/build.gradle.kts
Normal file
@ -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)
|
||||||
|
}
|
||||||
21
app/proguard-rules.pro
vendored
Normal file
@ -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
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
|
<uses-feature android:name="android.hardware.nfc" android:required="true" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.VBHelper"
|
||||||
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.VBHelper">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
121
app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package com.github.nacabaro.vbhelper
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.nfc.NfcAdapter
|
||||||
|
import android.nfc.Tag
|
||||||
|
import android.nfc.tech.NfcA
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.github.nacabaro.vbhelper.R
|
||||||
|
import com.github.cfogrady.vbnfc.CryptographicTransformer
|
||||||
|
import com.github.cfogrady.vbnfc.TagCommunicator
|
||||||
|
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||||
|
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
private lateinit var nfcAdapter: NfcAdapter
|
||||||
|
private lateinit var deviceToCryptographicTransformers: Map<UShort, CryptographicTransformer>
|
||||||
|
|
||||||
|
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
deviceToCryptographicTransformers = getMapOfCryptographicTransformers()
|
||||||
|
|
||||||
|
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||||
|
if (maybeNfcAdapter == null) {
|
||||||
|
Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nfcAdapter = maybeNfcAdapter
|
||||||
|
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
VBHelperTheme {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||||
|
private fun getMapOfCryptographicTransformers(): Map<UShort, CryptographicTransformer> {
|
||||||
|
return mapOf(
|
||||||
|
Pair(DeviceType.VitalBraceletBEDeviceType,
|
||||||
|
CryptographicTransformer(readableHmacKey1 = resources.getString(com.github.cfogrady.vbnfc.R.string.password1),
|
||||||
|
readableHmacKey2 = resources.getString(com.github.cfogrady.vbnfc.R.string.password2),
|
||||||
|
aesKey = resources.getString(com.github.cfogrady.vbnfc.R.string.decryptionKey),
|
||||||
|
substitutionCipher = resources.getIntArray(com.github.cfogrady.vbnfc.R.array.substitutionArray))),
|
||||||
|
// Pair(DeviceType.VitalSeriesDeviceType,
|
||||||
|
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
|
||||||
|
// hmacKey2 = resources.getString(R.string.password2),
|
||||||
|
// decryptionKey = resources.getString(R.string.decryptionKey),
|
||||||
|
// substitutionCipher = resources.getIntArray(R.array.substitutionArray))),
|
||||||
|
// Pair(DeviceType.VitalCharactersDeviceType,
|
||||||
|
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
|
||||||
|
// hmacKey2 = resources.getString(R.string.password2),
|
||||||
|
// decryptionKey = resources.getString(R.string.decryptionKey),
|
||||||
|
// substitutionCipher = resources.getIntArray(R.array.substitutionArray)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||||
|
private fun showWirelessSettings() {
|
||||||
|
Toast.makeText(this, "NFC must be enabled", Toast.LENGTH_SHORT).show()
|
||||||
|
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||||
|
private fun buildOnReadTag(handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
|
||||||
|
return { tag->
|
||||||
|
val nfcData = NfcA.get(tag)
|
||||||
|
if (nfcData == null) {
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(this, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nfcData.connect()
|
||||||
|
nfcData.use {
|
||||||
|
val tagCommunicator = TagCommunicator.getInstance(nfcData, deviceToCryptographicTransformers)
|
||||||
|
val successText = handlerFunc(tagCommunicator)
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(this, successText, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||||
|
private fun handleTag(handlerFunc: (TagCommunicator)->String) {
|
||||||
|
if (!nfcAdapter.isEnabled) {
|
||||||
|
showWirelessSettings()
|
||||||
|
} else {
|
||||||
|
val options = Bundle()
|
||||||
|
// Work around for some broken Nfc firmware implementations that poll the card too fast
|
||||||
|
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
|
||||||
|
nfcAdapter.enableReaderMode(this, buildOnReadTag(handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
if (nfcAdapter.isEnabled) {
|
||||||
|
nfcAdapter.disableReaderMode(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/src/main/java/com/github/nacabaro/vbhelper/domain/Dim.kt
Normal file
@ -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
|
||||||
|
)
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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
|
||||||
|
)
|
||||||
28
app/src/main/java/com/github/nacabaro/vbhelper/domain/Mon.kt
Normal file
@ -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
|
||||||
|
)
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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)
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
||||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
||||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
||||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
10
app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
3
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">VBHelper</string>
|
||||||
|
</resources>
|
||||||
5
app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.VBHelper" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
|
</resources>
|
||||||
13
app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample backup rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/guide/topics/data/autobackup
|
||||||
|
for details.
|
||||||
|
Note: This file is ignored for devices older that API 31
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore
|
||||||
|
-->
|
||||||
|
<full-backup-content>
|
||||||
|
<!--
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
|
-->
|
||||||
|
</full-backup-content>
|
||||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
for details.
|
||||||
|
-->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
-->
|
||||||
|
</cloud-backup>
|
||||||
|
<!--
|
||||||
|
<device-transfer>
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
</device-transfer>
|
||||||
|
-->
|
||||||
|
</data-extraction-rules>
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
build.gradle.kts
Normal file
@ -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
|
||||||
|
}
|
||||||
23
gradle.properties
Normal file
@ -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
|
||||||
35
gradle/libs.versions.toml
Normal file
@ -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" }
|
||||||
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -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
|
||||||
185
gradlew
vendored
Executable file
@ -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" "$@"
|
||||||
89
gradlew.bat
vendored
Normal file
@ -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
|
||||||
24
settings.gradle.kts
Normal file
@ -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")
|
||||||
1
vb-nfc-reader/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
3
vb-nfc-reader/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
340
vb-nfc-reader/.idea/caches/deviceStreaming.xml
generated
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceStreaming">
|
||||||
|
<option name="deviceSelectionList">
|
||||||
|
<list>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="27" />
|
||||||
|
<option name="brand" value="DOCOMO" />
|
||||||
|
<option name="codename" value="F01L" />
|
||||||
|
<option name="id" value="F01L" />
|
||||||
|
<option name="manufacturer" value="FUJITSU" />
|
||||||
|
<option name="name" value="F-01L" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1280" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="OPPO" />
|
||||||
|
<option name="codename" value="OP573DL1" />
|
||||||
|
<option name="id" value="OP573DL1" />
|
||||||
|
<option name="manufacturer" value="OPPO" />
|
||||||
|
<option name="name" value="CPH2557" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="28" />
|
||||||
|
<option name="brand" value="DOCOMO" />
|
||||||
|
<option name="codename" value="SH-01L" />
|
||||||
|
<option name="id" value="SH-01L" />
|
||||||
|
<option name="manufacturer" value="SHARP" />
|
||||||
|
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2160" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="Lenovo" />
|
||||||
|
<option name="codename" value="TB370FU" />
|
||||||
|
<option name="id" value="TB370FU" />
|
||||||
|
<option name="manufacturer" value="Lenovo" />
|
||||||
|
<option name="name" value="Tab P12" />
|
||||||
|
<option name="screenDensity" value="340" />
|
||||||
|
<option name="screenX" value="1840" />
|
||||||
|
<option name="screenY" value="2944" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="31" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="a51" />
|
||||||
|
<option name="id" value="a51" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy A51" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="akita" />
|
||||||
|
<option name="id" value="akita" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 8a" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="b0q" />
|
||||||
|
<option name="id" value="b0q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S22 Ultra" />
|
||||||
|
<option name="screenDensity" value="600" />
|
||||||
|
<option name="screenX" value="1440" />
|
||||||
|
<option name="screenY" value="3088" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="32" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="bluejay" />
|
||||||
|
<option name="id" value="bluejay" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 6a" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="caiman" />
|
||||||
|
<option name="id" value="caiman" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9 Pro" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="960" />
|
||||||
|
<option name="screenY" value="2142" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="comet" />
|
||||||
|
<option name="id" value="comet" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9 Pro Fold" />
|
||||||
|
<option name="screenDensity" value="390" />
|
||||||
|
<option name="screenX" value="2076" />
|
||||||
|
<option name="screenY" value="2152" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="29" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="crownqlteue" />
|
||||||
|
<option name="id" value="crownqlteue" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Note9" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="2220" />
|
||||||
|
<option name="screenY" value="1080" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="dm3q" />
|
||||||
|
<option name="id" value="dm3q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S23 Ultra" />
|
||||||
|
<option name="screenDensity" value="600" />
|
||||||
|
<option name="screenX" value="1440" />
|
||||||
|
<option name="screenY" value="3088" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="e1q" />
|
||||||
|
<option name="id" value="e1q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S24" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="felix" />
|
||||||
|
<option name="id" value="felix" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Fold" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="2208" />
|
||||||
|
<option name="screenY" value="1840" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="felix" />
|
||||||
|
<option name="id" value="felix" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Fold" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="2208" />
|
||||||
|
<option name="screenY" value="1840" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="felix_camera" />
|
||||||
|
<option name="id" value="felix_camera" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="2208" />
|
||||||
|
<option name="screenY" value="1840" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts8uwifi" />
|
||||||
|
<option name="id" value="gts8uwifi" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||||
|
<option name="screenDensity" value="320" />
|
||||||
|
<option name="screenX" value="1848" />
|
||||||
|
<option name="screenY" value="2960" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="husky" />
|
||||||
|
<option name="id" value="husky" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 8 Pro" />
|
||||||
|
<option name="screenDensity" value="390" />
|
||||||
|
<option name="screenX" value="1008" />
|
||||||
|
<option name="screenY" value="2244" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="30" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="java" />
|
||||||
|
<option name="id" value="java" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="G20" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="komodo" />
|
||||||
|
<option name="id" value="komodo" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9 Pro XL" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="1008" />
|
||||||
|
<option name="screenY" value="2244" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="lynx" />
|
||||||
|
<option name="id" value="lynx" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 7a" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="31" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="oriole" />
|
||||||
|
<option name="id" value="oriole" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 6" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="panther" />
|
||||||
|
<option name="id" value="panther" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 7" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="q5q" />
|
||||||
|
<option name="id" value="q5q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Z Fold5" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1812" />
|
||||||
|
<option name="screenY" value="2176" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="q6q" />
|
||||||
|
<option name="id" value="q6q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Z Fold6" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1856" />
|
||||||
|
<option name="screenY" value="2160" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="30" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="r11" />
|
||||||
|
<option name="id" value="r11" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Watch" />
|
||||||
|
<option name="screenDensity" value="320" />
|
||||||
|
<option name="screenX" value="384" />
|
||||||
|
<option name="screenY" value="384" />
|
||||||
|
<option name="type" value="WEAR_OS" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="30" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="redfin" />
|
||||||
|
<option name="id" value="redfin" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 5" />
|
||||||
|
<option name="screenDensity" value="440" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="shiba" />
|
||||||
|
<option name="id" value="shiba" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 8" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="tangorpro" />
|
||||||
|
<option name="id" value="tangorpro" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Tablet" />
|
||||||
|
<option name="screenDensity" value="320" />
|
||||||
|
<option name="screenX" value="1600" />
|
||||||
|
<option name="screenY" value="2560" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="tokay" />
|
||||||
|
<option name="id" value="tokay" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2424" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
13
vb-nfc-reader/.idea/gradle.xml
generated
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="jbr-21" />
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
vb-nfc-reader/.idea/misc.xml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
17
vb-nfc-reader/.idea/runConfigurations.xml
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
44
vb-nfc-reader/build.gradle.kts
Normal file
@ -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")
|
||||||
|
}
|
||||||
0
vb-nfc-reader/consumer-rules.pro
Normal file
21
vb-nfc-reader/proguard-rules.pro
vendored
Normal file
@ -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
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
vb-nfc-reader/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
package com.github.cfogrady.vbnfc
|
||||||
|
|
||||||
|
import java.lang.IllegalArgumentException
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
|
fun ByteArray.getUInt32(index: Int = 0, byteOrder: ByteOrder = ByteOrder.nativeOrder()): UInt {
|
||||||
|
if (this.size < index + 4) {
|
||||||
|
throw IllegalArgumentException("Must be 4 bytes from index to get a UInt")
|
||||||
|
}
|
||||||
|
var result: UInt = 0u
|
||||||
|
for (i in 0 until 4) {
|
||||||
|
result = if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||||
|
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(3 - i))
|
||||||
|
} else {
|
||||||
|
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UInt.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray {
|
||||||
|
val byteArray = byteArrayOf(0, 0, 0, 0)
|
||||||
|
for(i in 0 until 4) {
|
||||||
|
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
|
||||||
|
byteArray[i] = ((this shr 8*i) and 255u).toByte()
|
||||||
|
} else {
|
||||||
|
byteArray[3-i] = ((this shr 8*i) and 255u).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return byteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.getUInt16(index: Int = 0, byteOrder: ByteOrder = ByteOrder.nativeOrder()): UShort {
|
||||||
|
if (this.size < index + 2) {
|
||||||
|
throw IllegalArgumentException("Must be 2 bytes from index to get a UInt")
|
||||||
|
}
|
||||||
|
var result: UInt = 0u
|
||||||
|
for (i in 0 until 2) {
|
||||||
|
result = if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||||
|
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(1 - i))
|
||||||
|
} else {
|
||||||
|
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toUShort()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UShort.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray {
|
||||||
|
val byteArray = byteArrayOf(0, 0)
|
||||||
|
val asUInt = this.toUInt()
|
||||||
|
for(i in 0 until 2) {
|
||||||
|
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
|
||||||
|
byteArray[i] = ((asUInt shr 8*i) and 255u).toByte()
|
||||||
|
} else {
|
||||||
|
byteArray[1-i] = ((asUInt shr 8*i) and 255u).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return byteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UShort.toByteArray(bytes: ByteArray, dstIndex: Int, byteOrder: ByteOrder = ByteOrder.nativeOrder()) {
|
||||||
|
val asUInt = this.toUInt()
|
||||||
|
for(i in 0 until 2) {
|
||||||
|
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
|
||||||
|
bytes[i+dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
|
||||||
|
} else {
|
||||||
|
bytes[(1-i) + dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.copyIntoUShortArray(offset: Int, length: Int): Array<UShort> {
|
||||||
|
val result = Array<UShort>(length) { 0u }
|
||||||
|
for (i in 0..<length) {
|
||||||
|
result[i] = this.getUInt16(offset + i * 2, ByteOrder.BIG_ENDIAN)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.github.cfogrady.vbnfc
|
||||||
|
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
|
class ChecksumCalculator {
|
||||||
|
companion object {
|
||||||
|
private val PagesWithChecksum = hashSetOf(8, 16, 24, 32, 40, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 104, 192, 200, 208, 216)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
fun checkChecksums(data: ByteArray) {
|
||||||
|
operateOnChecksums(data) { checksumByte, checksumIdx ->
|
||||||
|
if (checksumByte != data[checksumIdx]) {
|
||||||
|
throw IllegalStateException("Checksum ${checksumByte.toHexString()} doesn't match expected ${data[checksumIdx].toHexString()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
fun recalculateChecksums(data: ByteArray) {
|
||||||
|
operateOnChecksums(data) { checksumByte, checksumIdx ->
|
||||||
|
data[checksumIdx] = checksumByte
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun operateOnChecksums(data: ByteArray, operator: (Byte, Int)->Unit) {
|
||||||
|
// loop through all data
|
||||||
|
for(i in data.indices step 16) {
|
||||||
|
val page = i/4 + 8 // first 8 pages are header data and not part of the character data
|
||||||
|
if (PagesWithChecksum.contains(page)) {
|
||||||
|
var sum = 0
|
||||||
|
val checksumIndex = i + 15
|
||||||
|
for(j in i..<checksumIndex) {
|
||||||
|
sum += data[j]
|
||||||
|
}
|
||||||
|
val checksumByte = (sum and 0xff).toByte()
|
||||||
|
operator(checksumByte, checksumIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
package com.github.cfogrady.vbnfc
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.Base64
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import kotlin.experimental.xor
|
||||||
|
|
||||||
|
class CryptographicTransformer(private val readableHmacKey1: String, private val readableHmacKey2: String, private val aesKey: String, private val substitutionCipher: IntArray) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val HMAC256 = "HmacSHA256"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a 4 byte password by hashing the current data using HMAC256 with the first key. Then
|
||||||
|
// applying a 4-bit substitution cypher on the result, and hashing again with the second key.
|
||||||
|
// The password are the 4 bytes starting at index 28 of that result.
|
||||||
|
fun createNfcPassword(inputData: ByteArray): ByteArray {
|
||||||
|
val hmacKey1 = decryptHmacKey(readableHmacKey1)
|
||||||
|
val hmacKey2 = decryptHmacKey(readableHmacKey2)
|
||||||
|
val hashedInput = generateHMacSHA256Hash(hmacKey1, inputData)
|
||||||
|
val substitutedBytes = apply4BitSubstitutionCipher(hashedInput)
|
||||||
|
val secondHash = generateHMacSHA256Hash(hmacKey2, substitutedBytes)
|
||||||
|
return secondHash.sliceArray(28..<32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptData(data: ByteArray, tagId: ByteArray): ByteArray {
|
||||||
|
val hmacKey1 = decryptHmacKey(readableHmacKey1)
|
||||||
|
val hmacKey2 = decryptHmacKey(readableHmacKey2)
|
||||||
|
return cryptoTransformation(Cipher.DECRYPT_MODE, data, tagId, hmacKey1, hmacKey2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptData(data: ByteArray, tagId: ByteArray): ByteArray {
|
||||||
|
val hmacKey1 = decryptHmacKey(readableHmacKey1)
|
||||||
|
val hmacKey2 = decryptHmacKey(readableHmacKey2)
|
||||||
|
return cryptoTransformation(Cipher.ENCRYPT_MODE, data, tagId, hmacKey1, hmacKey2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptHmacKey(str: String): String {
|
||||||
|
val decoded = Base64.getDecoder().decode(str)
|
||||||
|
val decrypt = decryptAesCbcPkcs5Padding(aesKey, decoded)
|
||||||
|
return String(decrypt, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptAesCbcPkcs5Padding(key: String, data: ByteArray): ByteArray {
|
||||||
|
val keyBytes = key.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val rightSizedKey = keyBytes.copyOf(32)
|
||||||
|
val ivBytes = keyBytes.copyOfRange(key.length - 16, key.length)
|
||||||
|
val secretKeySpec = SecretKeySpec(rightSizedKey, "AES")
|
||||||
|
val ivParameterSpec = IvParameterSpec(ivBytes)
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateHMacSHA256Hash(hmacKey: String, data: ByteArray): ByteArray {
|
||||||
|
val hmacKeyBytes = hmacKey.toByteArray(StandardCharsets.US_ASCII)
|
||||||
|
val secretKeySpec = SecretKeySpec(hmacKeyBytes, HMAC256)
|
||||||
|
val mac = Mac.getInstance(HMAC256)
|
||||||
|
mac.init(secretKeySpec)
|
||||||
|
return mac.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a 4 bit substitution cipher, where each 4 bits act as an index to another 4 bits
|
||||||
|
private fun apply4BitSubstitutionCipher(data: ByteArray): ByteArray {
|
||||||
|
val result = ByteArray(data.size)
|
||||||
|
for (idx in data.indices) {
|
||||||
|
val byte: Int = data[idx].toInt()
|
||||||
|
var newByte = 0
|
||||||
|
for (fourBitShifts in 0..<2) { // perform one OR without shift, and one OR shifted 4 bits
|
||||||
|
val shift = fourBitShifts * 4
|
||||||
|
val permutationIndex = (byte shr shift) and 0xF
|
||||||
|
newByte = newByte or (substitutionCipher[permutationIndex] shl shift)
|
||||||
|
}
|
||||||
|
result[idx] = newByte.toByte()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hashes the tagId once and applies substitution cipher. Then hashes again.
|
||||||
|
// Splits hash and original tagId into key and initialization vector
|
||||||
|
private fun cryptoTransformation(cipherMode: Int, data: ByteArray, tagId: ByteArray, hmacKey1: String, hmacKey2: String): ByteArray {
|
||||||
|
var hashedTagId = apply4BitSubstitutionCipher(generateHMacSHA256Hash(hmacKey1, tagId))
|
||||||
|
hashedTagId = generateHMacSHA256Hash(hmacKey2, hashedTagId) // second hash
|
||||||
|
|
||||||
|
// generate actual key and initializing vector from tagIdGeneratedKey
|
||||||
|
val iv1 = ByteArray(15)
|
||||||
|
hashedTagId.copyInto(iv1, 0, 24, 32)
|
||||||
|
tagId.copyInto(iv1, 8, 0, 7)
|
||||||
|
val iv2 = hashedTagId.copyOf(15)
|
||||||
|
val ivParameterSpec = IvParameterSpec(xorBytes(iv1, iv2, 16))
|
||||||
|
val secretKeySpec = SecretKeySpec(hashedTagId.copyOf(16), "AES")
|
||||||
|
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||||
|
cipher.init(cipherMode, secretKeySpec, ivParameterSpec)
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun xorBytes(data1: ByteArray, data2: ByteArray, resultSize: Int): ByteArray {
|
||||||
|
val results = ByteArray(resultSize)
|
||||||
|
results.fill(0)
|
||||||
|
for (i in data1.indices) {
|
||||||
|
results[i] = data1[i] xor data2[i]
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.github.cfogrady.vbnfc
|
||||||
|
|
||||||
|
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||||
|
import com.github.cfogrady.vbnfc.data.NfcHeader
|
||||||
|
|
||||||
|
interface NfcDataTranslator {
|
||||||
|
|
||||||
|
// setCharacterInByteArray takes the NfcCharacter and modifies the byte array with character
|
||||||
|
// data. At the time of writing this is used to write a parsed character into fresh unparsed
|
||||||
|
// device data when sending a character back to the device.
|
||||||
|
fun setCharacterInByteArray(character: NfcCharacter, bytes: ByteArray) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalizeByteArrayFormat finalizes the byte array for NFC format by setting all the
|
||||||
|
// checksums, and duplicating the duplicate memory pages.
|
||||||
|
fun finalizeByteArrayFormat(bytes: ByteArray)
|
||||||
|
|
||||||
|
// getOperationCommandBytes gets an operation command corresponding to the existing header and
|
||||||
|
// the input operation
|
||||||
|
fun getOperationCommandBytes(header: NfcHeader, operation: Byte): ByteArray
|
||||||
|
|
||||||
|
// parseNfcCharacter parses the nfc data byte array into an instance of a NfcCharacter object
|
||||||
|
fun parseNfcCharacter(bytes: ByteArray): NfcCharacter
|
||||||
|
|
||||||
|
// parseHeader parses the nfc header byte array into an instance of NfcHeader
|
||||||
|
fun parseHeader(headerBytes: ByteArray): NfcHeader
|
||||||
|
|
||||||
|
val cryptographicTransformer: CryptographicTransformer
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.github.cfogrady.vbnfc
|
||||||
|
|
||||||
|
import com.github.cfogrady.vbnfc.be.BENfcDataTranslator
|
||||||
|
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||||
|
|
||||||
|
class NfcDataTranslatorFactory(
|
||||||
|
private val translators: MutableMap<UShort,NfcDataTranslator> = HashMap()
|
||||||
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
fun getNfcDataTranslator(deviceTypeId: UShort): NfcDataTranslator {
|
||||||
|
val dataTranslator = translators[deviceTypeId]
|
||||||
|
if(dataTranslator != null) {
|
||||||
|
return dataTranslator
|
||||||
|
}
|
||||||
|
throw UnsupportedOperationException("Device type ${deviceTypeId} is not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addNfcDataTranslator(nfcDataTranslator: NfcDataTranslator, deviceTypeId: UShort) {
|
||||||
|
translators[deviceTypeId] = nfcDataTranslator
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,192 @@
|
|||||||
|
package com.github.cfogrady.vbnfc
|
||||||
|
|
||||||
|
import android.nfc.tech.NfcA
|
||||||
|
import android.util.Log
|
||||||
|
import com.github.cfogrady.vbnfc.be.BENfcDataTranslator
|
||||||
|
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||||
|
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||||
|
import com.github.cfogrady.vbnfc.data.NfcHeader
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
|
class TagCommunicator(
|
||||||
|
private val nfcData: NfcA,
|
||||||
|
private val checksumCalculator: ChecksumCalculator,
|
||||||
|
private val nfcDataTranslatorFactory: NfcDataTranslatorFactory,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "VBNfcHandler"
|
||||||
|
const val HEADER_PAGE: Byte = 0x04
|
||||||
|
const val NFC_PASSWORD_COMMAND: Byte = 0x1b
|
||||||
|
const val NFC_READ_COMMAND: Byte = 0x30
|
||||||
|
const val NFC_WRITE_COMMAND: Byte = 0xA2.toByte()
|
||||||
|
|
||||||
|
const val STATUS_IDLE: Byte = 0
|
||||||
|
const val STATUS_READY: Byte = 1
|
||||||
|
|
||||||
|
const val OPERATION_IDLE: Byte = 0
|
||||||
|
const val OPERATION_READY: Byte = 1
|
||||||
|
const val OPERATION_TRANSFERRED_TO_APP: Byte = 2
|
||||||
|
const val OPERATION_CHECK_DIM: Byte = 3
|
||||||
|
const val OPERATION_TRANSFERED_TO_DEVICE: Byte = 4
|
||||||
|
const val START_DATA_PAGE = 8
|
||||||
|
const val LAST_DATA_PAGE = 220 // technically 223, but we read 4 pages at a time.
|
||||||
|
|
||||||
|
fun getInstance(nfcData: NfcA, deviceTypeIdSecrets: Map<UShort, CryptographicTransformer>): TagCommunicator {
|
||||||
|
val checksumCalculator = ChecksumCalculator()
|
||||||
|
val deviceToTranslator = HashMap<UShort, NfcDataTranslator>()
|
||||||
|
for (keyValue in deviceTypeIdSecrets) {
|
||||||
|
when(keyValue.key) {
|
||||||
|
DeviceType.VitalBraceletBEDeviceType -> {
|
||||||
|
deviceToTranslator[keyValue.key] = BENfcDataTranslator(keyValue.value, checksumCalculator)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw IllegalArgumentException("DeviceId ${keyValue.key} Provided Without Known Parser")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TagCommunicator(nfcData, checksumCalculator, NfcDataTranslatorFactory(deviceToTranslator))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeviceTranslatorAndHeader(val nfcHeader: NfcHeader, val translator: NfcDataTranslator)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
fun receiveCharacter(): NfcCharacter {
|
||||||
|
val translatorAndHeader = fetchDeviceTranslatorAndHeader()
|
||||||
|
val header = translatorAndHeader.nfcHeader
|
||||||
|
val translator = translatorAndHeader.translator
|
||||||
|
Log.i(TAG, "Writing to make ready for operation")
|
||||||
|
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
|
||||||
|
Log.i(TAG, "Authenticating")
|
||||||
|
|
||||||
|
passwordAuth(translator.cryptographicTransformer)
|
||||||
|
Log.i(TAG, "Reading Character")
|
||||||
|
val encryptedCharacterData = readNfcData()
|
||||||
|
Log.i(TAG, "Raw NFC Data Received: ${encryptedCharacterData.toHexString()}")
|
||||||
|
val decryptedCharacterData = translator.cryptographicTransformer.decryptData(encryptedCharacterData, nfcData.tag.id)
|
||||||
|
checksumCalculator.checkChecksums(decryptedCharacterData)
|
||||||
|
val nfcCharacter = translator.parseNfcCharacter(decryptedCharacterData)
|
||||||
|
Log.i(TAG, "Known Character Stats: $nfcCharacter")
|
||||||
|
Log.i(TAG, "Signaling operation complete")
|
||||||
|
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_TRANSFERRED_TO_APP))
|
||||||
|
return nfcCharacter
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
private fun fetchDeviceTranslatorAndHeader(): DeviceTranslatorAndHeader {
|
||||||
|
val readData = nfcData.transceive(byteArrayOf(NFC_READ_COMMAND, HEADER_PAGE))
|
||||||
|
Log.i("TagCommunicator", "First 4 Pages: ${readData.toHexString()}")
|
||||||
|
val deviceTypeId = readData.getUInt16(4, ByteOrder.BIG_ENDIAN)
|
||||||
|
val translator = nfcDataTranslatorFactory.getNfcDataTranslator(deviceTypeId)
|
||||||
|
val header = translator.parseHeader(readData)
|
||||||
|
return DeviceTranslatorAndHeader(header, translator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readNfcData(): ByteArray {
|
||||||
|
val result = ByteArray(((LAST_DATA_PAGE +4)- START_DATA_PAGE) * 4)
|
||||||
|
for (page in START_DATA_PAGE..LAST_DATA_PAGE step 4) {
|
||||||
|
val pages = nfcData.transceive(byteArrayOf(NFC_READ_COMMAND, page.toByte()))
|
||||||
|
if (pages.size < 16) {
|
||||||
|
throw Exception("Failed to read page: $page")
|
||||||
|
}
|
||||||
|
System.arraycopy(pages, 0, result, (page - START_DATA_PAGE)*4, pages.size)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepareDIMForCharacter(dimId: UShort) {
|
||||||
|
val translatorAndHeader = fetchDeviceTranslatorAndHeader()
|
||||||
|
val header = translatorAndHeader.nfcHeader
|
||||||
|
val translator = translatorAndHeader.translator
|
||||||
|
// set app nonce to device ensure when we send back the character that we are preparing
|
||||||
|
// the same device we send to
|
||||||
|
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
|
||||||
|
// app authenticates, and reads everything, and checks the version
|
||||||
|
// The version check is only for the BE when transfering from DIM=0 (pulsemon).
|
||||||
|
// This was from the bug when the BE first came out.
|
||||||
|
// Check (page 103 [0:1] != 1, 0)
|
||||||
|
header.setDimId(dimId)
|
||||||
|
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_CHECK_DIM))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultNfcDataGenerator(translator: NfcDataTranslator, character: NfcCharacter): ByteArray {
|
||||||
|
val currentNfcData = readNfcData()
|
||||||
|
val newNfcData = translator.cryptographicTransformer.decryptData(currentNfcData, nfcData.tag.id)
|
||||||
|
translator.setCharacterInByteArray(character, newNfcData)
|
||||||
|
checksumCalculator.recalculateChecksums(newNfcData)
|
||||||
|
translator.finalizeByteArrayFormat(newNfcData)
|
||||||
|
return newNfcData
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendCharacter sends a character to the device using the nfcDataGenerator function. The
|
||||||
|
// default nfcDataGenerator reads the current data of the device and applies the new character
|
||||||
|
// data to the read data and prepares that to be sent back to the device. The nfcDataGenerator
|
||||||
|
// is a functor which takes in the NfcDataTranslator for the device and the NfcCharacter
|
||||||
|
// provided to the sendCharacter method and returns the decrypted byte array data to be sent
|
||||||
|
// back to the device.
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
fun sendCharacter(character: NfcCharacter, nfcDataGenerator: (NfcDataTranslator, NfcCharacter) -> ByteArray = this::defaultNfcDataGenerator) {
|
||||||
|
Log.i(TAG, "Sending Character: $character")
|
||||||
|
val deviceTranslatorAndHeader = fetchDeviceTranslatorAndHeader()
|
||||||
|
val translator = deviceTranslatorAndHeader.translator
|
||||||
|
val header = deviceTranslatorAndHeader.nfcHeader
|
||||||
|
|
||||||
|
// check the nonce
|
||||||
|
// if it's not expected, then the bracelet isn't ready
|
||||||
|
|
||||||
|
// Check the product and device ids that they match the target.
|
||||||
|
// This check relies on the app categories of BE, Vital Hero, and Vital Series.
|
||||||
|
|
||||||
|
// ensure the dim id matches the expected
|
||||||
|
if (character.dimId != header.getDimId()) {
|
||||||
|
throw IllegalArgumentException("Device is ready for DIM ${header.getDimId()}, but attempted to send ${character.dimId}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the memory data
|
||||||
|
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
|
||||||
|
passwordAuth(translator.cryptographicTransformer)
|
||||||
|
|
||||||
|
var newNfcData = nfcDataGenerator(translator, character)
|
||||||
|
newNfcData = translator.cryptographicTransformer.encryptData(newNfcData, nfcData.tag.id)
|
||||||
|
Log.i(TAG, "Sending Character: ${newNfcData.toHexString()}")
|
||||||
|
|
||||||
|
|
||||||
|
// write nfc data
|
||||||
|
val pagedData = ConvertToPages(newNfcData)
|
||||||
|
for(pageToWriteIdx in 8..<pagedData.size) {
|
||||||
|
val pageToWrite = pagedData[pageToWriteIdx]
|
||||||
|
nfcData.transceive(byteArrayOf(NFC_WRITE_COMMAND, pageToWriteIdx.toByte(), pageToWrite[0], pageToWrite[1], pageToWrite[2], pageToWrite[3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_TRANSFERED_TO_DEVICE))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
fun passwordAuth(cryptographicTransformer: CryptographicTransformer) {
|
||||||
|
val tagId = nfcData.tag.id
|
||||||
|
Log.i(TAG, "TagId: ${tagId.toHexString()}")
|
||||||
|
val password = cryptographicTransformer.createNfcPassword(tagId)
|
||||||
|
try {
|
||||||
|
val result = nfcData.transceive(byteArrayOf(NFC_PASSWORD_COMMAND, password[0], password[1], password[2], password[3]))
|
||||||
|
Log.i(TAG, "PasswordAuth Result: ${result.toHexString()}")
|
||||||
|
if (result.size == 1) {
|
||||||
|
throw AuthenticationException("Authentication failed. Result: ${result.toHexString()}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDataTranslator adds a new data translator to be used with the specified deviceTypeId.
|
||||||
|
// This can be used to keep the same general communication protocol, but allows for a different
|
||||||
|
// parsing of the data.
|
||||||
|
fun addDataTranslator(nfcDataTranslator: NfcDataTranslator, deviceTypeId: UShort) {
|
||||||
|
nfcDataTranslatorFactory.addNfcDataTranslator(nfcDataTranslator, deviceTypeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticationException(message: String): Exception(message)
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.github.cfogrady.vbnfc
|
||||||
|
|
||||||
|
// ConverToPages converts the byte array into the paged structure used in NFC communication
|
||||||
|
// If data for the header isn't included, the first 8 pages will be 0 filled.
|
||||||
|
fun ConvertToPages(data: ByteArray, header: ByteArray? = null) : List<ByteArray> {
|
||||||
|
val pages = ArrayList<ByteArray>()
|
||||||
|
// setup blank header pages
|
||||||
|
for (i in 0..7) {
|
||||||
|
if (header != null) {
|
||||||
|
val index = i*4
|
||||||
|
pages.add(header.sliceArray(index..<index+4))
|
||||||
|
} else {
|
||||||
|
pages.add(byteArrayOf(0, 0, 0, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(i in data.indices step 4) {
|
||||||
|
pages.add(data.sliceArray(i..<i+4))
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
@ -0,0 +1,183 @@
|
|||||||
|
package com.github.cfogrady.vbnfc.be
|
||||||
|
|
||||||
|
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||||
|
import java.util.Arrays
|
||||||
|
import java.util.Objects
|
||||||
|
|
||||||
|
class BENfcCharacter(
|
||||||
|
dimId: UShort,
|
||||||
|
charIndex: UShort,
|
||||||
|
stage: Byte,
|
||||||
|
attribute: Attribute,
|
||||||
|
ageInDays: Byte,
|
||||||
|
nextAdventureMissionStage: Byte,
|
||||||
|
mood: Byte,
|
||||||
|
vitalPoints: UShort,
|
||||||
|
transformationCountdownInMinutes: UShort,
|
||||||
|
injuryStatus: InjuryStatus,
|
||||||
|
trainingPp: UShort,
|
||||||
|
currentPhaseBattlesWon: UShort,
|
||||||
|
currentPhaseBattlesLost: UShort,
|
||||||
|
totalBattlesWon: UShort,
|
||||||
|
totalBattlesLost: UShort,
|
||||||
|
activityLevel: Byte,
|
||||||
|
heartRateCurrent: UByte,
|
||||||
|
transformationHistory: Array<Transformation>,
|
||||||
|
var trainingHp: UShort,
|
||||||
|
var trainingAp: UShort,
|
||||||
|
var trainingBp: UShort,
|
||||||
|
var remainingTrainingTimeInMinutes: UShort,
|
||||||
|
var itemEffectMentalStateValue: Byte,
|
||||||
|
var itemEffectMentalStateMinutesRemaining: Byte,
|
||||||
|
var itemEffectActivityLevelValue: Byte,
|
||||||
|
var itemEffectActivityLevelMinutesRemaining: Byte,
|
||||||
|
var itemEffectVitalPointsChangeValue: Byte,
|
||||||
|
var itemEffectVitalPointsChangeMinutesRemaining: Byte,
|
||||||
|
var abilityRarity: AbilityRarity,
|
||||||
|
var abilityType: UShort,
|
||||||
|
var abilityBranch: UShort,
|
||||||
|
var abilityReset: Byte,
|
||||||
|
var rank: Byte,
|
||||||
|
var itemType: Byte,
|
||||||
|
var itemMultiplier: Byte,
|
||||||
|
var itemRemainingTime: Byte,
|
||||||
|
internal val otp0: ByteArray, // OTP matches the character to the dim
|
||||||
|
internal val otp1: ByteArray, // OTP matches the character to the dim
|
||||||
|
var characterCreationFirmwareVersion: FirmwareVersion,
|
||||||
|
var appReserved1: ByteArray, // this is a 12 byte array reserved for new app features, a custom app should be able to safely use this for custom features
|
||||||
|
var appReserved2: Array<UShort>, // this is a 3 element array reserved for new app features, a custom app should be able to safely use this for custom features
|
||||||
|
) :
|
||||||
|
NfcCharacter(
|
||||||
|
dimId = dimId,
|
||||||
|
charIndex = charIndex,
|
||||||
|
stage = stage,
|
||||||
|
attribute = attribute,
|
||||||
|
ageInDays = ageInDays,
|
||||||
|
nextAdventureMissionStage = nextAdventureMissionStage,
|
||||||
|
mood = mood,
|
||||||
|
vitalPoints = vitalPoints,
|
||||||
|
transformationCountdown = transformationCountdownInMinutes,
|
||||||
|
injuryStatus = injuryStatus,
|
||||||
|
trophies = trainingPp,
|
||||||
|
currentPhaseBattlesWon = currentPhaseBattlesWon,
|
||||||
|
currentPhaseBattlesLost = currentPhaseBattlesLost,
|
||||||
|
totalBattlesWon = totalBattlesWon,
|
||||||
|
totalBattlesLost = totalBattlesLost,
|
||||||
|
activityLevel = activityLevel,
|
||||||
|
heartRateCurrent = heartRateCurrent,
|
||||||
|
transformationHistory = transformationHistory,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
fun getTrainingPp(): UShort {
|
||||||
|
return trophies
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTrainingPp(trainingPp: UShort) {
|
||||||
|
trophies = trainingPp
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getWinPercentage(): Byte {
|
||||||
|
val totalBatles = currentPhaseBattlesWon + currentPhaseBattlesLost
|
||||||
|
if (totalBatles == 0u) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return ((100u * currentPhaseBattlesWon) / totalBatles).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as BENfcCharacter
|
||||||
|
if(!super.equals(other)) return false
|
||||||
|
|
||||||
|
if (trainingHp != other.trainingHp) return false
|
||||||
|
if (trainingAp != other.trainingAp) return false
|
||||||
|
if (trainingBp != other.trainingBp) return false
|
||||||
|
if (remainingTrainingTimeInMinutes != other.remainingTrainingTimeInMinutes) return false
|
||||||
|
if (itemEffectMentalStateValue != other.itemEffectMentalStateValue) return false
|
||||||
|
if (itemEffectMentalStateMinutesRemaining != other.itemEffectMentalStateMinutesRemaining) return false
|
||||||
|
if (itemEffectActivityLevelValue != other.itemEffectActivityLevelValue) return false
|
||||||
|
if (itemEffectActivityLevelMinutesRemaining != other.itemEffectActivityLevelMinutesRemaining) return false
|
||||||
|
if (itemEffectVitalPointsChangeValue != other.itemEffectVitalPointsChangeValue) return false
|
||||||
|
if (itemEffectVitalPointsChangeMinutesRemaining != other.itemEffectVitalPointsChangeMinutesRemaining) return false
|
||||||
|
if (abilityRarity != other.abilityRarity) return false
|
||||||
|
if (abilityType != other.abilityType) return false
|
||||||
|
if (abilityBranch != other.abilityBranch) return false
|
||||||
|
if (abilityReset != other.abilityReset) return false
|
||||||
|
if (rank != other.rank) return false
|
||||||
|
if (itemType != other.itemType) return false
|
||||||
|
if (itemMultiplier != other.itemMultiplier) return false
|
||||||
|
if (itemRemainingTime != other.itemRemainingTime) return false
|
||||||
|
if (!otp0.contentEquals(other.otp0)) return false
|
||||||
|
if (!otp1.contentEquals(other.otp1)) return false
|
||||||
|
if (characterCreationFirmwareVersion != other.characterCreationFirmwareVersion) return false
|
||||||
|
if (!appReserved1.contentEquals(other.appReserved1)) return false
|
||||||
|
if (!appReserved2.contentEquals(other.appReserved2)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return Objects.hash(
|
||||||
|
super.hashCode(),
|
||||||
|
trainingHp,
|
||||||
|
trainingAp,
|
||||||
|
trainingBp,
|
||||||
|
remainingTrainingTimeInMinutes,
|
||||||
|
itemEffectMentalStateValue,
|
||||||
|
itemEffectMentalStateMinutesRemaining,
|
||||||
|
itemEffectActivityLevelValue,
|
||||||
|
itemEffectActivityLevelMinutesRemaining,
|
||||||
|
itemEffectVitalPointsChangeValue,
|
||||||
|
itemEffectVitalPointsChangeMinutesRemaining,
|
||||||
|
abilityRarity,
|
||||||
|
abilityType,
|
||||||
|
abilityBranch,
|
||||||
|
abilityReset,
|
||||||
|
rank,
|
||||||
|
itemType,
|
||||||
|
itemMultiplier,
|
||||||
|
itemRemainingTime,
|
||||||
|
otp0.contentHashCode(),
|
||||||
|
otp1.contentHashCode(),
|
||||||
|
characterCreationFirmwareVersion,
|
||||||
|
appReserved1.contentHashCode(),
|
||||||
|
appReserved2.contentHashCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
override fun toString(): String {
|
||||||
|
return """
|
||||||
|
${super.toString()}
|
||||||
|
BENfcCharacter(
|
||||||
|
trainingHp=$trainingHp,
|
||||||
|
trainingAp=$trainingAp,
|
||||||
|
trainingBp=$trainingBp,
|
||||||
|
remainingTrainingTimeInMinutes=$remainingTrainingTimeInMinutes,
|
||||||
|
itemEffectMentalStateValue=$itemEffectMentalStateValue,
|
||||||
|
itemEffectMentalStateMinutesRemaining=$itemEffectMentalStateMinutesRemaining,
|
||||||
|
itemEffectActivityLevelValue=$itemEffectActivityLevelValue,
|
||||||
|
itemEffectActivityLevelMinutesRemaining=$itemEffectActivityLevelMinutesRemaining,
|
||||||
|
itemEffectVitalPointsChangeValue=$itemEffectVitalPointsChangeValue,
|
||||||
|
itemEffectVitalPointsChangeMinutesRemaining=$itemEffectVitalPointsChangeMinutesRemaining,
|
||||||
|
abilityRarity=$abilityRarity,
|
||||||
|
abilityType=$abilityType,
|
||||||
|
abilityBranch=$abilityBranch,
|
||||||
|
abilityReset=$abilityReset,
|
||||||
|
rank=$rank,
|
||||||
|
itemType=$itemType,
|
||||||
|
itemMultiplier=$itemMultiplier,
|
||||||
|
itemRemainingTime=$itemRemainingTime,
|
||||||
|
otp0=${otp0.toHexString()},
|
||||||
|
otp1=${otp1.toHexString()},
|
||||||
|
characterCreationFirmwareVersion=$characterCreationFirmwareVersion,
|
||||||
|
appReserved1=${appReserved1.contentToString()},
|
||||||
|
appReserved2=${appReserved2.contentToString()}
|
||||||
|
)"""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,271 @@
|
|||||||
|
package com.github.cfogrady.vbnfc.be
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.github.cfogrady.vbnfc.ChecksumCalculator
|
||||||
|
import com.github.cfogrady.vbnfc.CryptographicTransformer
|
||||||
|
import com.github.cfogrady.vbnfc.NfcDataTranslator
|
||||||
|
import com.github.cfogrady.vbnfc.TagCommunicator
|
||||||
|
import com.github.cfogrady.vbnfc.copyIntoUShortArray
|
||||||
|
import com.github.cfogrady.vbnfc.data.DeviceSubType
|
||||||
|
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||||
|
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||||
|
import com.github.cfogrady.vbnfc.data.NfcHeader
|
||||||
|
import com.github.cfogrady.vbnfc.getUInt16
|
||||||
|
import com.github.cfogrady.vbnfc.toByteArray
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
|
class BENfcDataTranslator(
|
||||||
|
override val cryptographicTransformer: CryptographicTransformer,
|
||||||
|
private val checksumCalculator: ChecksumCalculator = ChecksumCalculator()
|
||||||
|
): NfcDataTranslator {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val OPERATION_PAGE: Byte = 0x6
|
||||||
|
|
||||||
|
// CHARACTER
|
||||||
|
const val APP_RESERVED_START = 0
|
||||||
|
const val APP_RESERVED_SIZE = 12
|
||||||
|
const val INJURY_STATUS_IDX = 64
|
||||||
|
const val APP_RESERVED_2_START = 66
|
||||||
|
const val APP_RESERVED_2_SIZE = 3 //3 ushorts
|
||||||
|
const val CHARACTER_INDEX_IDX = 72
|
||||||
|
const val DIM_ID_IDX = 74
|
||||||
|
const val PHASE_IDX = 76
|
||||||
|
const val ATTRIBUTE_IDX = 77
|
||||||
|
const val AGE_IN_DAYS_IDX = 78 // always 0 on BE :(
|
||||||
|
const val TRAINING_PP_IDX = 96
|
||||||
|
const val CURRENT_BATTLES_WON_IDX = 98
|
||||||
|
const val CURRENT_BATTLES_LOST_IDX = 100
|
||||||
|
const val TOTAL_BATTLES_WON_IDX = 102
|
||||||
|
const val TOTAL_BATTLES_LOST_IDX = 104
|
||||||
|
const val WIN_PCT_IDX = 106 // unused
|
||||||
|
const val CHARACTER_CREATION_FIRMWARE_VERSION_IDX = 108
|
||||||
|
const val NEXT_ADVENTURE_MISSION_STAGE_IDX = 128
|
||||||
|
const val MOOD_IDX = 129
|
||||||
|
const val ACTIVITY_LEVEL_IDX = 130
|
||||||
|
const val HEART_RATE_CURRENT_IDX = 131
|
||||||
|
const val VITAL_POINTS_IDX = 132
|
||||||
|
const val ITEM_EFFECT_MENTAL_STATE_VALUE_IDX = 134
|
||||||
|
const val ITEM_EFFECT_MENTAL_STATE_MINUTES_REMAINING_IDX = 135
|
||||||
|
const val ITEM_EFFECT_ACTIVITY_LEVEL_VALUE_IDX = 136
|
||||||
|
const val ITEM_EFFECT_ACTIVITY_LEVEL_MINUTES_REMAINING_IDX = 137
|
||||||
|
const val ITEM_EFFECT_VITAL_POINTS_CHANGE_VALUE_IDX = 138
|
||||||
|
const val ITEM_EFFECT_VITAL_POINTS_CHANGE_MINUTES_REMAINING_IDX = 139
|
||||||
|
// 140 reserved
|
||||||
|
const val TRANSFORMATION_COUNT_DOWN_IDX = 141
|
||||||
|
const val TRANSFORMATION_HISTORY_START = 208
|
||||||
|
const val TRAINING_HP_IDX = 256
|
||||||
|
const val TRAINING_AP_IDX = 258
|
||||||
|
const val TRAINING_BP_IDX = 260
|
||||||
|
const val TRAINING_TIME_IDX = 266
|
||||||
|
const val RANK_IDX = 288
|
||||||
|
const val ABILITY_RARITY_IDX = 290
|
||||||
|
const val ABILITY_TYPE_IDX = 292
|
||||||
|
const val ABILITY_BRANCH_IDX = 294
|
||||||
|
const val ABILITY_RESET_IDX = 296
|
||||||
|
const val ITEM_TYPE_IDX = 299
|
||||||
|
const val ITEM_MULTIPLIER_IDX = 300
|
||||||
|
const val ITEM_REMAINING_TIME_IDX = 301
|
||||||
|
const val OTP_START_IDX = 352
|
||||||
|
const val OTP_END_IDX = 359
|
||||||
|
const val OTP2_START_IDX = 368
|
||||||
|
const val OTP2_END_IDX = 375
|
||||||
|
|
||||||
|
// DEVICE
|
||||||
|
const val VITAL_POINTS_CURRENT_IDX = 160
|
||||||
|
const val FIMRWARE_VERSION_IDX = 380
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCharacterInByteArray takes the BENfcCharacter and modifies the byte array with character
|
||||||
|
// data. At the time of writing this is used to write a parsed character into fresh unparsed
|
||||||
|
// device data when sending a character back to the device.
|
||||||
|
override fun setCharacterInByteArray(
|
||||||
|
character: NfcCharacter,
|
||||||
|
bytes: ByteArray
|
||||||
|
) {
|
||||||
|
val beCharacter = character as BENfcCharacter
|
||||||
|
beCharacter.appReserved1.copyInto(bytes,
|
||||||
|
APP_RESERVED_START, 0,
|
||||||
|
APP_RESERVED_SIZE
|
||||||
|
)
|
||||||
|
beCharacter.injuryStatus.ordinal.toUShort().toByteArray(bytes,
|
||||||
|
INJURY_STATUS_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
for(i in 0..<APP_RESERVED_2_SIZE) {
|
||||||
|
val index = APP_RESERVED_2_START + 2*i
|
||||||
|
beCharacter.appReserved2[i].toByteArray(bytes, index, ByteOrder.BIG_ENDIAN)
|
||||||
|
}
|
||||||
|
beCharacter.charIndex.toByteArray(bytes, CHARACTER_INDEX_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
beCharacter.dimId.toByteArray(bytes, DIM_ID_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
bytes[PHASE_IDX] = beCharacter.stage
|
||||||
|
bytes[ATTRIBUTE_IDX] = beCharacter.attribute.ordinal.toByte()
|
||||||
|
bytes[AGE_IN_DAYS_IDX] = beCharacter.ageInDays
|
||||||
|
beCharacter.getTrainingPp().toByteArray(bytes, TRAINING_PP_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
beCharacter.currentPhaseBattlesWon.toByteArray(bytes,
|
||||||
|
CURRENT_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
beCharacter.currentPhaseBattlesLost.toByteArray(bytes,
|
||||||
|
CURRENT_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
beCharacter.totalBattlesWon.toByteArray(bytes,
|
||||||
|
TOTAL_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
beCharacter.totalBattlesLost.toByteArray(bytes,
|
||||||
|
TOTAL_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
bytes[WIN_PCT_IDX] = beCharacter.getWinPercentage()
|
||||||
|
bytes[NEXT_ADVENTURE_MISSION_STAGE_IDX] = beCharacter.nextAdventureMissionStage
|
||||||
|
bytes[MOOD_IDX] = beCharacter.mood
|
||||||
|
bytes[ACTIVITY_LEVEL_IDX] = beCharacter.activityLevel
|
||||||
|
bytes[HEART_RATE_CURRENT_IDX] = beCharacter.heartRateCurrent.toByte()
|
||||||
|
beCharacter.vitalPoints.toByteArray(bytes, VITAL_POINTS_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
bytes[ITEM_EFFECT_MENTAL_STATE_VALUE_IDX] = beCharacter.itemEffectMentalStateValue
|
||||||
|
bytes[ITEM_EFFECT_MENTAL_STATE_MINUTES_REMAINING_IDX] = beCharacter.itemEffectMentalStateMinutesRemaining
|
||||||
|
bytes[ITEM_EFFECT_ACTIVITY_LEVEL_VALUE_IDX] = beCharacter.itemEffectActivityLevelValue
|
||||||
|
bytes[ITEM_EFFECT_ACTIVITY_LEVEL_MINUTES_REMAINING_IDX] = beCharacter.itemEffectActivityLevelMinutesRemaining
|
||||||
|
bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_VALUE_IDX] = beCharacter.itemEffectVitalPointsChangeValue
|
||||||
|
bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_MINUTES_REMAINING_IDX] = beCharacter.itemEffectVitalPointsChangeMinutesRemaining
|
||||||
|
beCharacter.transformationCountdown.toByteArray(bytes,
|
||||||
|
TRANSFORMATION_COUNT_DOWN_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
transformationHistoryToByteArray(beCharacter.transformationHistory, bytes)
|
||||||
|
beCharacter.trainingHp.toByteArray(bytes, TRAINING_HP_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
beCharacter.trainingAp.toByteArray(bytes, TRAINING_AP_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
beCharacter.trainingBp.toByteArray(bytes, TRAINING_BP_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
beCharacter.remainingTrainingTimeInMinutes.toByteArray(bytes,
|
||||||
|
TRAINING_TIME_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
bytes[ABILITY_RARITY_IDX] = beCharacter.abilityRarity.ordinal.toByte()
|
||||||
|
beCharacter.abilityType.toByteArray(bytes, ABILITY_TYPE_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
beCharacter.abilityBranch.toByteArray(bytes, ABILITY_BRANCH_IDX, ByteOrder.BIG_ENDIAN)
|
||||||
|
bytes[ABILITY_RESET_IDX] = beCharacter.abilityReset
|
||||||
|
bytes[RANK_IDX] = beCharacter.rank
|
||||||
|
bytes[ITEM_TYPE_IDX] = beCharacter.itemType
|
||||||
|
bytes[ITEM_MULTIPLIER_IDX] = beCharacter.itemMultiplier
|
||||||
|
bytes[ITEM_REMAINING_TIME_IDX] = beCharacter.itemRemainingTime
|
||||||
|
beCharacter.otp0.copyInto(bytes, OTP_START_IDX, 0, beCharacter.otp0.size)
|
||||||
|
beCharacter.otp1.copyInto(bytes, OTP2_START_IDX, 0, beCharacter.otp1.size)
|
||||||
|
bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX] = beCharacter.characterCreationFirmwareVersion.majorVersion
|
||||||
|
bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX+1] = beCharacter.characterCreationFirmwareVersion.minorVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalizeByteArrayFormat finalizes the byte array for BE NFC format by setting all the
|
||||||
|
// checksums, and duplicating the duplicate memory pages.
|
||||||
|
override fun finalizeByteArrayFormat(bytes: ByteArray) {
|
||||||
|
checksumCalculator.recalculateChecksums(bytes)
|
||||||
|
performPageBlockDuplications(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOperationCommandBytes(header: NfcHeader, operation: Byte): ByteArray {
|
||||||
|
return byteArrayOf(TagCommunicator.NFC_WRITE_COMMAND, OPERATION_PAGE, header.status, operation, header.dimIdBytes[0], header.dimIdBytes[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// parses a BENfcCharacter from a ByteArray produced by the TagCommunicator
|
||||||
|
override fun parseNfcCharacter(bytes: ByteArray): BENfcCharacter {
|
||||||
|
return BENfcCharacter(
|
||||||
|
appReserved1 = bytes.sliceArray(APP_RESERVED_START..<(APP_RESERVED_START + APP_RESERVED_SIZE)),
|
||||||
|
injuryStatus = NfcCharacter.InjuryStatus.entries[bytes.getUInt16(INJURY_STATUS_IDX, ByteOrder.BIG_ENDIAN).toInt()],
|
||||||
|
appReserved2 = bytes.copyIntoUShortArray(APP_RESERVED_2_START, APP_RESERVED_2_SIZE),
|
||||||
|
charIndex = bytes.getUInt16(CHARACTER_INDEX_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
dimId = bytes.getUInt16(DIM_ID_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
stage = bytes[PHASE_IDX],
|
||||||
|
attribute = NfcCharacter.Attribute.entries[bytes[ATTRIBUTE_IDX].toInt()],
|
||||||
|
ageInDays = bytes[AGE_IN_DAYS_IDX],
|
||||||
|
trainingPp = bytes.getUInt16(TRAINING_PP_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
currentPhaseBattlesWon = bytes.getUInt16(CURRENT_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
currentPhaseBattlesLost = bytes.getUInt16(CURRENT_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
totalBattlesWon = bytes.getUInt16(TOTAL_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
totalBattlesLost = bytes.getUInt16(TOTAL_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
nextAdventureMissionStage = bytes[NEXT_ADVENTURE_MISSION_STAGE_IDX],
|
||||||
|
mood = bytes[MOOD_IDX],
|
||||||
|
activityLevel = bytes[ACTIVITY_LEVEL_IDX],
|
||||||
|
heartRateCurrent = bytes[HEART_RATE_CURRENT_IDX].toUByte(),
|
||||||
|
vitalPoints = bytes.getUInt16(VITAL_POINTS_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
itemEffectMentalStateValue = bytes[ITEM_EFFECT_MENTAL_STATE_VALUE_IDX],
|
||||||
|
itemEffectMentalStateMinutesRemaining = bytes[ITEM_EFFECT_MENTAL_STATE_MINUTES_REMAINING_IDX],
|
||||||
|
itemEffectActivityLevelValue = bytes[ITEM_EFFECT_ACTIVITY_LEVEL_VALUE_IDX],
|
||||||
|
itemEffectActivityLevelMinutesRemaining = bytes[ITEM_EFFECT_ACTIVITY_LEVEL_MINUTES_REMAINING_IDX],
|
||||||
|
itemEffectVitalPointsChangeValue = bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_VALUE_IDX],
|
||||||
|
itemEffectVitalPointsChangeMinutesRemaining = bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_MINUTES_REMAINING_IDX],
|
||||||
|
transformationCountdownInMinutes = bytes.getUInt16(TRANSFORMATION_COUNT_DOWN_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
transformationHistory = buildTransformationHistory(bytes),
|
||||||
|
trainingHp = bytes.getUInt16(TRAINING_HP_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
trainingAp = bytes.getUInt16(TRAINING_AP_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
trainingBp = bytes.getUInt16(TRAINING_BP_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
remainingTrainingTimeInMinutes = bytes.getUInt16(TRAINING_TIME_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
abilityRarity = NfcCharacter.AbilityRarity.entries[bytes[ABILITY_RARITY_IDX].toInt()],
|
||||||
|
abilityType = bytes.getUInt16(ABILITY_TYPE_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
abilityBranch = bytes.getUInt16(ABILITY_BRANCH_IDX, ByteOrder.BIG_ENDIAN),
|
||||||
|
abilityReset = bytes[ABILITY_RESET_IDX],
|
||||||
|
rank = bytes[RANK_IDX],
|
||||||
|
itemType = bytes[ITEM_TYPE_IDX],
|
||||||
|
itemMultiplier = bytes[ITEM_MULTIPLIER_IDX],
|
||||||
|
itemRemainingTime = bytes[ITEM_REMAINING_TIME_IDX],
|
||||||
|
otp0 = bytes.sliceArray(OTP_START_IDX..OTP_END_IDX),
|
||||||
|
otp1 = bytes.sliceArray(OTP2_START_IDX..OTP2_END_IDX),
|
||||||
|
characterCreationFirmwareVersion = FirmwareVersion(
|
||||||
|
majorVersion = bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX],
|
||||||
|
minorVersion = bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX+1]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseHeader(headerBytes: ByteArray): NfcHeader {
|
||||||
|
Log.i(TagCommunicator.TAG, "Bytes in header: ${headerBytes.size}")
|
||||||
|
val header = NfcHeader(
|
||||||
|
deviceId = DeviceType.VitalBraceletBEDeviceType,
|
||||||
|
deviceSubType = DeviceSubType.Original,
|
||||||
|
vbCompatibleTagIdentifier = headerBytes.sliceArray(0..3), // this is a magic number used to verify that the tag is a VB.
|
||||||
|
status = headerBytes[8],
|
||||||
|
operation = headerBytes[9],
|
||||||
|
dimIdBytes = headerBytes.sliceArray(10..11),
|
||||||
|
appFlag = headerBytes[12],
|
||||||
|
nonce = headerBytes.sliceArray(13..15)
|
||||||
|
)
|
||||||
|
Log.i(TagCommunicator.TAG, "Header: $header")
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
// a block being 4 pages
|
||||||
|
private val firstIndicesOfBlocksToCopy = intArrayOf(32, 64, 96, 128, 256, 416)
|
||||||
|
private fun performPageBlockDuplications(data: ByteArray) {
|
||||||
|
for (firstIndex in firstIndicesOfBlocksToCopy) {
|
||||||
|
for (i in firstIndex..firstIndex + 15) {
|
||||||
|
data[i+16] = data[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transformationHistoryToByteArray(transformationHistory: Array<NfcCharacter.Transformation>, bytes: ByteArray) {
|
||||||
|
if (transformationHistory.size != 8) {
|
||||||
|
throw IllegalArgumentException("Transformation History must be exactly size 8")
|
||||||
|
}
|
||||||
|
for (phase in 0..<transformationHistory.size) {
|
||||||
|
var rootIdx = phase*4 + TRANSFORMATION_HISTORY_START
|
||||||
|
if (phase > 2) {
|
||||||
|
rootIdx += 4 // we skip 220-223 for some reason
|
||||||
|
}
|
||||||
|
if (phase > 5) {
|
||||||
|
rootIdx += 4 // we skip 236-239 for some reason
|
||||||
|
}
|
||||||
|
bytes[rootIdx] = transformationHistory[phase].toCharIndex
|
||||||
|
bytes[rootIdx+1] = transformationHistory[phase].yearsSince1988
|
||||||
|
bytes[rootIdx+2] = transformationHistory[phase].month
|
||||||
|
bytes[rootIdx+3] = transformationHistory[phase].day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildTransformationHistory(data: ByteArray): Array<NfcCharacter.Transformation> {
|
||||||
|
val transformationHistory = Array<NfcCharacter.Transformation>(8) { phase ->
|
||||||
|
var rootIdx = phase*4 + TRANSFORMATION_HISTORY_START
|
||||||
|
if (phase > 2) {
|
||||||
|
rootIdx += 4 // we skip 220-223 for some reason
|
||||||
|
}
|
||||||
|
if (phase > 5) {
|
||||||
|
rootIdx += 4 // we skip 236-239 for some reason
|
||||||
|
}
|
||||||
|
NfcCharacter.Transformation(
|
||||||
|
toCharIndex = data[rootIdx],
|
||||||
|
yearsSince1988 = data[rootIdx+1],
|
||||||
|
month = data[rootIdx+2],
|
||||||
|
day = data[rootIdx+3]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return transformationHistory
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package com.github.cfogrady.vbnfc.be
|
||||||
|
|
||||||
|
data class FirmwareVersion(val majorVersion: Byte, val minorVersion: Byte)
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
package com.github.cfogrady.vbnfc.data
|
||||||
|
|
||||||
|
import java.util.Objects
|
||||||
|
|
||||||
|
open class NfcCharacter(
|
||||||
|
val dimId: UShort,
|
||||||
|
var charIndex: UShort,
|
||||||
|
var stage: Byte,
|
||||||
|
var attribute: Attribute,
|
||||||
|
var ageInDays: Byte,
|
||||||
|
var nextAdventureMissionStage: Byte, // next adventure mission stage on the character's dim
|
||||||
|
var mood: Byte,
|
||||||
|
var vitalPoints: UShort,
|
||||||
|
var transformationCountdown: UShort,
|
||||||
|
var injuryStatus: InjuryStatus,
|
||||||
|
var trophies: UShort,
|
||||||
|
var currentPhaseBattlesWon: UShort,
|
||||||
|
var currentPhaseBattlesLost: UShort,
|
||||||
|
var totalBattlesWon: UShort,
|
||||||
|
var totalBattlesLost: UShort,
|
||||||
|
var activityLevel: Byte,
|
||||||
|
var heartRateCurrent: UByte,
|
||||||
|
var transformationHistory: Array<Transformation>
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class Transformation(
|
||||||
|
val toCharIndex: Byte,
|
||||||
|
val yearsSince1988: Byte,
|
||||||
|
val month: Byte,
|
||||||
|
val day: Byte)
|
||||||
|
|
||||||
|
enum class AbilityRarity {
|
||||||
|
None,
|
||||||
|
Common,
|
||||||
|
Rare,
|
||||||
|
SuperRare,
|
||||||
|
SuperSuperRare,
|
||||||
|
UltraRare,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Attribute {
|
||||||
|
None,
|
||||||
|
Virus,
|
||||||
|
Data,
|
||||||
|
Vaccine,
|
||||||
|
Free
|
||||||
|
}
|
||||||
|
enum class InjuryStatus {
|
||||||
|
None,
|
||||||
|
Injury,
|
||||||
|
InjuryHealed,
|
||||||
|
InjuryTwo,
|
||||||
|
InjuryTwoHealed,
|
||||||
|
InjuryThree,
|
||||||
|
InjuryThreeHealed,
|
||||||
|
InjuryFour,
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTransformationHistoryString(separator: String = System.lineSeparator()): String {
|
||||||
|
val builder = StringBuilder()
|
||||||
|
for(i in transformationHistory.indices) {
|
||||||
|
builder.append(transformationHistory[i])
|
||||||
|
if(i != transformationHistory.size-1) {
|
||||||
|
builder.append(separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as NfcCharacter
|
||||||
|
|
||||||
|
if (dimId != other.dimId) return false
|
||||||
|
if (charIndex != other.charIndex) return false
|
||||||
|
if (stage != other.stage) return false
|
||||||
|
if (attribute != other.attribute) return false
|
||||||
|
if (ageInDays != other.ageInDays) return false
|
||||||
|
if (nextAdventureMissionStage != other.nextAdventureMissionStage) return false
|
||||||
|
if (mood != other.mood) return false
|
||||||
|
if (vitalPoints != other.vitalPoints) return false
|
||||||
|
if (transformationCountdown != other.transformationCountdown) return false
|
||||||
|
if (injuryStatus != other.injuryStatus) return false
|
||||||
|
if (trophies != other.trophies) return false
|
||||||
|
if (currentPhaseBattlesWon != other.currentPhaseBattlesWon) return false
|
||||||
|
if (currentPhaseBattlesLost != other.currentPhaseBattlesLost) return false
|
||||||
|
if (totalBattlesWon != other.totalBattlesWon) return false
|
||||||
|
if (totalBattlesLost != other.totalBattlesLost) return false
|
||||||
|
if (activityLevel != other.activityLevel) return false
|
||||||
|
if (heartRateCurrent != other.heartRateCurrent) return false
|
||||||
|
if (!transformationHistory.contentEquals(other.transformationHistory)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return Objects.hash(
|
||||||
|
dimId,
|
||||||
|
charIndex,
|
||||||
|
stage,
|
||||||
|
attribute,
|
||||||
|
ageInDays,
|
||||||
|
nextAdventureMissionStage,
|
||||||
|
mood,
|
||||||
|
vitalPoints,
|
||||||
|
transformationCountdown,
|
||||||
|
injuryStatus,
|
||||||
|
trophies,
|
||||||
|
currentPhaseBattlesWon,
|
||||||
|
currentPhaseBattlesLost,
|
||||||
|
totalBattlesWon,
|
||||||
|
totalBattlesLost,
|
||||||
|
activityLevel,
|
||||||
|
heartRateCurrent,
|
||||||
|
transformationHistory.contentHashCode()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return """NfcCharacter(
|
||||||
|
dimId=$dimId,
|
||||||
|
charIndex=$charIndex,
|
||||||
|
stage=$stage,
|
||||||
|
attribute=$attribute,
|
||||||
|
ageInDays=$ageInDays,
|
||||||
|
nextAdventureMissionStage=$nextAdventureMissionStage,
|
||||||
|
mood=$mood,
|
||||||
|
vitalPoints=$vitalPoints,
|
||||||
|
transformationCountdown=$transformationCountdown,
|
||||||
|
injuryStatus=$injuryStatus,
|
||||||
|
trophies=$trophies,
|
||||||
|
currentPhaseBattlesWon=$currentPhaseBattlesWon,
|
||||||
|
currentPhaseBattlesLost=$currentPhaseBattlesLost,
|
||||||
|
totalBattlesWon=$totalBattlesWon,
|
||||||
|
totalBattlesLost=$totalBattlesLost,
|
||||||
|
activityLevel=$activityLevel,
|
||||||
|
heartRateCurrent=$heartRateCurrent,
|
||||||
|
transformationHistory=${transformationHistory.contentToString()}
|
||||||
|
)"""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package com.github.cfogrady.vbnfc.data
|
||||||
|
|
||||||
|
class NfcData(val nfcCharacter: NfcCharacter, val nfcDevice: NfcDevice)
|
||||||
@ -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()]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
)
|
||||||
4
vb-nfc-reader/src/main/res/values/arrays.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="substitutionArray" />
|
||||||
|
</resources>
|
||||||
6
vb-nfc-reader/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="password1" />
|
||||||
|
<string name="password2" />
|
||||||
|
<string name="decryptionKey" />
|
||||||
|
</resources>
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.github.cfogrady.vbnfc
|
||||||
|
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class CryptographicTransformerTest {
|
||||||
|
|
||||||
|
val testTagId = byteArrayOf(0x04, 0x40, 0xaf.toByte(), 0xa2.toByte(), 0xee.toByte(), 0x0f, 0x90.toByte())
|
||||||
|
|
||||||
|
val testAesKey = "8A4PEGIXJS454EFRTX9F5PCT"
|
||||||
|
val testHmacKey1 = "40nz2LdPI99D+x748XmQmw=="
|
||||||
|
val testHmacKey2 = "5Jz9lWtNg28qxqIBoR5kLw=="
|
||||||
|
val testSubstitutionCipher = intArrayOf(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
@Test
|
||||||
|
fun createPasswordCreatesExpectedPassword() {
|
||||||
|
|
||||||
|
mockkStatic(android.util.Log::class)
|
||||||
|
every { android.util.Log.i(any<String>(), any<String>()) } answers {
|
||||||
|
val message = it.invocation.args[1] as String
|
||||||
|
println(message)
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
val cryptographicTransformer = CryptographicTransformer(testHmacKey1, testHmacKey2, testAesKey, testSubstitutionCipher)
|
||||||
|
|
||||||
|
val result = cryptographicTransformer.createNfcPassword(testTagId)
|
||||||
|
val expected = "a3c83dd7"
|
||||||
|
|
||||||
|
Assert.assertEquals(expected, result.toHexString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
@Test
|
||||||
|
fun dataEncryptionAndDecryptionWorks() {
|
||||||
|
val cryptographicTransformer = CryptographicTransformer(testHmacKey1, testHmacKey2, testAesKey, testSubstitutionCipher)
|
||||||
|
|
||||||
|
val characters = listOf(
|
||||||
|
"000000000000000000000000000000000000000000000000000000000000000010400010040010001000000000001094104000100400100010000000000010940000000000000000000400840203008d0000000000000000000400840203008d00000006000300060003000001010014000000060003000600030000010100140156025309850000000000000000d6100156025309850000000000000000d610000000000000280000000000000000280000000000000000030c08302402279424022624022524022424022324020151002402290124022904240229000000f2ffffffffffffffffffffffff000000f4ffffffffffffffff00000000000000f80000000000000000000014860000009a0000000000000000000014860000009a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010101000000000000008702020202020202020000000001010033000000000000000000000000000000000000000000000000000000000000000014c5400000000000000000000000001914c540000000000000000000000000190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"000000000000000000000000000000000000000000000000000000000000000010400010040010001000000000001094104000100400100010000000000010940000000000000000000500820302008c0000000000000000000500820302008c000b00030002000b000b000001010028000b00030002000b000b0000010100280464028b04b4000000000000000308b80464028b04b4000000000000000308b80474000000001500000000000000008d0002000000000000000000002401062d240105240104240103240102240101c80024010601240106042401060000008605240116ffffffffffffffff00000038ffffffffffffffff00000000000000f80005000f000a00000000046b0000008d0005000f000a00000000046b0000008d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030303030303030300000000000000210404040404040404000000000101003a000000000000000000000000000000000000000000000000000000000000000014c5400000000000000000000000001914c540000000000000000000000000190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
)
|
||||||
|
val expectedEncryptions = listOf(
|
||||||
|
"74b46e2d78292ce251a84e644f5451dd4aa2c6d782a9aeef2c3229103c7a26e5d9c2c53d2f63077a1cf9ae18efc382edca9e50d7d861e2927cd9df2494a0772ff1b8791bae7883a862d218559277db6a9665f18da0f1caa92ccc903dbc80e66367ee39b2f243cc0b5bfcc2131dfeb9d361179d4033517c4ebbceea15ab9bfb27a9a05cb45e5dfe37287a010db3bcfd9a6f1cc54a31ea7da193dbd341e83997ad80713d5fd19c7d76a6dbedfb25049ab5472e4094d0949f19c853b2ac6c6827412904fd1683fae61ea1a64508e9243e04bdfdee63ff98321204fd4fd2a17112c0bd5aa7c11986ea7bc00110dedea2d0644ea5ea3f6e928f8c6c98107eeedbfd1b774d07df237dc294f3b8fccaa8ab2e1c59de2c695f367214f55accc2e43ca0b2c021161d3c8b9ea2405717d6884d089d848d82b37a9ed711a8e9809336d05bab0091face97671e9260d4ae741e94ef1f8d950ff390dd05bfa28adb6b72a8214192be530af26db6abda69dce0707d3d92f44beb1f01f24d7c1123ad25d37349bf7d5777723f1acb56d2f96ba435ffa505c3ae3d270c85bcb64aabac0d15d8128f7fe34cc303a74f8d42871a25d452a1ef033b824b17e51b808b5363f2f1da5a57c3fcc881b57028e76155f6d49e35e43bd142747c3a4a14217fa8600a6e0d3cdbc6b5598510023ad0897d38ca04e78a78e198ee76e784d6337ad8fa65e4b617ea11433fb3d9d7d3269a7b5e1ff9a4cd723f3fc4f81768816619fadf21b5276f31d704bba1a3a9afb0363cca3ddb272bdced2d252e21824d2828b7c36cdd37202cd5a6b5224e505f87188d3c63af33c8916aa8ae0116dd1028c370236591a4413559fe40d14dcdf72959a122def9b4c6cf5d928142a66b0dcf0ffa5d447b53a8661e3578a49d632e0285d180814116a3e78fc00fc01106a248af8afa329a69054b827e41a62abdc65074554fa2f07b773d56bae73252efa374f47397c1d36e056eb9d32b6dcf18f40669a5b23723c475c50c5ba4154b550c67dd90d4a6919686c69fdcfd7d4d988a0df5e420b6240068ae8aa07c88c38bac3b6ce7a87c8df0f540cc3c5be2180b70127c523102ed9be2cfb25a032089d47d05db6dbde2758239ad54b94756906922ed7e7c4ecf6b256c658417fa8003100c72de1dee627c1a0c670dbc91596e4dc0d4393eb24884b41979e8ff3d5b0583416750d1d856a2a689cd",
|
||||||
|
"74b46e2d78292ce251a84e644f5451dd4aa2c6d782a9aeef2c3229103c7a26e5d9c2c53d2f63077a1cf9ae18efc382edca9e50d7d861e2927cd9df2494a0772ff1b8791bae7883a862d318539376db6b9665f18da0f1caa92ccd903bbd81e66267e539b7f242cc065bf4c2131dfeb9ef611c9d4533507c43bbc6ea15ab9bfb1bac925c6c536cfe37287a010db3bf23326a2ec5923cdb7da193dbd341e83a490584053d5fd19c4076a6dbedfb25049a10472c4094d0949f19cb5fba9c6c6b06f82907de1680dbe61d86a64629e9273e9dbdfded4cff98313d04fd4cfda17112b4478159281986ea7bc00110dedea2d0a84ea5ea3f6e928f8c6c98107eeedbfd1b774807d02377c294f3b8ec27a8ab2e0b59db2c665f3c7214f55adc2fe43ca0a5c021161d3c8b9ea2405717d6884d089d848d82b37a9ed711a8e9809336d05bab0091face97671e9260d4ae741e94ef1f8d950ff390dd05bfa28adb6b72a8214190bc5108f06fb4a9da69dce0707d3d34f24ded1907f44b7a1123ad25d37349b67d5777723f1acb56d2f96ba435ffa505c3ae3d270c85bcb64aabac0d15d8128f7fe34cc303a74f8d42871a25d452a1ef033b824b17e51b808b5363f2f1da5a57c3fcc881b57028e76155f6d49e35e43bd142747c3a4a14217fa8600a6e0d3cdbc6b5598510023ad0897d38ca04e78a78e198ee76e784d6337ad8fa65e4b617ea11433fb3d9d7d3269a7b5e1ff9a4cd723f3fc4f81768816619fadf21b5276f31d704bba1a3a9afb0363cca3ddb272bdced2d252e21824d2828b7c36cdd37202cd5a6b5224e505f87188d3c63af33c8916aa8ae0116dd1028c370236591a4413559fe40d14dcdf72959a122def9b4c6cf5d928142a66b0dcf0ffa5d447b53a8661e3578a49d632e0285d180814116a3e78fc00fc01106a248af8afa329a69054b827e41a62abdc65074554fa2f07b773d56bae73252efa374f47397c1d36e056eb9d32b6dcf18f40669a5b23723c475c50c5ba4154b550c67dd90d4a6919686c69fdcfd7d4d988a0df5e420b6240068ae8aa07c88c38bac3b6ce7a87c8df0f540cc3c5be2180b70127c523102ed9be2cfb25a032089d47d05db6dbde2758239ad54b94756906922ed7e7c4ecf6b256c658417fa8003100c72de1dee627c1a0c670dbc91596e4dc0d4393eb24884b41979e8ff3d5b0583416750d1d856a2a689cd",
|
||||||
|
)
|
||||||
|
for (i in characters.indices) {
|
||||||
|
val encrypted = cryptographicTransformer.encryptData(characters[i].hexToByteArray(), testTagId)
|
||||||
|
Assert.assertEquals(expectedEncryptions[i], encrypted.toHexString())
|
||||||
|
val decrypted = cryptographicTransformer.decryptData(encrypted, testTagId)
|
||||||
|
Assert.assertEquals(characters[i], decrypted.toHexString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||