Compare commits

..
Author SHA1 Message Date
nacabaroandGitHub 5d996edc1e Merge branch 'main' into ui/home_screen 2025-01-16 01:05:21 +01:00
nacabaroandGitHub 074ced1082 Merge pull request #17 from cfogrady/ObscuredDex
Algorithm to obscure undiscovered digimon
2025-01-16 01:04:32 +01:00
Nacho 5a1d52aa1d A lot more things
- Moved scan button to inside the home screen
- Added items screen
- Scanning functionality updated to work with BEms
- Different home screen layouts depending on the configuration (BE with BEm, BE with DIm, since they don't have the same data, looking at you, special training)
- It is now possible to import BEms
- Character size is now more accurate, (no more big babies)
- Once sent to the watch, characters are deleted from the app, and it shouldn't fail
- Updated domain model to support evolution history.

TODO:
- Reorganize some of the code, mostly SQL queries and reorganize the scan screen functionality
- Create home layout for the VB watch
- Start figuring out reading data from the VB
    - Also create VB data domain model
    - Also start figuring out writing to the VB (it's 3 steps)
- Block off scan button until secrets are imported
- Start working with the dex
    - Update domain model
    - Use cfogrady's blank character when not seen
- Cancel character upload in case the app database does not contain the card for it (otherwise, the watch will delete the character)
- Export character data (in case of any app issues or changing phones)

(Also added some more icons)

Woo
2025-01-16 01:03:57 +01:00
Christopher O'Grady 98bd74c544 Algorithm to obscure undiscovered digimon
Logic still needs to be implemented to distinguish discovered from undiscovered.
2025-01-12 21:36:02 -05:00
Nacho ce1cf3eddb Quicm fix, DisposableEffect isn't reloading when isDoneSendCard is true
TODO: add more disposableeffects
2025-01-12 13:50:41 +01:00
nacabaroandGitHub 0bd745ac86 Merge pull request #16 from nacabaro/ui/home_screen
UI/home screen
2025-01-12 10:23:49 +01:00
nacabaroandGitHub 0d999925b5 Merge pull request #14 from cfogrady/home_screen
Fix arrays for NFCCharacter
2025-01-12 10:23:09 +01:00
nacabaroandGitHub dd893a08da Merge pull request #15 from nacabaro/ui/home_screen
A few things, again...
2025-01-12 10:22:57 +01:00
Christopher O'Grady c5e8236e19 Fix arrays for NFCCharacter 2025-01-11 22:57:07 -05:00
Nacho cac5198488 A few things, again...
- Added ability to write character (incomplete)
- Renamed BottomNavItem.kt to NavigationItems.kt, as it covers the entire application navigation, not just the bottom navigation bar
- Modified the ScanScreenController and the Impl to accomodate writing characters on the BE (need to do the VB later on)
- Modified repositories to fetch data from the database and additional information needed to convert back to NfcCharacter
- Function to convert to NfcCharacter

Originally this was also going to cover the home screen, since my idea was to have marked as active (the one shown in the home screen) be the one sent to the watch, but for testing I have added a "send to bracelet" button on the pop-up on the storage screen.
2025-01-12 00:57:34 +01:00
nacabaroandGitHub 7f22650601 Merge pull request #13 from cfogrady/TransparentBackground
Make background on characters and icons transparent.
2025-01-10 23:10:04 +01:00
Christopher O'Grady 1088e5259a Make background on characters and icons transparent. 2025-01-10 14:24:12 -05:00
nacabaroandGitHub eabf8770cc Merge pull request #12 from cfogrady/IntegrateSecretsWithApp
Integrate secrets with app
2025-01-10 19:38:26 +01:00
nacabaroandGitHub da5e6c6628 Merge pull request #11 from cfogrady/SecretsRepo
Add Secrets Repo Using a Proto DataStore
2025-01-10 19:38:13 +01:00
nacabaroandGitHub 4c0e2fafd0 Merge pull request #10 from cfogrady/APK_Import
APK Import
2025-01-10 19:38:01 +01:00
Christopher O'Grady 4529925906 Integrate SecretsRepository and ApkImporter with app
Refactor methods out of MainActivity into Controllers for ScanScreen and SettingsScreen
2025-01-10 13:21:53 -05:00
Christopher O'Grady c456d455ef Implement Secrets Repository
Add proto plugin and dependencies
Create Secrets Proto
Create Secrets Proto DataStore
Replace old secrets with proto secrets
Fix importers and tests to use new proto secrets.
2025-01-09 15:24:10 -05:00
Christopher O'Grady 9871f0420f Verify Secret correctness as part of loading.
Improve tests
2025-01-08 22:12:03 -05:00
Christopher O'Grady f4974c8705 Import APK in tests 2025-01-08 21:01:37 -05:00
nacabaroandGitHub 5d6e3743f0 Merge pull request #9 from nacabaro/nfc/upload_mon_new_model
Removed unused imports that'd cause compile issues.
2025-01-08 02:37:01 +01:00
nacabaroandGitHub e4366ce15c Merge pull request #8 from nacabaro/nfc/upload_mon_new_model
Nfc/upload mon new model
2025-01-08 02:33:22 +01:00
66 changed files with 2324 additions and 399 deletions
+4
View File
@@ -6,3 +6,7 @@
local.properties
app/src/main/res/values/keys.xml
app/src/test/resources/com/github/nacabaro/vbhelper/source/com.bandai.vitalbraceletarena.apk
app/src/test/resources/com/github/nacabaro/vbhelper/source/classes.dex
+26
View File
@@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp") version "2.0.21-1.0.27"
id("com.google.protobuf")
}
android {
@@ -38,6 +39,29 @@ android {
buildFeatures {
compose = true
}
lint {
baseline = file("lint-baseline.xml")
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.27.0"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}
}
}
}
dependencies {
@@ -51,6 +75,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.datastore)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
@@ -64,5 +89,6 @@ dependencies {
debugImplementation(libs.androidx.ui.test.manifest)
implementation("androidx.navigation:navigation-compose:2.7.0")
implementation("com.google.android.material:material:1.2.0")
implementation(libs.protobuf.javalite)
implementation("androidx.compose.material:material")
}
@@ -0,0 +1,16 @@
package com.github.nacabaro.vbhelper
interface ActivityLifecycleListener {
fun onPause()
fun onResume()
companion object {
fun noOpInstance(): ActivityLifecycleListener {
return object: ActivityLifecycleListener {
override fun onPause() {}
override fun onResume() {}
}
}
}
}
@@ -1,11 +1,8 @@
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.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -13,18 +10,13 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vb.dim.card.BemCard
import com.github.cfogrady.vb.dim.card.DimReader
import com.github.nacabaro.vbhelper.navigation.AppNavigation
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.Dim
import com.github.nacabaro.vbhelper.domain.Sprites
@@ -32,39 +24,74 @@ import com.github.nacabaro.vbhelper.domain.Character
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.SettingsScreenController
import com.github.nacabaro.vbhelper.source.ApkSecretsImporter
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.util.Date
import java.util.GregorianCalendar
class MainActivity : ComponentActivity() {
private lateinit var nfcAdapter: NfcAdapter
private lateinit var deviceToCryptographicTransformers: Map<UShort, CryptographicTransformer>
private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null)
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
// EXTRACTED DIRECTLY FROM EXAMPLE APP
override fun onCreate(savedInstanceState: Bundle?) {
deviceToCryptographicTransformers = getMapOfCryptographicTransformers()
private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()
private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
if( onActivityLifecycleListeners[key] != null) {
throw IllegalStateException("Key is already in use")
}
onActivityLifecycleListeners[key] = activityLifecycleListener
}
private fun unregisterActivityLifecycleListener(key: String) {
onActivityLifecycleListeners.remove(key)
}
override fun onCreate(savedInstanceState: Bundle?) {
registerFileActivityResult()
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (maybeNfcAdapter == null) {
Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show()
finish()
return
}
nfcAdapter = maybeNfcAdapter
val application = applicationContext as VBHelper
val settingsScreenController = SettingsScreenController.Factory(this, ApkSecretsImporter(), application.container.dataStoreSecretsRepository)
.buildSettingScreenHandlers()
val scanScreenController = ScanScreenControllerImpl(
application.container.dataStoreSecretsRepository.secretsFlow,
this::handleReceivedNfcCharacter,
this,
this::registerActivityLifecycleListener,
this::unregisterActivityLifecycleListener)
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
VBHelperTheme {
MainApplication()
MainApplication(settingsScreenController, scanScreenController)
}
}
Log.i("MainActivity", "Activity onCreated")
}
override fun onPause() {
super.onPause()
Log.i("MainActivity", "onPause")
for(activityListener in onActivityLifecycleListeners) {
activityListener.value.onPause()
}
}
override fun onResume() {
super.onResume()
Log.i("MainActivity", "Resume")
for(activityListener in onActivityLifecycleListeners) {
activityListener.value.onResume()
}
}
private fun registerFileActivityResult() {
@@ -83,13 +110,17 @@ class MainActivity : ComponentActivity() {
inputStream.use { fileReader ->
val dimReader = DimReader()
val card = dimReader.readCard(fileReader, false)
Log.i("MainActivity", "Card name: ${card is BemCard}")
val dimModel = Dim(
dimId = card.header.dimId,
logo = card.spriteData.sprites[0].pixelData,
name = card.spriteData.text, // TODO Make user write card name
stageCount = card.adventureLevels.levels.size,
logoHeight = card.spriteData.sprites[0].height,
logoWidth = card.spriteData.sprites[0].width
logoWidth = card.spriteData.sprites[0].width,
isBEm = card is BemCard
)
val dimId = storageRepository
@@ -98,7 +129,11 @@ class MainActivity : ComponentActivity() {
val characters = card.characterStats.characterEntries
var spriteCounter = 10
var spriteCounter = when (card is BemCard) {
true -> 55
false -> 10
}
val domainCharacters = mutableListOf<Character>()
for (index in 0 until characters.size) {
@@ -122,12 +157,14 @@ class MainActivity : ComponentActivity() {
)
// TODO: Improve this
if (index == 0) {
spriteCounter += 6
} else if (index == 1) {
spriteCounter += 7
} else {
if (card is BemCard) {
spriteCounter += 14
} else {
when (index) {
0 -> spriteCounter += 6
1 -> spriteCounter += 7
else -> spriteCounter += 14
}
}
}
@@ -154,26 +191,10 @@ class MainActivity : ComponentActivity() {
}
@Composable
private fun MainApplication() {
var isDoneReadingCharacter by remember { mutableStateOf(false) }
private fun MainApplication(settingsScreenController: SettingsScreenController, scanScreenController: ScanScreenControllerImpl) {
AppNavigation(
isDoneReadingCharacter = isDoneReadingCharacter,
onClickRead = {
handleTag {
val character = it.receiveCharacter()
nfcCharacter.value = character
val importStatus = addCharacterScannedIntoDatabase()
isDoneReadingCharacter = true
importStatus
}
},
onClickScan = {
isDoneReadingCharacter = false
},
applicationNavigationHandlers = AppNavigationHandlers(settingsScreenController, scanScreenController),
onClickImportCard = {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
@@ -184,73 +205,12 @@ class MainActivity : ComponentActivity() {
)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun getMapOfCryptographicTransformers(): Map<UShort, CryptographicTransformer> {
return mapOf(
Pair(DeviceType.VitalBraceletBEDeviceType,
CryptographicTransformer(readableHmacKey1 = resources.getString(R.string.password1),
readableHmacKey2 = resources.getString(R.string.password2),
aesKey = resources.getString(R.string.decryptionKey),
substitutionCipher = resources.getIntArray(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)))
)
}
private fun handleReceivedNfcCharacter(character: NfcCharacter): String {
nfcCharacter.value = character
// 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))
}
val importStatus = addCharacterScannedIntoDatabase()
// 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)
}
return importStatus
}
//
@@ -271,9 +231,7 @@ class MainActivity : ComponentActivity() {
.dimDao()
.getDimById(nfcCharacter.value!!.dimId.toInt())
if (dimData == null) {
return "Card not found"
}
if (dimData == null) return "Card not found"
val cardCharData = storageRepository
.characterDao()
@@ -299,60 +257,63 @@ class MainActivity : ComponentActivity() {
characterType = when (nfcCharacter.value) {
is BENfcCharacter -> com.github.nacabaro.vbhelper.domain.DeviceType.BEDevice
else -> com.github.nacabaro.vbhelper.domain.DeviceType.VBDevice
}
},
isActive = true
)
storageRepository
.userCharacterDao()
.clearActiveCharacter()
val characterId: Long = storageRepository
.userCharacterDao()
.insertCharacterData(characterData)
if (nfcCharacter.value is BENfcCharacter) {
val beCharacter = nfcCharacter as MutableStateFlow<BENfcCharacter?>
val beCharacter = nfcCharacter.value as BENfcCharacter
val extraCharacterData = BECharacterData(
id = characterId,
trainingHp = beCharacter.value!!.trainingHp.toInt(),
trainingAp = beCharacter.value!!.trainingAp.toInt(),
trainingBp = beCharacter.value!!.trainingBp.toInt(),
remainingTrainingTimeInMinutes = beCharacter.value!!.remainingTrainingTimeInMinutes.toInt(),
itemEffectActivityLevelValue = beCharacter.value!!.itemEffectActivityLevelValue.toInt(),
itemEffectMentalStateValue = beCharacter.value!!.itemEffectMentalStateValue.toInt(),
itemEffectMentalStateMinutesRemaining = beCharacter.value!!.itemEffectMentalStateMinutesRemaining.toInt(),
itemEffectActivityLevelMinutesRemaining = beCharacter.value!!.itemEffectActivityLevelMinutesRemaining.toInt(),
itemEffectVitalPointsChangeValue = beCharacter.value!!.itemEffectVitalPointsChangeValue.toInt(),
itemEffectVitalPointsChangeMinutesRemaining = beCharacter.value!!.itemEffectVitalPointsChangeMinutesRemaining.toInt(),
abilityRarity = beCharacter.value!!.abilityRarity,
abilityType = beCharacter.value!!.abilityType.toInt(),
abilityBranch = beCharacter.value!!.abilityBranch.toInt(),
abilityReset = beCharacter.value!!.abilityReset.toInt(),
rank = beCharacter.value!!.abilityReset.toInt(),
itemType = beCharacter.value!!.itemType.toInt(),
itemMultiplier = beCharacter.value!!.itemMultiplier.toInt(),
itemRemainingTime = beCharacter.value!!.itemRemainingTime.toInt(),
trainingHp = beCharacter.trainingHp.toInt(),
trainingAp = beCharacter.trainingAp.toInt(),
trainingBp = beCharacter.trainingBp.toInt(),
remainingTrainingTimeInMinutes = beCharacter.remainingTrainingTimeInMinutes.toInt(),
itemEffectActivityLevelValue = beCharacter.itemEffectActivityLevelValue.toInt(),
itemEffectMentalStateValue = beCharacter.itemEffectMentalStateValue.toInt(),
itemEffectMentalStateMinutesRemaining = beCharacter.itemEffectMentalStateMinutesRemaining.toInt(),
itemEffectActivityLevelMinutesRemaining = beCharacter.itemEffectActivityLevelMinutesRemaining.toInt(),
itemEffectVitalPointsChangeValue = beCharacter.itemEffectVitalPointsChangeValue.toInt(),
itemEffectVitalPointsChangeMinutesRemaining = beCharacter.itemEffectVitalPointsChangeMinutesRemaining.toInt(),
abilityRarity = beCharacter.abilityRarity,
abilityType = beCharacter.abilityType.toInt(),
abilityBranch = beCharacter.abilityBranch.toInt(),
abilityReset = beCharacter.abilityReset.toInt(),
rank = beCharacter.abilityReset.toInt(),
itemType = beCharacter.itemType.toInt(),
itemMultiplier = beCharacter.itemMultiplier.toInt(),
itemRemainingTime = beCharacter.itemRemainingTime.toInt(),
otp0 = "", //beCharacter.value!!.otp0.toString(),
otp1 = "", //beCharacter.value!!.otp1.toString(),
minorVersion = beCharacter.value!!.characterCreationFirmwareVersion.minorVersion.toInt(),
majorVersion = beCharacter.value!!.characterCreationFirmwareVersion.majorVersion.toInt(),
minorVersion = beCharacter.characterCreationFirmwareVersion.minorVersion.toInt(),
majorVersion = beCharacter.characterCreationFirmwareVersion.majorVersion.toInt(),
)
storageRepository
.userCharacterDao()
.insertBECharacterData(extraCharacterData)
val transformationHistoryWatch = beCharacter.value!!.transformationHistory
val domainTransformationHistory = transformationHistoryWatch.map { item ->
TransformationHistory(
monId = characterId,
toCharIndex = item.toCharIndex.toInt(),
year = item.year.toInt(),
month = item.month.toInt(),
day = item.day.toInt()
)
val transformationHistoryWatch = beCharacter.transformationHistory
transformationHistoryWatch.map { item ->
if (item.toCharIndex.toInt() != 255) {
val date = GregorianCalendar(item.year.toInt(), item.month.toInt(), item.day.toInt())
.time
.time
storageRepository
.characterDao()
.insertTransformation(characterId, item.toCharIndex.toInt(), dimData.id, date)
}
}
storageRepository
.userCharacterDao()
.insertTransformationHistory(*domainTransformationHistory.toTypedArray())
} else {
} else if (nfcCharacter.value is VBNfcCharacter) {
return "Not implemented yet"
}
@@ -1,48 +1,116 @@
package com.github.nacabaro.vbhelper.components
import android.graphics.Bitmap
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getBitmap
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import com.github.nacabaro.vbhelper.utils.getObscuredBitmap
import java.nio.ByteBuffer
@Composable
fun CharacterEntry(
icon: BitmapData,
obscure: Boolean = false,
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.shapes.medium,
multiplier: Int = 3,
onClick: () -> Unit = { }
) {
val bitmap = remember (icon.bitmap) {
Bitmap.createBitmap(icon.width, icon.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(icon.bitmap))
}
if(obscure) icon.getObscuredBitmap() else icon.getBitmap()
}
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
val density: Float = LocalContext.current.resources.displayMetrics.density
val dpSize = (icon.width * multiplier / density).dp
Card(
shape = MaterialTheme.shapes.medium,
shape = shape,
onClick = onClick,
modifier = modifier
.aspectRatio(1f)
.padding(8.dp)
.size(96.dp)
) {
Image(
bitmap = imageBitmap,
contentDescription = "Icon",
filterQuality = FilterQuality.None,
Box(
contentAlignment = Alignment.BottomCenter,
modifier = Modifier
.padding(8.dp)
.fillMaxSize()
)
.padding(16.dp)
) {
Image(
bitmap = imageBitmap,
contentDescription = "Icon",
filterQuality = FilterQuality.None,
modifier = Modifier
.size(dpSize)
)
}
}
}
@Composable
fun ItemDisplay(
icon: Int,
textValue: String,
modifier: Modifier = Modifier,
iconSize: Dp = 48.dp,
textSize: TextUnit = 24.sp,
definition: String = "",
) {
val context = LocalContext.current
Card(
modifier = modifier,
shape = androidx.compose.material.MaterialTheme.shapes.small,
onClick = {
Toast.makeText(context, definition, Toast.LENGTH_SHORT).show()
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
) {
Icon(
painter = painterResource(icon),
contentDescription = "Vitals",
modifier = Modifier
.padding(8.dp)
.size(iconSize)
)
Text(
text = textValue,
textAlign = TextAlign.Center,
fontSize = textSize,
fontFamily = MaterialTheme.typography.titleLarge.fontFamily,
fontWeight = FontWeight.Bold
)
}
}
}
@@ -1,6 +1,5 @@
package com.github.nacabaro.vbhelper.components
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -15,9 +14,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.utils.BitmapData
import java.nio.ByteBuffer
import com.github.nacabaro.vbhelper.utils.getBitmap
@Composable
fun DexDiMEntry(
@@ -26,12 +26,10 @@ fun DexDiMEntry(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val bitmap = remember (logo.bitmap) {
Bitmap.createBitmap(logo.width, logo.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(logo.bitmap))
}
}
val bitmap = remember (logo.bitmap) { logo.getBitmap() }
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
val density: Float = LocalContext.current.resources.displayMetrics.density
val dpSize = (logo.width * 4 / density).dp
Card (
shape = MaterialTheme.shapes.medium,
@@ -50,7 +48,7 @@ fun DexDiMEntry(
filterQuality = FilterQuality.None,
modifier = Modifier
.padding(8.dp)
.size(64.dp)
.size(dpSize)
)
Text(
text = name,
@@ -23,6 +23,7 @@ fun TopBanner(
modifier: Modifier = Modifier,
onGearClick: (() -> Unit)? = null,
onBackClick: (() -> Unit)? = null,
onScanClick: (() -> Unit)? = null
) {
Box( // Use Box to overlay elements
modifier = modifier
@@ -37,7 +38,7 @@ fun TopBanner(
modifier = Modifier
.align(Alignment.Center) // Center the text
)
if (onGearClick != null) {
if (onGearClick != null) {
IconButton(
onClick = onGearClick,
modifier = Modifier
@@ -50,7 +51,18 @@ fun TopBanner(
}
}
if (onBackClick != null) {
if (onScanClick != null) {
IconButton(
onClick = onScanClick,
modifier = Modifier
.align(Alignment.CenterStart) // Place gear icon at the end
) {
Icon(
painter = painterResource(R.drawable.baseline_nfc_24), // Use a gear icon
contentDescription = "Scan"
)
}
} else if (onBackClick != null) {
IconButton(
onClick = onBackClick,
modifier = Modifier
@@ -0,0 +1,76 @@
package com.github.nacabaro.vbhelper.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getBitmap
@Composable
fun TransformationHistoryCard(
transformationHistory: List<CharacterDtos.TransformationHistory>,
modifier: Modifier= Modifier
) {
Card (
shape = androidx.compose.material.MaterialTheme.shapes.small,
modifier = modifier
) {
LazyRow (
modifier = Modifier
.padding(8.dp)
) {
items(transformationHistory) { transformation ->
TransformationHistoryItem(transformation)
}
}
}
}
@Composable
fun TransformationHistoryItem(
transformation: CharacterDtos.TransformationHistory
) {
val bitmapData = BitmapData(
bitmap = transformation.spriteIdle,
width = transformation.spriteWidth,
height = transformation.spriteHeight
)
val bitmap = remember (bitmapData) { bitmapData.getBitmap() }
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
val density: Float = LocalContext.current.resources.displayMetrics.density
val dpSize = (bitmap.width * 3 / density).dp
Box (
contentAlignment = Alignment.BottomCenter,
modifier = Modifier
.aspectRatio(1f)
.fillMaxWidth()
.size((64*3/density).dp)
) {
Image(
bitmap = imageBitmap,
contentDescription = "Transformation",
filterQuality = FilterQuality.None,
modifier = Modifier
.size(dpSize)
)
}
}
@@ -5,6 +5,8 @@ import androidx.room.Insert
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.Character
import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import java.util.GregorianCalendar
@Dao
interface CharacterDao {
@@ -25,4 +27,24 @@ interface CharacterDao {
@Query("SELECT * FROM Sprites")
suspend fun getAllSprites(): List<Sprites>
@Query("""
SELECT
d.dimId as cardId,
c.monIndex as charId
FROM Character c
JOIN UserCharacter uc ON c.id = uc.charId
JOIN Dim d ON c.dimId = d.id
WHERE uc.id = :charId
""")
suspend fun getCharacterInfo(charId: Long): CharacterDtos.DiMInfo
@Query("""
INSERT INTO TransformationHistory(monId, stageId, transformationDate)
VALUES
(:monId,
(SELECT id FROM Character WHERE monIndex = :stage AND dimId = :dimId),
:transformationDate)
""")
fun insertTransformation(monId: Long, stage: Int, dimId: Long, transformationDate: Long)
}
@@ -20,20 +20,60 @@ interface UserCharacterDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertTransformationHistory(vararg transformationHistory: TransformationHistory)
@Query("SELECT * FROM TransformationHistory WHERE monId = :monId")
fun getTransformationHistory(monId: Int): List<TransformationHistory>
@Query("""
SELECT
c.id AS id,
c.sprite1 AS spriteIdle,
c.spritesWidth AS spriteWidth,
c.spritesHeight AS spriteHeight,
c.monIndex AS monIndex,
t.transformationDate AS transformationDate
FROM TransformationHistory t
JOIN Character c ON c.id = t.stageId
WHERE monId = :monId
""")
fun getTransformationHistory(monId: Long): List<CharacterDtos.TransformationHistory>?
@Query("""
SELECT
uc.*,
c.sprite1 AS spriteIdle,
c.spritesWidth AS spriteWidth,
c.spritesHeight AS spriteHeight
c.spritesHeight AS spriteHeight,
d.isBEm as isBemCard
FROM UserCharacter uc
JOIN Character c ON uc.charId = c.id
JOIN Dim d ON c.dimId = d.id
""")
suspend fun getAllCharacters(): List<CharacterDtos.CharacterWithSprites>
@Query("SELECT * FROM UserCharacter WHERE id = :id")
suspend fun getCharacter(id: Long): UserCharacter
@Query("SELECT * FROM BECharacterData WHERE id = :id")
suspend fun getBeData(id: Long): BECharacterData
@Query("""
SELECT
uc.*,
c.sprite1 AS spriteIdle,
c.spritesWidth AS spriteWidth,
c.spritesHeight AS spriteHeight,
d.isBEm as isBemCard
FROM UserCharacter uc
JOIN Character c ON uc.charId = c.id
JOIN Dim d ON c.dimId = d.id
WHERE uc.isActive = 1
LIMIT 1
""")
suspend fun getActiveCharacter(): CharacterDtos.CharacterWithSprites?
@Query("DELETE FROM UserCharacter WHERE id = :id")
fun deleteCharacterById(id: Long)
@Query("UPDATE UserCharacter SET isActive = 0 WHERE isActive = 1")
fun clearActiveCharacter()
@Query("UPDATE UserCharacter SET isActive = 1 WHERE id = :id")
fun setActiveCharacter(id: Long)
}
@@ -1,7 +1,9 @@
package com.github.nacabaro.vbhelper.di
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
interface AppContainer {
val db: AppDatabase
val dataStoreSecretsRepository: DataStoreSecretsRepository
}
@@ -1,9 +1,22 @@
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import androidx.room.Room
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.AppContainer
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
import com.github.nacabaro.vbhelper.source.SecretsSerializer
import com.github.nacabaro.vbhelper.source.proto.Secrets
private const val SECRETS_DATA_STORE_NAME = "secrets.pb"
val Context.secretsStore: DataStore<Secrets> by dataStore(
fileName = SECRETS_DATA_STORE_NAME,
serializer = SecretsSerializer
)
class DefaultAppContainer(private val context: Context) : AppContainer {
override val db: AppDatabase by lazy {
Room.databaseBuilder(
context = context,
@@ -11,4 +24,8 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
"internalDb"
).build()
}
}
override val dataStoreSecretsRepository = DataStoreSecretsRepository(context.secretsStore)
}
@@ -13,5 +13,6 @@ data class Dim(
val logoWidth: Int,
val logoHeight: Int,
val name: String,
val stageCount: Int
val stageCount: Int,
val isBEm: Boolean
)
@@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.github.nacabaro.vbhelper.domain.Character
@Entity(
foreignKeys = [
@@ -11,14 +12,18 @@ import androidx.room.PrimaryKey
parentColumns = ["id"],
childColumns = ["monId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Character::class,
parentColumns = ["id"],
childColumns = ["stageId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class TransformationHistory (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val monId: Long,
val toCharIndex: Int,
val year: Int,
val month: Int,
val day: Int
val stageId: Long,
val transformationDate: Long
)
@@ -35,5 +35,6 @@ data class UserCharacter (
var totalBattlesLost: Int,
var activityLevel: Int,
var heartRateCurrent: Int,
var characterType: DeviceType
var characterType: DeviceType,
var isActive: Boolean
)
@@ -1,6 +1,5 @@
package com.github.nacabaro.vbhelper.dtos
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.domain.DeviceType
@@ -27,6 +26,21 @@ object CharacterDtos {
var characterType: DeviceType,
val spriteIdle: ByteArray,
val spriteWidth: Int,
val spriteHeight: Int
val spriteHeight: Int,
val isBemCard: Boolean
)
data class DiMInfo(
val cardId: Int,
val charId: Int
)
data class TransformationHistory(
val id: Long,
val spriteIdle: ByteArray,
val spriteWidth: Int,
val spriteHeight: Int,
val monIndex: Int,
val transformationDate: Long
)
}
@@ -11,18 +11,21 @@ import androidx.navigation.compose.rememberNavController
import com.github.nacabaro.vbhelper.screens.BattlesScreen
import com.github.nacabaro.vbhelper.screens.DexScreen
import com.github.nacabaro.vbhelper.screens.DiMScreen
import com.github.nacabaro.vbhelper.screens.HomeScreen
import com.github.nacabaro.vbhelper.screens.ScanScreen
import com.github.nacabaro.vbhelper.screens.homeScreens.HomeScreen
import com.github.nacabaro.vbhelper.screens.ItemsScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.SettingsScreen
import com.github.nacabaro.vbhelper.screens.SettingsScreenController
import com.github.nacabaro.vbhelper.screens.SpriteViewer
import com.github.nacabaro.vbhelper.screens.StorageScreen
data class AppNavigationHandlers(val settingsScreenController: SettingsScreenController, val scanScreenController: ScanScreenControllerImpl)
@Composable
fun AppNavigation(
onClickRead: () -> Unit,
onClickScan: () -> Unit,
applicationNavigationHandlers: AppNavigationHandlers,
onClickImportCard: () -> Unit,
isDoneReadingCharacter: Boolean
) {
val navController = rememberNavController()
@@ -33,46 +36,51 @@ fun AppNavigation(
) { contentPadding ->
NavHost(
navController = navController,
startDestination = BottomNavItem.Home.route,
startDestination = NavigationItems.Home.route,
modifier = Modifier
.padding(contentPadding)
) {
composable(BottomNavItem.Battles.route) {
composable(NavigationItems.Battles.route) {
BattlesScreen()
}
composable(BottomNavItem.Home.route) {
composable(NavigationItems.Home.route) {
HomeScreen(
navController = navController
)
}
composable(BottomNavItem.Storage.route) {
StorageScreen()
}
composable(BottomNavItem.Scan.route) {
onClickScan()
ScanScreen(
navController = navController,
onClickRead = onClickRead,
isDoneReadingCharacter = isDoneReadingCharacter
composable(NavigationItems.Storage.route) {
StorageScreen(
navController = navController
)
}
composable(BottomNavItem.Dex.route) {
composable(NavigationItems.Scan.route) {
val characterIdString = it.arguments?.getString("characterId")
val characterId = characterIdString?.toLongOrNull()
ScanScreen(
navController = navController,
scanScreenController = applicationNavigationHandlers.scanScreenController,
characterId = characterId
)
}
composable(NavigationItems.Dex.route) {
DexScreen(
navController = navController
)
}
composable(BottomNavItem.Settings.route) {
composable(NavigationItems.Settings.route) {
SettingsScreen(
navController = navController,
settingsScreenController = applicationNavigationHandlers.settingsScreenController,
onClickImportCard = onClickImportCard
)
}
composable(BottomNavItem.Viewer.route) {
composable(NavigationItems.Viewer.route) {
SpriteViewer(
navController = navController
)
}
composable(BottomNavItem.CardView.route) {
composable(NavigationItems.CardView.route) {
val dimId = it.arguments?.getString("dimId")
Log.d("dimId", dimId.toString())
if (dimId != null) {
@@ -82,6 +90,11 @@ fun AppNavigation(
)
}
}
composable(NavigationItems.Items.route) {
ItemsScreen(
navController = navController
)
}
}
}
}
@@ -1,18 +0,0 @@
package com.github.nacabaro.vbhelper.navigation
import com.github.nacabaro.vbhelper.R
sealed class BottomNavItem (
var route: String,
var icon: Int,
var label: String
) {
object Scan : BottomNavItem("Scan", R.drawable.baseline_nfc_24, "Scan")
object Battles : BottomNavItem("Battle", R.drawable.baseline_swords_24, "Battle")
object Home : BottomNavItem("Home", R.drawable.baseline_cottage_24, "Home")
object Dex : BottomNavItem("Dex", R.drawable.baseline_menu_book_24, "Dex")
object Storage : BottomNavItem("Storage", R.drawable.baseline_catching_pokemon_24, "Storage")
object Settings : BottomNavItem("Settings", R.drawable.baseline_settings_24, "Settings")
object Viewer : BottomNavItem("Viewer", R.drawable.baseline_image_24, "Viewer")
object CardView : BottomNavItem("Card/{dimId}", R.drawable.baseline_image_24, "Card")
}
@@ -13,11 +13,11 @@ import androidx.navigation.compose.currentBackStackEntryAsState
@Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(
BottomNavItem.Scan,
BottomNavItem.Battles,
BottomNavItem.Home,
BottomNavItem.Dex,
BottomNavItem.Storage,
NavigationItems.Items,
NavigationItems.Battles,
NavigationItems.Home,
NavigationItems.Dex,
NavigationItems.Storage,
)
NavigationBar {
val currentBackStackEntry = navController.currentBackStackEntryAsState()
@@ -0,0 +1,19 @@
package com.github.nacabaro.vbhelper.navigation
import com.github.nacabaro.vbhelper.R
sealed class NavigationItems (
var route: String,
var icon: Int,
var label: String
) {
object Scan : NavigationItems("Scan/{characterId}", R.drawable.baseline_nfc_24, "Scan")
object Battles : NavigationItems("Battle", R.drawable.baseline_swords_24, "Battle")
object Home : NavigationItems("Home", R.drawable.baseline_cottage_24, "Home")
object Dex : NavigationItems("Dex", R.drawable.baseline_menu_book_24, "Dex")
object Storage : NavigationItems("Storage", R.drawable.baseline_catching_pokemon_24, "Storage")
object Settings : NavigationItems("Settings", R.drawable.baseline_settings_24, "Settings")
object Viewer : NavigationItems("Viewer", R.drawable.baseline_image_24, "Viewer")
object CardView : NavigationItems("Card/{dimId}", R.drawable.baseline_image_24, "Card")
object Items : NavigationItems("Items", R.drawable.baseline_data_24, "Items")
}
@@ -19,7 +19,7 @@ import com.github.nacabaro.vbhelper.components.DexDiMEntry
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.Dim
import com.github.nacabaro.vbhelper.navigation.BottomNavItem
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.source.DexRepository
import kotlinx.coroutines.launch
@@ -45,7 +45,7 @@ fun DexScreen(
TopBanner(
text = "Discovered Digimon",
onGearClick = {
navController.navigate(BottomNavItem.Viewer.route)
navController.navigate(NavigationItems.Viewer.route)
}
)
}
@@ -65,7 +65,7 @@ fun DexScreen(
onClick = {
navController
.navigate(
BottomNavItem
NavigationItems
.CardView.route
.replace("{dimId}", "${it.id}")
)
@@ -1,5 +1,6 @@
package com.github.nacabaro.vbhelper.screens
import android.util.Log
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
@@ -54,6 +55,7 @@ fun DiMScreen(
items(characterList.value) { character ->
CharacterEntry(
onClick = { },
obscure = true,
icon = BitmapData(
bitmap = character.sprite1,
width = character.spritesWidth,
@@ -1,34 +0,0 @@
package com.github.nacabaro.vbhelper.screens
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.navigation.BottomNavItem
@Composable
fun HomeScreen(
navController: NavController
) {
Scaffold (
topBar = {
TopBanner(
text = "VB Helper",
onGearClick = {
navController.navigate(BottomNavItem.Settings.route)
}
)
}
) { contentPadding ->
Box (
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
Text("Home Screen")
}
}
}
@@ -0,0 +1,12 @@
package com.github.nacabaro.vbhelper.screens
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
@Composable
fun ItemsScreen(
navController: NavController
) {
Text(text = "Items")
}
@@ -1,108 +0,0 @@
package com.github.nacabaro.vbhelper.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.navigation.BottomNavItem
import com.github.nacabaro.vbhelper.screens.scanScreen.ReadingCharacterScreen
@Composable
fun ScanScreen(
navController: NavController,
onClickRead: () -> Unit,
isDoneReadingCharacter: Boolean
) {
var readingScreen by remember { mutableStateOf(false) }
if (isDoneReadingCharacter) {
readingScreen = false
navController.navigate(BottomNavItem.Home.route)
}
if (readingScreen) {
ReadingCharacterScreen { readingScreen = false }
} else {
ChooseConnectOption(
onClickRead = {
readingScreen = true
onClickRead()
},
)
}
}
@Composable
private fun ChooseConnectOption(
onClickRead: () -> Unit,
) {
Scaffold(
topBar = { TopBanner(text = "Scan a Vital Bracelet") }
) { contentPadding ->
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
ScanButton(
text = "Vital Bracelet to App",
onClick = onClickRead
)
Spacer(modifier = Modifier.height(16.dp))
ScanButton(
text = "App to Vital Bracelet",
onClick = {}
)
}
}
}
@Composable
fun ScanButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
modifier = modifier
) {
Text(
text = text,
fontSize = 16.sp,
modifier = Modifier
.padding(4.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun ScanScreenPreview() {
ScanScreen(
navController = rememberNavController(),
onClickRead = { },
isDoneReadingCharacter = false
)
}
@@ -1,5 +1,9 @@
package com.github.nacabaro.vbhelper.screens
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -22,6 +26,7 @@ import com.github.nacabaro.vbhelper.components.TopBanner
@Composable
fun SettingsScreen(
navController: NavController,
settingsScreenController: SettingsScreenController,
onClickImportCard: () -> Unit
) {
Scaffold (
@@ -42,12 +47,10 @@ fun SettingsScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
SettingsSection("General")
SettingsEntry(title = "Import VB key", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import VB Characters key", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import VB BE key", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import transform functions", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import decryption key", description = "Import standard vital bracelet keys") { }
SettingsSection("NFC Communication")
SettingsEntry(title = "Import APK", description = "Import Secrets From Vital Arean 2.1.0 APK") {
settingsScreenController.apkFilePickLauncher.launch(arrayOf("*/*"))
}
SettingsSection("DiM/BEm management")
SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
SettingsEntry(title = "Rename DiM/BEm", description = "Set card name") { }
@@ -58,6 +61,12 @@ fun SettingsScreen(
}
}
fun buildFilePickLauncher(activity: ComponentActivity, onItemPicked: (Uri?) -> Unit): ActivityResultLauncher<Array<String>> {
return activity.registerForActivityResult(ActivityResultContracts.OpenDocument()) {
onItemPicked.invoke(it)
}
}
@Composable
fun SettingsEntry(
title: String,
@@ -0,0 +1,71 @@
package com.github.nacabaro.vbhelper.screens
import android.net.Uri
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import com.github.nacabaro.vbhelper.source.SecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsRepository
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
data class SettingsScreenController(val apkFilePickLauncher: ActivityResultLauncher<Array<String>>) {
class Factory(private val componentActivity: ComponentActivity, private val secretsImporter: SecretsImporter, private val secretsRepository: SecretsRepository) {
fun buildSettingScreenHandlers(): SettingsScreenController {
return SettingsScreenController(
apkFilePickLauncher = buildFilePickerActivityLauncher(this::importApk)
)
}
private fun buildFilePickerActivityLauncher(onResult : (Uri?) ->Unit): ActivityResultLauncher<Array<String>> {
return componentActivity.registerForActivityResult(ActivityResultContracts.OpenDocument()) {
onResult.invoke(it)
}
}
private fun importApk(uri: Uri?) {
if(uri == null) {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "APK Import Cancelled", Toast.LENGTH_SHORT)
.show()
}
return
}
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
componentActivity.contentResolver.openInputStream(uri).use {
if(it == null) {
componentActivity.runOnUiThread {
Toast.makeText(
componentActivity,
"Selected file is empty!",
Toast.LENGTH_SHORT
).show()
}
return@launch
}
var secrets: Secrets? = null
try {
secrets = secretsImporter.importSecrets(it)
} catch (e: Exception) {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "Secrets import failed. Please only select the official Vital Arena App 2.1.0 APK.", Toast.LENGTH_SHORT).show()
}
return@launch
}
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
secretsRepository.updateSecrets(secrets)
}.invokeOnCompletion {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "Secrets successfully imported. Connections with devices are now possible.", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
}
@@ -81,6 +81,7 @@ fun SpriteViewer(
contentDescription = "Sprite",
modifier = Modifier
.size(256.dp)
.padding(8.dp)
)
}
}
@@ -1,11 +1,11 @@
package com.github.nacabaro.vbhelper.screens
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -14,6 +14,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Scaffold
@@ -21,11 +22,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -34,18 +33,24 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun StorageScreen() {
fun StorageScreen(
navController: NavController
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
@@ -60,8 +65,6 @@ fun StorageScreen() {
}
}
Log.d("StorageScreen", "monList: $monList")
Scaffold (
topBar = { TopBanner(text = "My Digimon") }
) { contentPadding ->
@@ -70,7 +73,7 @@ fun StorageScreen() {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(contentPadding)
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
) {
Text(
@@ -94,16 +97,32 @@ fun StorageScreen() {
width = index.spriteWidth,
height = index.spriteHeight
),
modifier = Modifier
.padding(8.dp)
.size(96.dp)
onClick = {
selectedCharacter = index.id
}
)
if (selectedCharacter != null) {
StorageDialog(
characterId = selectedCharacter!!,
onDismissRequest = { selectedCharacter = null }
onDismissRequest = { selectedCharacter = null },
onClickSetActive = {
coroutineScope.launch {
withContext(Dispatchers.IO) {
storageRepository.setActiveCharacter(selectedCharacter!!)
selectedCharacter = null
}
navController.navigate(NavigationItems.Home.route)
}
},
onSendToBracelet = {
navController.navigate(
NavigationItems.Scan.route.replace(
"{characterId}",
selectedCharacter.toString()
)
)
}
)
}
}
@@ -114,7 +133,9 @@ fun StorageScreen() {
@Composable
fun StorageDialog(
characterId: Long,
onDismissRequest: () -> Unit
onDismissRequest: () -> Unit,
onSendToBracelet: () -> Unit,
onClickSetActive: () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
@@ -149,10 +170,25 @@ fun StorageDialog(
.padding(8.dp)
)
}
Button(
onClick = onDismissRequest
Row (
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
Text(text = "Close")
Button(
onClick = onSendToBracelet
) {
Text(text = "Send to bracelet")
}
Button(
onClick = onClickSetActive
) {
Text(text = "Set active")
}
Button(
onClick = onDismissRequest
) {
Text(text = "Close")
}
}
}
}
@@ -0,0 +1,201 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.ItemDisplay
import com.github.nacabaro.vbhelper.components.TransformationHistoryCard
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.utils.BitmapData
import java.util.Locale
@Composable
fun BEBEmHomeScreen(
activeMon: CharacterDtos.CharacterWithSprites,
beData: BECharacterData,
transformationHistory: List<CharacterDtos.TransformationHistory>,
contentPadding: PaddingValues
) {
Column(
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.verticalScroll(state = rememberScrollState())
) {
Row (
modifier = Modifier
.fillMaxWidth()
) {
CharacterEntry(
icon = BitmapData(
bitmap = activeMon.spriteIdle,
width = activeMon.spriteWidth,
height = activeMon.spriteHeight
),
multiplier = 8,
shape = androidx.compose.material.MaterialTheme.shapes.small,
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
)
Column (
modifier = Modifier
.weight(0.5f)
.aspectRatio(0.5f)
) {
ItemDisplay(
icon = R.drawable.baseline_vitals_24,
textValue = activeMon.vitalPoints.toString(),
definition = "Vitals",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_trophy_24,
textValue = activeMon.trophies.toString(),
definition = "Trophies",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
.padding(8.dp)
)
}
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
ItemDisplay(
icon = R.drawable.baseline_mood_24,
textValue = activeMon.mood.toString(),
definition = "Mood",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
val timeInHours = (beData.remainingTrainingTimeInMinutes / 60)
ItemDisplay(
icon = R.drawable.baseline_timer_24,
textValue = "$timeInHours h",
definition = "Training limit",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_rank_24,
textValue = beData.rank.toString(),
definition = "Rank",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
val transformationCountdownInHours = activeMon.transformationCountdown / 60
ItemDisplay(
icon = R.drawable.baseline_next_24,
textValue = when (transformationCountdownInHours) {
0 -> "${activeMon.transformationCountdown} m"
else -> "$transformationCountdownInHours h"
},
definition = "Next timer",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_swords_24,
textValue = when {
activeMon.totalBattlesLost == 0 -> "0.00 %"
else -> {
val battleWinPercentage = activeMon.totalBattlesWon.toFloat() / (activeMon.totalBattlesWon + activeMon.totalBattlesLost).toFloat()
String.format(Locale.getDefault(), "%.2f", battleWinPercentage * 100) + " %" // Specify locale
}
},
definition = "Total battle win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_swords_24,
textValue = when {
activeMon.totalBattlesLost == 0 -> "0.00 %"
else -> {
val battleWinPercentage = activeMon.currentPhaseBattlesWon.toFloat() / (activeMon.currentPhaseBattlesWon + activeMon.currentPhaseBattlesLost).toFloat()
String.format(Locale.getDefault(), "%.2f", battleWinPercentage * 100) + " %" // Specify locale
}
},
definition = "Current phase win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
TransformationHistoryCard(
transformationHistory = transformationHistory,
modifier = Modifier
.weight(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
ItemDisplay(
icon = R.drawable.baseline_health_24,
textValue = "+${beData.trainingHp}",
definition = "Training HP",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_agility_24,
textValue = "+${beData.trainingBp}",
definition = "Training BP",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_attack_24,
textValue = "+${beData.trainingAp}",
definition = "Training AP",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
}
}
@@ -0,0 +1,171 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import java.util.Locale
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.ItemDisplay
import com.github.nacabaro.vbhelper.components.TransformationHistoryCard
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.utils.BitmapData
import kotlin.text.format
@Composable
fun BEDiMHomeScreen(
activeMon: CharacterDtos.CharacterWithSprites,
beData: BECharacterData,
transformationHistory: List<CharacterDtos.TransformationHistory>,
contentPadding: PaddingValues
) {
Column(
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.verticalScroll(state = rememberScrollState())
) {
Row (
modifier = Modifier
.fillMaxWidth()
) {
CharacterEntry(
icon = BitmapData(
bitmap = activeMon.spriteIdle,
width = activeMon.spriteWidth,
height = activeMon.spriteHeight
),
multiplier = 8,
shape = androidx.compose.material.MaterialTheme.shapes.small,
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
)
Column (
modifier = Modifier
.weight(0.5f)
.aspectRatio(0.5f)
) {
ItemDisplay(
icon = R.drawable.baseline_vitals_24,
textValue = activeMon.vitalPoints.toString(),
definition = "Vitals",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_trophy_24,
textValue = activeMon.trophies.toString(),
definition = "Trophies",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
.padding(8.dp)
)
}
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
ItemDisplay(
icon = R.drawable.baseline_mood_24,
textValue = activeMon.mood.toString(),
definition = "Mood",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
val timeInHours = (beData.remainingTrainingTimeInMinutes / 60)
ItemDisplay(
icon = R.drawable.baseline_timer_24,
textValue = "$timeInHours h",
definition = "Training limit",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
// Maybe get rid of this?
ItemDisplay(
icon = R.drawable.baseline_rank_24,
textValue = beData.rank.toString(),
definition = "Rank",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
val transformationCountdownInHours = activeMon.transformationCountdown / 60
ItemDisplay(
icon = R.drawable.baseline_next_24,
textValue = when (transformationCountdownInHours) {
0 -> "${activeMon.transformationCountdown} m"
else -> "$transformationCountdownInHours h"
},
definition = "Next timer",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_swords_24,
textValue = when {
activeMon.totalBattlesLost == 0 -> "0.00 %"
else -> {
val battleWinPercentage = activeMon.totalBattlesWon.toFloat() / (activeMon.totalBattlesWon + activeMon.totalBattlesLost).toFloat()
String.format(Locale.getDefault(), "%.2f", battleWinPercentage * 100) + " %" // Specify locale
}
},
definition = "Total battle win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_swords_24,
textValue = when {
activeMon.totalBattlesLost == 0 -> "0.00 %"
else -> {
val battleWinPercentage = activeMon.currentPhaseBattlesWon.toFloat() / (activeMon.currentPhaseBattlesWon + activeMon.currentPhaseBattlesLost).toFloat()
String.format(Locale.getDefault(), "%.2f", battleWinPercentage * 100) + " %" // Specify locale
}
},
definition = "Current phase win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
TransformationHistoryCard(
transformationHistory = transformationHistory,
modifier = Modifier
.weight(1f)
.padding(8.dp)
)
}
}
}
@@ -0,0 +1,102 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.DeviceType
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.source.StorageRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun HomeScreen(
navController: NavController
) {
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
val activeMon = remember { mutableStateOf<CharacterDtos.CharacterWithSprites?>(null) }
val transformationHistory = remember { mutableStateOf<List<CharacterDtos.TransformationHistory>?>(null) }
val beData = remember { mutableStateOf<BECharacterData?>(null) }
val vbData = remember { mutableStateOf<VBCharacterData?>(null) }
LaunchedEffect(storageRepository, activeMon) {
withContext(Dispatchers.IO) {
activeMon.value = storageRepository.getActiveCharacter()
if (activeMon.value != null) {
beData.value = storageRepository.getCharacterBeData(activeMon.value!!.id)
transformationHistory.value = storageRepository.getTransformationHistory(activeMon.value!!.id)
}
}
}
Scaffold (
topBar = {
TopBanner(
text = "VB Helper",
onScanClick = {
navController.navigate(NavigationItems.Scan.route)
},
onGearClick = {
navController.navigate(NavigationItems.Settings.route)
}
)
}
) { contentPadding ->
if (activeMon.value == null || (beData.value == null && vbData.value == null) || transformationHistory.value == null) {
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.padding(top = contentPadding.calculateTopPadding())
) {
Text(text = "Nothing to see here")
}
} else {
if (activeMon.value!!.isBemCard) {
Log.d("HomeScreen", "BEDeviceBEm")
BEBEmHomeScreen(
activeMon = activeMon.value!!,
beData = beData.value!!,
transformationHistory = transformationHistory.value!!,
contentPadding = contentPadding
)
} else if (!activeMon.value!!.isBemCard && activeMon.value!!.characterType == DeviceType.BEDevice) {
Log.d("HomeScreen", "BEDevice")
BEDiMHomeScreen(
activeMon = activeMon.value!!,
beData = beData.value!!,
transformationHistory = transformationHistory.value!!,
contentPadding = contentPadding
)
} else {
VBDiMHomeScreen(
activeMon = activeMon.value!!,
vbData = vbData.value!!,
transformationHistory = transformationHistory.value!!,
contentPadding = contentPadding
)
}
}
}
}
@@ -0,0 +1,16 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
@Composable
fun VBDiMHomeScreen(
activeMon: CharacterDtos.CharacterWithSprites,
vbData: VBCharacterData,
transformationHistory: List<CharacterDtos.TransformationHistory>,
contentPadding: PaddingValues
) {
TODO("Not implemented yet")
}
@@ -15,11 +15,12 @@ import com.github.nacabaro.vbhelper.components.TopBanner
@Composable
fun ReadingCharacterScreen(
topBannerText: String,
onClickCancel: () -> Unit,
) {
Scaffold (
topBar = {
TopBanner("Reading Character")
TopBanner(topBannerText)
}
) { innerPadding ->
Column (
@@ -0,0 +1,265 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
import com.github.nacabaro.vbhelper.utils.characterToNfc
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
const val SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER = "SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER"
@Composable
fun ScanScreen(
navController: NavController,
characterId: Long?,
scanScreenController: ScanScreenController,
) {
val secrets by scanScreenController.secretsFlow.collectAsState(null)
var readingScreen by remember { mutableStateOf(false) }
var writingScreen by remember { mutableStateOf(false) }
var isDoneReadingCharacter by remember { mutableStateOf(false) }
var isDoneSendingCard by remember { mutableStateOf(false) }
var isDoneWritingCharacter by remember { mutableStateOf(false) }
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
var nfcCharacter by remember { mutableStateOf<NfcCharacter?>(null) }
val context = LocalContext.current
LaunchedEffect(storageRepository) {
withContext(Dispatchers.IO) {
if(characterId != null && nfcCharacter == null) {
nfcCharacter = characterToNfc(context, characterId)
}
}
}
DisposableEffect(readingScreen || writingScreen, isDoneSendingCard) {
if(readingScreen) {
scanScreenController.registerActivityLifecycleListener(SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER, object: ActivityLifecycleListener {
override fun onPause() {
scanScreenController.cancelRead()
}
override fun onResume() {
scanScreenController.onClickRead(secrets!!) {
isDoneReadingCharacter = true
}
}
})
scanScreenController.onClickRead(secrets!!) {
isDoneReadingCharacter = true
}
} else if (writingScreen) {
scanScreenController.registerActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER,
object : ActivityLifecycleListener {
override fun onPause() {
scanScreenController.cancelRead()
}
override fun onResume() {
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter!!) {
isDoneSendingCard = true
}
} else if (!isDoneWritingCharacter) {
scanScreenController.onClickWrite(secrets!!, nfcCharacter!!) {
isDoneWritingCharacter = true
}
}
}
}
)
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter!!) {
isDoneSendingCard = true
}
} else if (!isDoneWritingCharacter) {
scanScreenController.onClickWrite(secrets!!, nfcCharacter!!) {
isDoneWritingCharacter = true
}
}
}
onDispose {
if(readingScreen || writingScreen) {
scanScreenController.unregisterActivityLifecycleListener(SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER)
scanScreenController.cancelRead()
}
}
}
if (isDoneReadingCharacter) {
readingScreen = false
navController.navigate(NavigationItems.Home.route)
} else if (isDoneSendingCard && isDoneWritingCharacter) {
writingScreen = false
navController.navigate(NavigationItems.Home.route)
LaunchedEffect(storageRepository) {
withContext(Dispatchers.IO) {
storageRepository
.deleteCharacter(characterId!!)
}
}
}
if (readingScreen) {
ReadingCharacterScreen("Reading character") {
readingScreen = false
scanScreenController.cancelRead()
}
} else if (writingScreen) {
if (!isDoneSendingCard) {
ReadingCharacterScreen("Sending card") {
writingScreen = false
scanScreenController.cancelRead()
}
} else if (!isDoneWritingCharacter) {
ReadingCharacterScreen("Writing character") {
isDoneSendingCard = false
writingScreen = false
scanScreenController.cancelRead()
}
}
} else {
ChooseConnectOption(
onClickRead = when {
characterId != null -> null
else -> {
{
if(secrets == null) {
Toast.makeText(context, "Secrets is not yet initialized. Try again.", Toast.LENGTH_SHORT).show()
} else if(secrets?.isMissingSecrets() == true) {
Toast.makeText(context, "Secrets not yet imported. Go to Settings and Import APK", Toast.LENGTH_SHORT).show()
} else {
readingScreen = true // kicks off nfc adapter in DisposableEffect
}
}
}
},
onClickWrite = when {
nfcCharacter == null -> null
else -> {
{
if(secrets == null) {
Toast.makeText(context, "Secrets is not yet initialized. Try again.", Toast.LENGTH_SHORT).show()
} else if(secrets?.isMissingSecrets() == true) {
Toast.makeText(context, "Secrets not yet imported. Go to Settings and Import APK", Toast.LENGTH_SHORT).show()
} else {
writingScreen = true // kicks off nfc adapter in DisposableEffect
}
}
}
}
)
}
}
@Composable
fun ChooseConnectOption(
onClickRead: (() -> Unit)? = null,
onClickWrite: (() -> Unit)? = null,
) {
Scaffold(
topBar = { TopBanner(text = "Scan a Vital Bracelet") }
) { contentPadding ->
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
ScanButton(
text = "Vital Bracelet to App",
disabled = onClickRead == null,
onClick = onClickRead?: { },
)
Spacer(modifier = Modifier.height(16.dp))
ScanButton(
text = "App to Vital Bracelet",
disabled = onClickWrite == null,
onClick = onClickWrite?: { },
)
}
}
}
@Composable
fun ScanButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
disabled: Boolean = false,
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = !disabled,
) {
Text(
text = text,
fontSize = 16.sp,
modifier = Modifier
.padding(4.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun ScanScreenPreview() {
ScanScreen(
navController = rememberNavController(),
scanScreenController = object: ScanScreenController {
override val secretsFlow = MutableStateFlow<Secrets>(Secrets.getDefaultInstance())
override fun unregisterActivityLifecycleListener(key: String) { }
override fun registerActivityLifecycleListener(
key: String,
activityLifecycleListener: ActivityLifecycleListener
) {
}
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit) {}
override fun onClickCheckCard(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit) {}
override fun onClickWrite(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit) {}
override fun cancelRead() {}
},
characterId = null
)
}
@@ -0,0 +1,18 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.flow.Flow
interface ScanScreenController {
val secretsFlow: Flow<Secrets>
fun onClickRead(secrets: Secrets, onComplete: ()->Unit)
fun onClickCheckCard(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit)
fun onClickWrite(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit)
fun cancelRead()
fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener)
fun unregisterActivityLifecycleListener(key: String)
}
@@ -0,0 +1,147 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
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.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.source.getCryptographicTransformerMap
import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class ScanScreenControllerImpl(
override val secretsFlow: Flow<Secrets>,
private val nfcHandler: (NfcCharacter)->String,
private val context: ComponentActivity,
private val registerActivityLifecycleListener: (String, ActivityLifecycleListener)->Unit,
private val unregisterActivityLifecycleListener: (String)->Unit,
): ScanScreenController {
private val nfcAdapter: NfcAdapter
init {
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(context)
if (maybeNfcAdapter == null) {
Toast.makeText(context, "No NFC on device!", Toast.LENGTH_SHORT).show()
}
nfcAdapter = maybeNfcAdapter
checkSecrets()
}
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit) {
handleTag(secrets) { tagCommunicator ->
val character = tagCommunicator.receiveCharacter()
val resultMessage = nfcHandler(character)
onComplete.invoke()
resultMessage
}
}
override fun cancelRead() {
if(nfcAdapter.isEnabled) {
nfcAdapter.disableReaderMode(context)
}
}
override fun registerActivityLifecycleListener(
key: String,
activityLifecycleListener: ActivityLifecycleListener
) {
registerActivityLifecycleListener.invoke(key, activityLifecycleListener)
}
override fun unregisterActivityLifecycleListener(key: String) {
unregisterActivityLifecycleListener.invoke(key)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun handleTag(secrets: Secrets, 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(context, buildOnReadTag(secrets, handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
options
)
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun buildOnReadTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
return { tag->
val nfcData = NfcA.get(tag)
if (nfcData == null) {
context.runOnUiThread {
Toast.makeText(context, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
}
}
nfcData.connect()
nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
val successText = handlerFunc(tagCommunicator)
context.runOnUiThread {
Toast.makeText(context, successText, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun checkSecrets() {
context.lifecycleScope.launch(Dispatchers.IO) {
if(secretsFlow.stateIn(context.lifecycleScope).value.isMissingSecrets()) {
context.runOnUiThread {
Toast.makeText(context, "Missing Secrets. Go to settings and import Vital Arena APK", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onClickWrite(
secrets: Secrets,
nfcCharacter: NfcCharacter,
onComplete: () -> Unit
) {
handleTag(secrets) { tagCommunicator ->
try {
tagCommunicator.sendCharacter(nfcCharacter)
onComplete.invoke()
"Sent character successfully!"
} catch (e: Throwable) {
Log.e("TAG", e.stackTraceToString())
"Whoops"
}
}
}
override fun onClickCheckCard(
secrets: Secrets,
nfcCharacter: NfcCharacter,
onComplete: () -> Unit
) {
handleTag(secrets) { tagCommunicator ->
tagCommunicator.prepareDIMForCharacter(nfcCharacter.dimId)
onComplete.invoke()
"Sent DIM successfully!"
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun showWirelessSettings() {
Toast.makeText(context, "NFC must be enabled", Toast.LENGTH_SHORT).show()
context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
}
@@ -0,0 +1,27 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.source.proto.Secrets
import java.io.InputStream
import java.util.zip.ZipInputStream
class ApkSecretsImporter(private val dexFileSecretsImporter: SecretsImporter = DexFileSecretsImporter()): SecretsImporter {
companion object {
const val DEX_FILE = "classes.dex"
}
// importSecrets imports the secrets from the apk input stream, and validates them.
override fun importSecrets(inputStream: InputStream): Secrets {
ZipInputStream(inputStream).use { zip ->
var zipEntry = zip.nextEntry
while(zipEntry != null) {
println("Zip Entry: ${zipEntry.name}")
if(zipEntry.name == DEX_FILE) {
return dexFileSecretsImporter.importSecrets(zip)
}
zipEntry = zip.nextEntry
}
throw IllegalArgumentException("File `$DEX_FILE` is missing from apk!")
}
}
}
@@ -0,0 +1,59 @@
package com.github.nacabaro.vbhelper.source
import androidx.datastore.core.DataStore
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.nacabaro.vbhelper.source.proto.Secrets
import com.github.nacabaro.vbhelper.source.proto.Secrets.HmacKeys
import kotlinx.coroutines.flow.single
class DataStoreSecretsRepository(
private val secretsDataStore: DataStore<Secrets>,
): SecretsRepository {
override val secretsFlow = secretsDataStore.data
override suspend fun updateSecrets(secrets: Secrets) {
secretsDataStore.updateData {
secrets
}
}
override suspend fun getSecrets(): Secrets {
return secretsFlow.single()
}
}
private fun Secrets.getHmacKeys(deviceTypeId: UShort): HmacKeys {
return when(deviceTypeId) {
DeviceType.VitalBraceletBEDeviceType -> this.beHmacKeys
DeviceType.VitalCharactersDeviceType -> this.vbcHmacKeys
DeviceType.VitalSeriesDeviceType -> this.vbdmHmacKeys
else -> throw IllegalArgumentException("Unknown DeviceTypeId")
}
}
fun Secrets.getCryptographicTransformerMap(): Map<UShort, CryptographicTransformer> {
val cipher = this.vbCipherList.toIntArray()
val beCipher = this.beCipherList.toIntArray()
val vbdmHmacKeys = this.getHmacKeys(DeviceType.VitalSeriesDeviceType)
val vbcHmacKeys = this.getHmacKeys(DeviceType.VitalCharactersDeviceType)
val beHmacKeys = this.getHmacKeys(DeviceType.VitalBraceletBEDeviceType)
return mapOf(
Pair(DeviceType.VitalSeriesDeviceType, CryptographicTransformer(vbdmHmacKeys.hmacKey1, vbdmHmacKeys.hmacKey2, this.aesKey, cipher)),
Pair(DeviceType.VitalCharactersDeviceType, CryptographicTransformer(vbcHmacKeys.hmacKey1, vbcHmacKeys.hmacKey2, this.aesKey, cipher)),
Pair(DeviceType.VitalBraceletBEDeviceType, CryptographicTransformer(beHmacKeys.hmacKey1, beHmacKeys.hmacKey2, this.aesKey, beCipher)),
)
}
fun Secrets.isMissingSecrets(): Boolean {
return this.aesKey.length != 24 ||
this.vbCipherList.size != 16 ||
this.beCipherList.size != 16 ||
this.vbdmHmacKeys.isMissingKey() ||
this.vbcHmacKeys.isMissingKey() ||
this.beHmacKeys.isMissingKey()
}
fun HmacKeys.isMissingKey(): Boolean {
return this.hmacKey1.length != 24 || this.hmacKey2.length != 24
}
@@ -0,0 +1,93 @@
package com.github.nacabaro.vbhelper.source
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.nacabaro.vbhelper.source.proto.Secrets
import com.github.nacabaro.vbhelper.source.proto.Secrets.HmacKeys
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.charset.StandardCharsets
import java.security.InvalidKeyException
class DexFileSecretsImporter: SecretsImporter {
companion object {
const val VBDM_SUBSTITUTION_CIPHER_IDX = 1080145
const val BE_SUBSTITUTION_CIPHER_IDX = 1080217
const val VBDM_HMAC_KEY_2_IDX = 1249063
const val VBDM_HMAC_KEY_1_IDX = 1494074
const val VBC_HMAC_KEY_1_IDX = 1241640
const val VBC_HMAC_KEY_2_IDX = 1466955
const val BE_HMAC_KEY_1_IDX = 1580157
const val BE_HMAC_KEY_2_IDX = 1593759
const val AES_KEY_IDX = 1277527
val TEST_TAG = byteArrayOf(0x34, 0x01, 0x10, 0xff.toByte(), 0xf5.toByte(), 0x00, 0xa2.toByte())
const val BE_TEST_TAG_PASSWORD = "be29a87e"
const val VBDM_TEST_TAG_PASSWORD = "6ea33673"
const val VBC_TEST_TAG_PASSWORD = "a71dfb22"
}
override fun importSecrets(inputStream: InputStream): Secrets {
val deviceToSecrets = readSecrets(inputStream)
verifySecretCorrectness(deviceToSecrets)
return deviceToSecrets
}
private fun readSecrets(inputStream: InputStream): Secrets {
val dexFile = inputStream.readBytes()
val byteOrder = ByteOrder.BIG_ENDIAN
val vbdmSubstitutionCipher = dexFile.sliceArray(VBDM_SUBSTITUTION_CIPHER_IDX until VBDM_SUBSTITUTION_CIPHER_IDX+(16*4)).toIntArray(byteOrder)
val beSubstitutionCipher = dexFile.sliceArray(BE_SUBSTITUTION_CIPHER_IDX until BE_SUBSTITUTION_CIPHER_IDX+(16*4)).toIntArray(byteOrder)
val aesKey = dexFile.sliceArray(AES_KEY_IDX until AES_KEY_IDX+24).toString(StandardCharsets.UTF_8)
return Secrets.newBuilder()
.setAesKey(aesKey)
.addAllVbCipher(vbdmSubstitutionCipher.toList())
.addAllBeCipher(beSubstitutionCipher.toList())
.setVbdmHmacKeys(getHmac(dexFile, VBDM_HMAC_KEY_1_IDX, VBDM_HMAC_KEY_2_IDX))
.setVbcHmacKeys(getHmac(dexFile, VBC_HMAC_KEY_1_IDX, VBC_HMAC_KEY_2_IDX))
.setBeHmacKeys(getHmac(dexFile, BE_HMAC_KEY_1_IDX, BE_HMAC_KEY_2_IDX))
.build()
}
private fun getHmac(dexFile: ByteArray, hmacKeyIdx1: Int, hmacKeyIdx2: Int): HmacKeys {
val hmacKey1 = dexFile.sliceArray(hmacKeyIdx1 until hmacKeyIdx1+24).toString(StandardCharsets.UTF_8)
val hmacKey2 = dexFile.sliceArray(hmacKeyIdx2 until hmacKeyIdx2+24).toString(StandardCharsets.UTF_8)
return HmacKeys.newBuilder()
.setHmacKey1(hmacKey1)
.setHmacKey2(hmacKey2)
.build()
}
@OptIn(ExperimentalStdlibApi::class)
private fun verifySecretCorrectness(secrets: Secrets) {
val deviceToCryptographicTransformers = secrets.getCryptographicTransformerMap()
for (keyValue in deviceToCryptographicTransformers) {
when(keyValue.key) {
DeviceType.VitalBraceletBEDeviceType -> assertBuildsCorrectPassword(keyValue.value, BE_TEST_TAG_PASSWORD)
DeviceType.VitalCharactersDeviceType -> assertBuildsCorrectPassword(keyValue.value, VBC_TEST_TAG_PASSWORD)
DeviceType.VitalSeriesDeviceType -> assertBuildsCorrectPassword(keyValue.value, VBDM_TEST_TAG_PASSWORD)
}
}
}
@OptIn(ExperimentalStdlibApi::class)
private fun assertBuildsCorrectPassword(cryptographicTransformer: CryptographicTransformer, expectedPassword: String) {
val result = cryptographicTransformer.createNfcPassword(
TEST_TAG
)
if( result.toHexString() != expectedPassword) {
throw InvalidKeyException("Secrets were loaded, but were unsuccessful at generating the test password")
}
}
}
fun ByteArray.toIntArray(byteOrder: ByteOrder): IntArray {
require(this.size % 4 == 0) { "Number of bytes must be multiple of 4 to convert into 32-bit words" }
val values = IntArray(this.size / 4)
ByteBuffer.wrap(this).order(byteOrder).asIntBuffer()[values]
return values
}
@@ -0,0 +1,8 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.source.proto.Secrets
import java.io.InputStream
fun interface SecretsImporter {
fun importSecrets(inputStream: InputStream): Secrets
}
@@ -0,0 +1,11 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.flow.Flow
interface SecretsRepository {
val secretsFlow: Flow<Secrets>
suspend fun getSecrets(): Secrets
suspend fun updateSecrets(secrets: Secrets)
}
@@ -0,0 +1,24 @@
package com.github.nacabaro.vbhelper.source
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.github.nacabaro.vbhelper.source.proto.Secrets
import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream
import java.io.OutputStream
object SecretsSerializer: Serializer<Secrets> {
override val defaultValue = Secrets.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Secrets {
try {
return Secrets.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: Secrets, output: OutputStream) {
t.writeTo(output)
}
}
@@ -1,6 +1,8 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
@@ -14,4 +16,29 @@ class StorageRepository (
suspend fun getSingleCharacter(id: Long): UserCharacter {
return db.userCharacterDao().getCharacter(id)
}
suspend fun getCharacterBeData(id: Long): BECharacterData {
return db.userCharacterDao().getBeData(id)
}
fun getTransformationHistory(characterId: Long): List<CharacterDtos.TransformationHistory>? {
return db.userCharacterDao().getTransformationHistory(characterId)
}
suspend fun getCharacterData(id: Long): CharacterDtos.DiMInfo {
return db.characterDao().getCharacterInfo(id)
}
suspend fun getActiveCharacter(): CharacterDtos.CharacterWithSprites? {
return db.userCharacterDao().getActiveCharacter()
}
fun deleteCharacter(id: Long) {
return db.userCharacterDao().deleteCharacterById(id)
}
fun setActiveCharacter(id: Long) {
db.userCharacterDao().clearActiveCharacter()
return db.userCharacterDao().setActiveCharacter(id)
}
}
@@ -1,8 +1,59 @@
package com.github.nacabaro.vbhelper.utils
import android.graphics.Bitmap
import com.github.cfogrady.vb.dim.sprite.SpriteData
// simple, but smooth
data class BitmapData (
val bitmap: ByteArray,
val width: Int,
val height: Int
)
)
fun BitmapData.getBitmap(): Bitmap {
return Bitmap.createBitmap(createARGBIntArray(), this.width, this.height, Bitmap.Config.HARDWARE)
}
object ARGBMasks {
const val ALPHA = (0xFF shl 24)
const val RED = (0xFF shl 16)
const val GREEN = (0xFF shl 8)
const val BLUE = 0xFF
}
const val BLACK = ARGBMasks.ALPHA
fun BitmapData.getObscuredBitmap(): Bitmap {
val argbPixels = createARGBIntArray()
for(i in argbPixels.indices) {
val currentPixel = argbPixels[i]
if( currentPixel and ARGBMasks.ALPHA != 0) {
// non transparent pixel
argbPixels[i] = BLACK
}
}
return Bitmap.createBitmap(argbPixels, this.width, this.height, Bitmap.Config.HARDWARE)
}
fun BitmapData.createARGBIntArray(): IntArray {
// hack to get it into correct format by relying on the DIM Sprites methods since we haven't changed the raw pixel data at this point.
val bytes = SpriteData.Sprite.builder().width(this.width).height(this.height).pixelData(this.bitmap).build()
.get24BitRGB()
val result = IntArray(this.width*this.height)
for(i in result.indices) {
val originalIndex = i*3
val red = bytes[originalIndex].toUInt() and 0xFFu
val green = bytes[originalIndex+1].toUInt() and 0xFFu
val blue = bytes[originalIndex+2].toUInt() and 0xFFu
val alpha = if(red == 0u && blue == 0u && green == 0xFFu) 0 else 0xFF
result[i] = (alpha shl 24) or (red shl 16).toInt() or (green shl 8).toInt() or blue.toInt()
}
return result
}
fun List<BitmapData>.getBitmaps(): List<Bitmap> {
return this.map{it.getBitmap()}
}
@@ -0,0 +1,100 @@
package com.github.nacabaro.vbhelper.utils
import android.content.Context
import android.icu.util.Calendar
import android.icu.util.GregorianCalendar
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.be.FirmwareVersion
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.DeviceType
import com.github.nacabaro.vbhelper.source.StorageRepository
import java.util.Date
suspend fun characterToNfc(context: Context, characterId: Long): NfcCharacter? {
val app = context.applicationContext as VBHelper
val database = app.container.db
val storageRepository = StorageRepository(database)
val userCharacter = storageRepository.getSingleCharacter(characterId)
val characterInfo = storageRepository.getCharacterData(characterId)
if (userCharacter.characterType == DeviceType.BEDevice) {
val beData = storageRepository.getCharacterBeData(characterId)
val transformationHistory = storageRepository
.getTransformationHistory(characterId)!!
.map {
val date = Date(it.transformationDate)
val calendar = GregorianCalendar()
calendar.time = date
NfcCharacter.Transformation(
toCharIndex = it.monIndex.toUByte(),
year = calendar
.get(Calendar.YEAR)
.toUShort(),
month = calendar
.get(Calendar.MONTH)
.toUByte(),
day = calendar
.get(Calendar.DAY_OF_MONTH)
.toUByte()
)
}.toTypedArray()
val paddedTransformationArray = padTransformationArray(transformationHistory)
val nfcData = BENfcCharacter(
dimId = characterInfo.cardId.toUShort(),
charIndex = characterInfo.charId.toUShort(),
stage = userCharacter.stage.toByte(),
attribute = userCharacter.attribute,
ageInDays = userCharacter.ageInDays.toByte(),
nextAdventureMissionStage = userCharacter.nextAdventureMissionStage.toByte(),
mood = userCharacter.mood.toByte(),
vitalPoints = userCharacter.vitalPoints.toUShort(),
itemEffectMentalStateValue = beData.itemEffectMentalStateValue.toByte(),
itemEffectMentalStateMinutesRemaining = beData.itemEffectMentalStateMinutesRemaining.toByte(),
itemEffectActivityLevelValue = beData.itemEffectActivityLevelValue.toByte(),
itemEffectActivityLevelMinutesRemaining = beData.itemEffectActivityLevelMinutesRemaining.toByte(),
itemEffectVitalPointsChangeValue = beData.itemEffectVitalPointsChangeValue.toByte(),
itemEffectVitalPointsChangeMinutesRemaining = beData.itemEffectVitalPointsChangeMinutesRemaining.toByte(),
transformationCountdownInMinutes = userCharacter.transformationCountdown.toUShort(),
injuryStatus = userCharacter.injuryStatus,
trainingPp = userCharacter.trophies.toUShort(),
currentPhaseBattlesWon = userCharacter.currentPhaseBattlesWon.toUShort(),
currentPhaseBattlesLost = userCharacter.currentPhaseBattlesLost.toUShort(),
totalBattlesWon = userCharacter.totalBattlesWon.toUShort(),
totalBattlesLost = userCharacter.totalBattlesLost.toUShort(),
activityLevel = userCharacter.activityLevel.toByte(),
heartRateCurrent = userCharacter.heartRateCurrent.toUByte(),
transformationHistory = paddedTransformationArray,
vitalHistory = Array(7) {
NfcCharacter.DailyVitals(0u, 0u, 0u, 0u)
},
appReserved1 = ByteArray(12) {0},
appReserved2 = Array(3) {0u},
trainingHp = beData.trainingHp.toUShort(),
trainingAp = beData.trainingAp.toUShort(),
trainingBp = beData.trainingBp.toUShort(),
remainingTrainingTimeInMinutes = beData.remainingTrainingTimeInMinutes.toUShort(),
abilityRarity = beData.abilityRarity,
abilityType = beData.abilityType.toUShort(),
abilityBranch = beData.abilityBranch.toUShort(),
abilityReset = beData.abilityReset.toByte(),
rank = beData.rank.toByte(),
itemType = beData.itemType.toByte(),
itemMultiplier = beData.itemMultiplier.toByte(),
itemRemainingTime = beData.itemRemainingTime.toByte(),
otp0 = byteArrayOf(8),
otp1 = byteArrayOf(8),
characterCreationFirmwareVersion = FirmwareVersion(
minorVersion = beData.minorVersion.toByte(),
majorVersion = beData.majorVersion.toByte()
)
)
return nfcData
}
return null
}
@@ -0,0 +1,23 @@
package com.github.nacabaro.vbhelper.utils
import com.github.cfogrady.vbnfc.data.NfcCharacter
fun padTransformationArray(
transformationArray: Array<NfcCharacter.Transformation>
): Array<NfcCharacter.Transformation> {
if (transformationArray.size >= 8) {
return transformationArray
}
val paddedArray = Array(8) {
NfcCharacter.Transformation(
toCharIndex = 255u,
year = 65535u,
month = 255u,
day = 255u
)
}
System.arraycopy(transformationArray, 0, paddedArray, 0, transformationArray.size)
return paddedArray
}
@@ -0,0 +1,19 @@
syntax = "proto3";
option java_package = "com.github.nacabaro.vbhelper.source.proto";
option java_multiple_files = true;
message Secrets {
string aes_key = 1;
repeated int32 vb_cipher = 2;
repeated int32 be_cipher = 3;
message HmacKeys {
string hmac_key_1 = 1;
string hmac_key_2 = 2;
}
HmacKeys vbdm_hmac_keys = 4;
HmacKeys vbc_hmac_keys = 5;
HmacKeys be_hmac_keys = 6;
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M160,880q-17,0 -28.5,-11.5T120,840v-200q0,-33 23.5,-56.5T200,560v-160q0,-33 23.5,-56.5T280,320h160v-58q-18,-12 -29,-29t-11,-41q0,-15 6,-29.5t18,-26.5l56,-56 56,56q12,12 18,26.5t6,29.5q0,24 -11,41t-29,29v58h160q33,0 56.5,23.5T760,400v160q33,0 56.5,23.5T840,640v200q0,17 -11.5,28.5T800,880L160,880ZM280,560h400v-160L280,400v160ZM200,800h560v-160L200,640v160ZM280,560h400,-400ZM200,800h560,-560ZM760,560L200,560h560Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M520,920v-240l-84,-80 -40,176 -276,-56 16,-80 192,40 64,-324 -72,28v136h-80v-188l158,-68q35,-15 51.5,-19.5T480,240q21,0 39,11t29,29l40,64q26,42 70.5,69T760,440v80q-66,0 -123.5,-27.5T540,420l-24,120 84,80v300h-80ZM540,220q-33,0 -56.5,-23.5T460,140q0,-33 23.5,-56.5T540,60q33,0 56.5,23.5T620,140q0,33 -23.5,56.5T540,220Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M57,880 L1,824l146,-146 -44,-118q-7,-18 -3,-41.5t23,-42.5l132,-132q12,-12 26,-18t31,-6q17,0 31,6t26,18l80,78q27,27 66,42.5t84,15.5v80q-60,0 -112,-19t-90,-57l-28,-28 -94,94 84,86v244h-80v-210l-52,-48v88L57,880ZM599,880v-280l84,-80 -24,-140q-15,18 -33,32t-39,26q-33,-2 -62.5,-14T475,392q45,-8 79.5,-30.5T611,304l40,-64q17,-27 47,-36.5t59,2.5l202,86v188h-80v-136l-72,-28L919,880h-84l-72,-300 -84,80v220h-80ZM459,340q-33,0 -56.5,-23.5T379,260q0,-33 23.5,-56.5T459,180q33,0 56.5,23.5T539,260q0,33 -23.5,56.5T459,340ZM659,180q-33,0 -56.5,-23.5T579,100q0,-33 23.5,-56.5T659,20q33,0 56.5,23.5T739,100q0,33 -23.5,56.5T659,180Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M440,777v-274L200,364v274l240,139ZM520,777 L760,638v-274L520,503v274ZM480,434 L717,297 480,160 243,297 480,434ZM160,708q-19,-11 -29.5,-29T120,639v-318q0,-22 10.5,-40t29.5,-29l280,-161q19,-11 40,-11t40,11l280,161q19,11 29.5,29t10.5,40v318q0,22 -10.5,40T800,708L520,869q-19,11 -40,11t-40,-11L160,708ZM480,480Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M160,520v-80h640v80L160,520Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m480,840 l-58,-52q-101,-91 -167,-157T150,512.5Q111,460 95.5,416T80,326q0,-94 63,-157t157,-63q52,0 99,22t81,62q34,-40 81,-62t99,-22q94,0 157,63t63,157q0,46 -15.5,90T810,512.5Q771,565 705,631T538,788l-58,52ZM480,732q96,-86 158,-147.5t98,-107q36,-45.5 50,-81t14,-70.5q0,-60 -40,-100t-100,-40q-47,0 -87,26.5T518,280h-76q-15,-41 -55,-67.5T300,186q-60,0 -100,40t-40,100q0,35 14,70.5t50,81q36,45.5 98,107T480,732ZM480,459Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M620,440q25,0 42.5,-17.5T680,380q0,-25 -17.5,-42.5T620,320q-25,0 -42.5,17.5T560,380q0,25 17.5,42.5T620,440ZM340,440q25,0 42.5,-17.5T400,380q0,-25 -17.5,-42.5T340,320q-25,0 -42.5,17.5T280,380q0,25 17.5,42.5T340,440ZM480,700q68,0 123.5,-38.5T684,560L276,560q25,63 80.5,101.5T480,700ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,480ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M660,720v-480h80v480h-80ZM220,720v-480l360,240 -360,240ZM300,480ZM300,570 L436,480 300,390v180Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m354,673 l126,-76 126,77 -33,-144 111,-96 -146,-13 -58,-136 -58,135 -146,13 111,97 -33,143ZM233,840l65,-281L80,370l288,-25 112,-265 112,265 288,25 -218,189 65,281 -247,-149 -247,149ZM480,490Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M360,120v-80h240v80L360,120ZM440,560h80v-240h-80v240ZM480,880q-74,0 -139.5,-28.5T226,774q-49,-49 -77.5,-114.5T120,520q0,-74 28.5,-139.5T226,266q49,-49 114.5,-77.5T480,160q62,0 119,20t107,58l56,-56 56,56 -56,56q38,50 58,107t20,119q0,74 -28.5,139.5T734,774q-49,49 -114.5,77.5T480,880ZM480,800q116,0 198,-82t82,-198q0,-116 -82,-198t-198,-82q-116,0 -198,82t-82,198q0,116 82,198t198,82ZM480,520Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M280,840v-80h160v-124q-49,-11 -87.5,-41.5T296,518q-75,-9 -125.5,-65.5T120,320v-40q0,-33 23.5,-56.5T200,200h80v-80h400v80h80q33,0 56.5,23.5T840,280v40q0,76 -50.5,132.5T664,518q-18,46 -56.5,76.5T520,636v124h160v80L280,840ZM280,432v-152h-80v40q0,38 22,68.5t58,43.5ZM480,560q50,0 85,-35t35,-85v-240L360,200v240q0,50 35,85t85,35ZM680,432q36,-13 58,-43.5t22,-68.5v-40h-80v152ZM480,380Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M156,447q-11,-12 -11,-28.5t11,-28.5l112,-112 -43,-43 -12,12q-12,12 -28.5,12T156,247q-11,-11 -11,-28t11,-28l80,-80q12,-12 28.5,-12t28.5,12q11,11 11,28t-11,28l-12,12 43,43 112,-112q12,-12 28.5,-12t28.5,12q12,12 12,28.5T493,167l-27,26 295,295q23,23 23,56.5T761,601l-28,29 189,188L808,818L676,686l-28,29q-23,23 -56.5,23T535,715L240,420l-27,27q-12,11 -28.5,11T156,447ZM296,364 L591,659 704,545 644,484 588,540q-12,11 -28.5,11.5T532,541q-12,-12 -12,-28.5t12,-28.5l56,-56 -60,-60 -56,56q-12,11 -28.5,11T415,424q-11,-12 -11,-28.5t11,-28.5l56,-56 -61,-61 -114,114ZM296,364 L410,250 296,364Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M600,880q-17,0 -28.5,-11.5T560,840v-43q-23,-4 -43.5,-11.5T478,765l-40,40q-12,11 -28.5,11.5T381,805q-12,-12 -12,-28.5t12,-28.5l41,-41q-3,-5 -6,-10.5t-6,-10.5l-27,-53 -49,49q-12,11 -28,11.5T278,682q-12,-12 -12,-28t12,-28l49,-50 -53,-26q-5,-2 -9,-4.5t-9,-5.5l-36,36q-12,11 -28.5,11.5T163,576q-12,-12 -12,-28t12,-28l35,-35q-14,-19 -22.5,-40.5T163,400h-43q-17,0 -28.5,-11.5T80,360q0,-17 11.5,-28.5T120,320h45q5,-19 12,-36t18,-33l-35,-35q-12,-12 -12,-28t12,-28q12,-12 28,-12t28,12l35,35q16,-11 33,-18t36,-12v-45q0,-17 11.5,-28.5T360,80q17,0 28.5,11.5T400,120v43q24,4 45.5,13t40.5,23l35,-35q12,-12 28.5,-12t28.5,12q12,12 12,28t-12,28l-37,37q2,4 4.5,8t4.5,9l25,50 46,-46q12,-12 28.5,-12t28.5,12q12,12 12,28.5T678,335l-48,47 56,28q6,3 12.5,6.5T710,424l40,-40q12,-12 28,-12t28,12q12,12 12,28.5T806,441l-40,39q12,18 19.5,38t11.5,42h43q17,0 28.5,11.5T880,600q0,17 -11.5,28.5T840,640h-45q-5,19 -12,35.5T765,708l34,34q12,12 12,28.5T799,799q-12,11 -28.5,11.5T742,799l-33,-34q-16,11 -33,18t-36,12v45q0,17 -11.5,28.5T600,880ZM594,720q58,0 95.5,-44T718,574q-5,-30 -22.5,-54T650,482l-66,-34q-23,-12 -41.5,-30.5T512,376l-34,-66q-16,-32 -46,-51t-66,-19q-58,0 -95.5,44T242,386q5,30 22.5,54t45.5,38l66,34q23,12 41.5,30.5T448,584l34,66q16,32 46,51t66,19ZM380,420q25,0 42.5,-17.5T440,360q0,-25 -17.5,-42.5T380,300q-25,0 -42.5,17.5T320,360q0,25 17.5,42.5T380,420ZM580,670q21,0 35.5,-14.5T630,620q0,-21 -14.5,-35.5T580,570q-21,0 -35.5,14.5T530,620q0,21 14.5,35.5T580,670ZM480,480Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M300,120q52,0 99,22t81,62q34,-40 81,-62t99,-22q94,0 157,63t63,157q0,5 -0.5,10t-0.5,10h-80q1,-5 1,-10v-10q0,-60 -40,-100t-100,-40q-47,0 -87,26.5T518,294h-76q-15,-41 -55,-67.5T300,200q-60,0 -100,40t-40,100v10q0,5 1,10L81,360q0,-5 -0.5,-10t-0.5,-10q0,-94 63,-157t157,-63ZM212,600h112q32,31 70,67t86,79q48,-43 86,-79t70,-67h113q-38,42 -90,91T538,802l-58,52 -58,-52q-69,-62 -120.5,-111T212,600ZM442,640q13,0 22.5,-7.5T478,613l54,-163 35,52q5,8 14,13t19,5h320v-80L623,440l-69,-102q-6,-9 -15.5,-13.5T518,320q-13,0 -22.5,7.5T482,347l-54,162 -34,-51q-5,-8 -14,-13t-19,-5L40,440v80h297l69,102q6,9 15.5,13.5T442,640ZM480,473Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,76 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.source.proto.Secrets
import org.junit.Assert
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class ApkSecretsImporterTest {
@Test
fun testThatRealImportSecretsPasses() {
val apkFileSecretsImporter = ApkSecretsImporter()
val url = getAndAssertApkFile()
val file = File(url.path)
file.inputStream().use {
val secrets = apkFileSecretsImporter.importSecrets(it)
Assert.assertEquals("Cipher size isn't correct", 16, secrets.vbCipherCount)
Assert.assertEquals("BE Cipher size isn't correct", 16, secrets.beCipherCount)
Assert.assertFalse("AES Key is empty", secrets.aesKey.isEmpty())
assertHmacKeysArePopulated("VBDM", secrets.vbdmHmacKeys)
assertHmacKeysArePopulated("VBC", secrets.vbcHmacKeys)
assertHmacKeysArePopulated("BE", secrets.beHmacKeys)
}
}
@Test
fun testThatApkSecretsImporterCallsDexSecretImporterOnDexFile() {
val expectedDexContents = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1)
var foundFile = false
val apkFileSecretsImporter = ApkSecretsImporter {
val inputStreamContents = it.readAllBytes()
Assert.assertTrue("Unexpected file contents received by DexSecretsImporter", inputStreamContents.contentEquals(expectedDexContents))
foundFile = true
Secrets.newBuilder().build()
}
val apkBytes = constructTestApk(expectedDexContents)
ByteArrayInputStream(apkBytes).use {
apkFileSecretsImporter.importSecrets(it)
}
Assert.assertTrue("${ApkSecretsImporter.DEX_FILE} not found", foundFile)
}
fun constructTestApk(dexContents: ByteArray): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
ZipOutputStream(byteArrayOutputStream).use { zipOutputStream ->
zipOutputStream.putNextEntry(ZipEntry("dummy.txt"))
zipOutputStream.write("This is a text file".toByteArray(StandardCharsets.UTF_8))
zipOutputStream.putNextEntry(ZipEntry("AndroidManifest.xml"))
zipOutputStream.write("Malformed xml!".toByteArray(StandardCharsets.UTF_8))
zipOutputStream.putNextEntry(ZipEntry("assets/"))
zipOutputStream.putNextEntry(ZipEntry("assets/bad.assets"))
zipOutputStream.write("Malformed asset!".toByteArray(StandardCharsets.UTF_8))
zipOutputStream.putNextEntry(ZipEntry(ApkSecretsImporter.DEX_FILE))
zipOutputStream.write(dexContents)
}
return byteArrayOutputStream.toByteArray()
}
private fun getAndAssertApkFile(): URL {
val url = javaClass.getResource("com.bandai.vitalbraceletarena.apk")
if(url == null) {
Assert.assertTrue("""
Create `resources\com\github\nacabaro\vbhelper\source` within the src/test directory.
Add com.bandai.vitalbraceletarena.apk (the official apk) in the above directory. It
should never be checked in and should be on the .gitignore.
""".trimIndent(), false)
}
return url!!
}
}
@@ -0,0 +1,82 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.source.proto.Secrets.HmacKeys
import org.junit.Assert
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.File
import java.net.URL
import java.nio.ByteOrder
import java.security.InvalidKeyException
fun assertHmacKeysArePopulated(msg: String, hmacKeys: HmacKeys) {
Assert.assertFalse("$msg hmacKey1 is empty", hmacKeys.hmacKey1.isEmpty())
Assert.assertFalse("$msg hmacKey2 is empty", hmacKeys.hmacKey2.isEmpty())
}
class DexFileSecretsImporterTest {
@Test
fun testThatImportSecretsIsPopulated() {
val dexFileSecretsImporter = DexFileSecretsImporter()
val url = getAndAssertClassesDexFile()
val file = File(url.path)
file.inputStream().use {
val secrets = dexFileSecretsImporter.importSecrets(it)
Assert.assertEquals("Cipher size isn't correct", 16, secrets.vbCipherCount)
Assert.assertEquals("BE Cipher size isn't correct", 16, secrets.beCipherCount)
Assert.assertFalse("AES Key is empty", secrets.aesKey.isEmpty())
assertHmacKeysArePopulated("VBDM", secrets.vbdmHmacKeys)
assertHmacKeysArePopulated("VBC", secrets.vbcHmacKeys)
assertHmacKeysArePopulated("BE", secrets.beHmacKeys)
}
}
@Test
fun testThatImportWrongSecretsThrows() {
val dexFileSecretsImporter = DexFileSecretsImporter()
val url = getAndAssertClassesDexFile()
val file = File(url.path)
val content = file.readBytes()
val badCipher = intArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15).toByteArray(ByteOrder.BIG_ENDIAN)
badCipher.copyInto(content, DexFileSecretsImporter.BE_SUBSTITUTION_CIPHER_IDX)
ByteArrayInputStream(content).use {
Assert.assertThrows("Secrets are validated", InvalidKeyException::class.java) {
dexFileSecretsImporter.importSecrets(it)
}
}
}
private fun getAndAssertClassesDexFile(): URL {
val url = javaClass.getResource("classes.dex")
if(url == null) {
Assert.assertTrue("""
Create `resources\com\github\nacabaro\vbhelper\source` within the src/test directory.
Add classes.dex from the official apk in the above directory. It should never be
checked in and should be on the .gitignore.
""".trimIndent(), false)
}
return url!!
}
}
fun IntArray.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray {
val byteArray = ByteArray(this.size*4)
for(i in this.indices) {
val byteArrayIndex = i*4
this[i].toByteArray(byteArray, byteArrayIndex, byteOrder)
}
return byteArray
}
fun Int.toByteArray(bytes: ByteArray, dstIndex: Int, byteOrder: ByteOrder = ByteOrder.nativeOrder()) {
val asUInt = this.toUInt()
for(i in 0 until 4) {
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
bytes[i+dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
} else {
bytes[(3-i) + dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
}
}
}
+6
View File
@@ -3,4 +3,10 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
buildscript {
dependencies {
classpath(libs.protobuf.gradle.plugin)
}
}
+6
View File
@@ -1,5 +1,6 @@
[versions]
agp = "8.7.3"
datastore = "1.1.1"
kotlin = "2.0.0"
coreKtx = "1.15.0"
junit = "4.13.2"
@@ -8,12 +9,15 @@ espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
composeBom = "2024.04.01"
protobufGradlePlugin = "0.9.4"
protobufJavalite = "4.27.0"
roomRuntime = "2.6.1"
vbNfcReader = "0.1.0"
dimReader = "2.1.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
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" }
@@ -29,6 +33,8 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
protobuf-gradle-plugin = { module = "com.google.protobuf:protobuf-gradle-plugin", version.ref = "protobufGradlePlugin" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufJavalite" }
vb-nfc-reader = { module = "com.github.cfogrady:vb-nfc-reader", version.ref = "vbNfcReader" }
dim-reader = { module = "com.github.cfogrady:vb-dim-reader", version.ref = "dimReader" }