mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-09-30 23:21:36 +00:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
660dbaca19 | ||
|
|
23e233227a | ||
|
|
542072c238 | ||
|
|
42dd87f0c4 | ||
|
|
88163684ca | ||
|
|
586d6c01ef | ||
|
|
5d996edc1e | ||
|
|
074ced1082 | ||
|
|
5a1d52aa1d | ||
|
|
98bd74c544 | ||
|
|
ce1cf3eddb | ||
|
|
0bd745ac86 | ||
|
|
0d999925b5 | ||
|
|
dd893a08da | ||
|
|
c5e8236e19 | ||
|
|
cac5198488 | ||
|
|
7f22650601 | ||
|
|
1088e5259a | ||
|
|
eabf8770cc | ||
|
|
da5e6c6628 | ||
|
|
4c0e2fafd0 | ||
|
|
4529925906 | ||
|
|
c456d455ef | ||
|
|
9871f0420f | ||
|
|
f4974c8705 | ||
|
|
5d6e3743f0 | ||
|
|
e4366ce15c |
@@ -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
|
||||
|
||||
@@ -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,58 +10,84 @@ 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.characters.Card
|
||||
import com.github.nacabaro.vbhelper.domain.Sprites
|
||||
import com.github.nacabaro.vbhelper.domain.Character
|
||||
import com.github.nacabaro.vbhelper.domain.characters.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.navigation.AppNavigationHandlers
|
||||
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
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 scanScreenController = ScanScreenControllerImpl(
|
||||
application.container.dataStoreSecretsRepository.secretsFlow,
|
||||
this::handleReceivedNfcCharacter,
|
||||
this,
|
||||
this::registerActivityLifecycleListener,
|
||||
this::unregisterActivityLifecycleListener
|
||||
)
|
||||
val settingsScreenController = SettingsScreenControllerImpl(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
VBHelperTheme {
|
||||
MainApplication()
|
||||
MainApplication(scanScreenController, settingsScreenController)
|
||||
}
|
||||
}
|
||||
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,22 +106,30 @@ class MainActivity : ComponentActivity() {
|
||||
inputStream.use { fileReader ->
|
||||
val dimReader = DimReader()
|
||||
val card = dimReader.readCard(fileReader, false)
|
||||
val dimModel = Dim(
|
||||
|
||||
Log.i("MainActivity", "Card name: ${card is BemCard}")
|
||||
|
||||
val cardModel = Card(
|
||||
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
|
||||
.dimDao()
|
||||
.insertNewDim(dimModel)
|
||||
.insertNewDim(cardModel)
|
||||
|
||||
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 +153,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 +187,16 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainApplication() {
|
||||
var isDoneReadingCharacter by remember { mutableStateOf(false) }
|
||||
private fun MainApplication(
|
||||
scanScreenController: ScanScreenControllerImpl,
|
||||
settingsScreenController: SettingsScreenControllerImpl
|
||||
) {
|
||||
|
||||
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,79 +207,17 @@ 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
|
||||
}
|
||||
|
||||
//
|
||||
/*
|
||||
TODO:
|
||||
- Make it able to detect the different model of watches
|
||||
- Support for regular VB
|
||||
|
||||
The good news is that the theory behind inserting to the database should be working
|
||||
@@ -271,9 +232,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()
|
||||
@@ -297,62 +256,69 @@ class MainActivity : ComponentActivity() {
|
||||
activityLevel = nfcCharacter.value!!.activityLevel.toInt(),
|
||||
heartRateCurrent = nfcCharacter.value!!.heartRateCurrent.toInt(),
|
||||
characterType = when (nfcCharacter.value) {
|
||||
is BENfcCharacter -> com.github.nacabaro.vbhelper.domain.DeviceType.BEDevice
|
||||
else -> com.github.nacabaro.vbhelper.domain.DeviceType.VBDevice
|
||||
}
|
||||
is BENfcCharacter -> DeviceType.BEDevice
|
||||
else -> 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
|
||||
.userCharacterDao()
|
||||
.insertTransformationHistory(*domainTransformationHistory.toTypedArray())
|
||||
} else {
|
||||
storageRepository
|
||||
.characterDao()
|
||||
.insertTransformation(characterId, item.toCharIndex.toInt(), dimData.id, date)
|
||||
|
||||
storageRepository
|
||||
.dexDao()
|
||||
.insertCharacter(item.toCharIndex.toInt(), dimData.id, date)
|
||||
}
|
||||
}
|
||||
} 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,8 +1,8 @@
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -15,23 +15,24 @@ 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(
|
||||
name: String,
|
||||
logo: BitmapData,
|
||||
obtainedCharacters: Int,
|
||||
totalCharacters: Int,
|
||||
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,13 +51,23 @@ fun DexDiMEntry(
|
||||
filterQuality = FilterQuality.None,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.size(64.dp)
|
||||
.size(dpSize)
|
||||
)
|
||||
Text(
|
||||
text = name,
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
modifier = Modifier
|
||||
)
|
||||
Text(
|
||||
text = "$obtainedCharacters of $totalCharacters characters obtained",
|
||||
fontFamily = MaterialTheme.typography.labelSmall.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelSmall.fontSize,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+76
@@ -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)
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,8 +3,9 @@ package com.github.nacabaro.vbhelper.daos
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.domain.Character
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
import com.github.nacabaro.vbhelper.domain.Sprites
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
|
||||
@Dao
|
||||
interface CharacterDao {
|
||||
@@ -25,4 +26,26 @@ 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 Card 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)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.dtos.CardDtos
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
|
||||
@Dao
|
||||
interface DexDao {
|
||||
@Query("""
|
||||
INSERT OR IGNORE INTO Dex(id, discoveredOn)
|
||||
VALUES (
|
||||
(SELECT id FROM Character WHERE monIndex = :charIndex AND dimId = :cardId),
|
||||
:discoveredOn
|
||||
)
|
||||
""")
|
||||
fun insertCharacter(charIndex: Int, cardId: Long, discoveredOn: Long)
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
c.id AS id,
|
||||
c.sprite1 AS spriteIdle,
|
||||
c.spritesWidth AS spriteWidth,
|
||||
c.spritesHeight AS spriteHeight,
|
||||
d.discoveredOn AS discoveredOn
|
||||
FROM character c
|
||||
LEFT JOIN dex d ON c.id = d.id
|
||||
WHERE c.dimId = :cardId
|
||||
""")
|
||||
suspend fun getSingleCardProgress(cardId: Long): List<CharacterDtos.CardProgress>
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
c.id as cardId,
|
||||
c.name as cardName,
|
||||
c.logo as cardLogo,
|
||||
c.logoWidth as logoWidth,
|
||||
c.logoHeight as logoHeight,
|
||||
(SELECT COUNT(*) FROM Character cc WHERE cc.dimId = c.id) AS totalCharacters,
|
||||
(SELECT COUNT(*) FROM Dex d JOIN Character cc ON d.id = cc.id WHERE cc.dimId = c.id AND d.discoveredOn IS NOT NULL) AS obtainedCharacters
|
||||
FROM Card c
|
||||
""")
|
||||
suspend fun getCardsWithProgress(): List<CardDtos.CardProgress>
|
||||
}
|
||||
@@ -4,16 +4,16 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.domain.Dim
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Card
|
||||
|
||||
@Dao
|
||||
interface DiMDao {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertNewDim(dim: Dim): Long
|
||||
suspend fun insertNewDim(card: Card): Long
|
||||
|
||||
@Query("SELECT * FROM Dim")
|
||||
suspend fun getAllDims(): List<Dim>
|
||||
@Query("SELECT * FROM Card")
|
||||
suspend fun getAllDims(): List<Card>
|
||||
|
||||
@Query("SELECT * FROM Dim WHERE dimId = :id")
|
||||
fun getDimById(id: Int): Dim?
|
||||
@Query("SELECT * FROM Card WHERE dimId = :id")
|
||||
fun getDimById(id: Int): Card?
|
||||
}
|
||||
@@ -20,20 +20,64 @@ 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 Card 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 Card 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)
|
||||
}
|
||||
@@ -3,11 +3,13 @@ package com.github.nacabaro.vbhelper.database
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.github.nacabaro.vbhelper.daos.CharacterDao
|
||||
import com.github.nacabaro.vbhelper.daos.DexDao
|
||||
import com.github.nacabaro.vbhelper.daos.DiMDao
|
||||
import com.github.nacabaro.vbhelper.daos.UserCharacterDao
|
||||
import com.github.nacabaro.vbhelper.domain.Character
|
||||
import com.github.nacabaro.vbhelper.domain.Dim
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Card
|
||||
import com.github.nacabaro.vbhelper.domain.Sprites
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Dex
|
||||
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
|
||||
@@ -15,16 +17,18 @@ import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [
|
||||
Dim::class,
|
||||
Card::class,
|
||||
Character::class,
|
||||
Sprites::class,
|
||||
UserCharacter::class,
|
||||
BECharacterData::class,
|
||||
TransformationHistory::class
|
||||
TransformationHistory::class,
|
||||
Dex::class
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun dimDao(): DiMDao
|
||||
abstract fun characterDao(): CharacterDao
|
||||
abstract fun userCharacterDao(): UserCharacterDao
|
||||
abstract fun dexDao(): DexDao
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.domain
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Card
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
@@ -13,7 +14,7 @@ import androidx.room.PrimaryKey
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = Dim::class,
|
||||
entity = Card::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["dimId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.domain
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.domain
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
|
||||
+4
-4
@@ -1,11 +1,10 @@
|
||||
package com.github.nacabaro.vbhelper.domain
|
||||
package com.github.nacabaro.vbhelper.domain.characters
|
||||
|
||||
import android.icu.text.ListFormatter.Width
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Dim(
|
||||
data class Card(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val dimId: Int,
|
||||
@@ -13,5 +12,6 @@ data class Dim(
|
||||
val logoWidth: Int,
|
||||
val logoHeight: Int,
|
||||
val name: String,
|
||||
val stageCount: Int
|
||||
val stageCount: Int,
|
||||
val isBEm: Boolean
|
||||
)
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.github.nacabaro.vbhelper.domain
|
||||
package com.github.nacabaro.vbhelper.domain.characters
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@@ -7,7 +7,7 @@ import androidx.room.ForeignKey
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Dim::class,
|
||||
entity = Card::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["dimId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.github.nacabaro.vbhelper.domain.characters
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Character::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class Dex(
|
||||
@PrimaryKey val id: Long = 0,
|
||||
val discoveredOn: Long
|
||||
)
|
||||
+9
-4
@@ -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.characters.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
|
||||
)
|
||||
|
||||
@@ -4,8 +4,8 @@ import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.DeviceType
|
||||
import com.github.nacabaro.vbhelper.domain.Character
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.github.nacabaro.vbhelper.dtos
|
||||
|
||||
object CardDtos {
|
||||
data class CardProgress (
|
||||
val cardId: Long,
|
||||
val cardName: String,
|
||||
val cardLogo: ByteArray,
|
||||
val logoWidth: Int,
|
||||
val logoHeight: Int,
|
||||
val totalCharacters: Int,
|
||||
val obtainedCharacters: Int,
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.github.nacabaro.vbhelper.dtos
|
||||
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.DeviceType
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
|
||||
|
||||
object CharacterDtos {
|
||||
@@ -27,6 +26,29 @@ 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
|
||||
)
|
||||
|
||||
data class CardProgress(
|
||||
val id: Long,
|
||||
val spriteIdle: ByteArray,
|
||||
val spriteWidth: Int,
|
||||
val spriteHeight: Int,
|
||||
val discoveredOn: Long?
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.github.nacabaro.vbhelper.navigation
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -11,18 +10,24 @@ import androidx.navigation.compose.rememberNavController
|
||||
import com.github.nacabaro.vbhelper.screens.BattlesScreen
|
||||
import com.github.nacabaro.vbhelper.screens.DexScreen
|
||||
import com.github.nacabaro.vbhelper.screens.DiMScreen
|
||||
import com.github.nacabaro.vbhelper.screens.HomeScreen
|
||||
import com.github.nacabaro.vbhelper.screens.ScanScreen
|
||||
import com.github.nacabaro.vbhelper.screens.SettingsScreen
|
||||
import com.github.nacabaro.vbhelper.screens.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.SettingsScreen
|
||||
import com.github.nacabaro.vbhelper.screens.SpriteViewer
|
||||
import com.github.nacabaro.vbhelper.screens.StorageScreen
|
||||
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl
|
||||
|
||||
data class AppNavigationHandlers(
|
||||
val settingsScreenController: SettingsScreenControllerImpl,
|
||||
val scanScreenController: ScanScreenControllerImpl,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AppNavigation(
|
||||
onClickRead: () -> Unit,
|
||||
onClickScan: () -> Unit,
|
||||
applicationNavigationHandlers: AppNavigationHandlers,
|
||||
onClickImportCard: () -> Unit,
|
||||
isDoneReadingCharacter: Boolean
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
@@ -33,55 +38,64 @@ 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,
|
||||
onClickImportCard = onClickImportCard
|
||||
settingsScreenController = applicationNavigationHandlers.settingsScreenController,
|
||||
onClickImportCard = onClickImportCard,
|
||||
)
|
||||
}
|
||||
composable(BottomNavItem.Viewer.route) {
|
||||
composable(NavigationItems.Viewer.route) {
|
||||
SpriteViewer(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
composable(BottomNavItem.CardView.route) {
|
||||
val dimId = it.arguments?.getString("dimId")
|
||||
Log.d("dimId", dimId.toString())
|
||||
if (dimId != null) {
|
||||
composable(NavigationItems.CardView.route) {
|
||||
val cardId = it.arguments?.getString("cardId")
|
||||
if (cardId != null) {
|
||||
DiMScreen(
|
||||
navController = navController,
|
||||
dimId = dimId.toInt()
|
||||
dimId = cardId.toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
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/{cardId}", R.drawable.baseline_image_24, "Card")
|
||||
object Items : NavigationItems("Items", R.drawable.baseline_data_24, "Items")
|
||||
}
|
||||
@@ -18,8 +18,9 @@ import com.github.nacabaro.vbhelper.utils.BitmapData
|
||||
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.domain.characters.Card
|
||||
import com.github.nacabaro.vbhelper.dtos.CardDtos
|
||||
import com.github.nacabaro.vbhelper.navigation.NavigationItems
|
||||
import com.github.nacabaro.vbhelper.source.DexRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -31,21 +32,21 @@ fun DexScreen(
|
||||
val application = LocalContext.current.applicationContext as VBHelper
|
||||
val dexRepository = DexRepository(application.container.db)
|
||||
|
||||
val dimList = remember { mutableStateOf<List<Dim>>(emptyList()) }
|
||||
val cardList = remember { mutableStateOf<List<CardDtos.CardProgress>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(dexRepository) {
|
||||
coroutineScope.launch {
|
||||
val newDimList = dexRepository.getAllDims()
|
||||
dimList.value = newDimList // Replace the entire list atomically
|
||||
cardList.value = newDimList // Replace the entire list atomically
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold (
|
||||
topBar = {
|
||||
TopBanner(
|
||||
text = "Discovered Digimon",
|
||||
text = "Discovered characters",
|
||||
onGearClick = {
|
||||
navController.navigate(BottomNavItem.Viewer.route)
|
||||
navController.navigate(NavigationItems.Viewer.route)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -54,22 +55,24 @@ fun DexScreen(
|
||||
modifier = Modifier
|
||||
.padding(top = contentPadding.calculateTopPadding())
|
||||
) {
|
||||
items(dimList.value) {
|
||||
items(cardList.value) {
|
||||
DexDiMEntry(
|
||||
name = it.name,
|
||||
name = it.cardName,
|
||||
logo = BitmapData(
|
||||
bitmap = it.logo,
|
||||
bitmap = it.cardLogo,
|
||||
width = it.logoWidth,
|
||||
height = it.logoHeight
|
||||
),
|
||||
onClick = {
|
||||
navController
|
||||
.navigate(
|
||||
BottomNavItem
|
||||
NavigationItems
|
||||
.CardView.route
|
||||
.replace("{dimId}", "${it.id}")
|
||||
.replace("{cardId}", "${it.cardId}")
|
||||
)
|
||||
},
|
||||
obtainedCharacters = it.obtainedCharacters,
|
||||
totalCharacters = it.totalCharacters,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
|
||||
@@ -14,21 +14,22 @@ import androidx.navigation.NavController
|
||||
import com.github.nacabaro.vbhelper.utils.BitmapData
|
||||
import com.github.nacabaro.vbhelper.components.CharacterEntry
|
||||
import com.github.nacabaro.vbhelper.components.TopBanner
|
||||
import com.github.nacabaro.vbhelper.domain.Character
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
import com.github.nacabaro.vbhelper.source.DexRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun DiMScreen(
|
||||
navController: NavController,
|
||||
dimId: Int
|
||||
dimId: Long
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val application = LocalContext.current.applicationContext as VBHelper
|
||||
val dexRepository = DexRepository(application.container.db)
|
||||
|
||||
val characterList = remember { mutableStateOf<List<Character>>(emptyList()) }
|
||||
val characterList = remember { mutableStateOf<List<CharacterDtos.CardProgress>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(dexRepository) {
|
||||
coroutineScope.launch {
|
||||
@@ -40,7 +41,7 @@ fun DiMScreen(
|
||||
Scaffold (
|
||||
topBar = {
|
||||
TopBanner(
|
||||
text = "Discovered Digimon",
|
||||
text = "Discovered characters",
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
@@ -54,10 +55,11 @@ fun DiMScreen(
|
||||
items(characterList.value) { character ->
|
||||
CharacterEntry(
|
||||
onClick = { },
|
||||
obscure = character.discoveredOn == null,
|
||||
icon = BitmapData(
|
||||
bitmap = character.sprite1,
|
||||
width = character.spritesWidth,
|
||||
height = character.spritesHeight
|
||||
bitmap = character.spriteIdle,
|
||||
width = character.spriteWidth,
|
||||
height = character.spriteHeight,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+201
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+171
@@ -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.utils.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")
|
||||
}
|
||||
+2
-1
@@ -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
|
||||
)
|
||||
}
|
||||
+18
@@ -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)
|
||||
}
|
||||
+147
@@ -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))
|
||||
}
|
||||
}
|
||||
+13
-7
@@ -1,4 +1,4 @@
|
||||
package com.github.nacabaro.vbhelper.screens
|
||||
package com.github.nacabaro.vbhelper.screens.settingsScreen
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -22,6 +22,7 @@ import com.github.nacabaro.vbhelper.components.TopBanner
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
navController: NavController,
|
||||
settingsScreenController: SettingsScreenControllerImpl,
|
||||
onClickImportCard: () -> Unit
|
||||
) {
|
||||
Scaffold (
|
||||
@@ -42,12 +43,17 @@ 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.onClickImportApk()
|
||||
}
|
||||
SettingsSection("Data management")
|
||||
SettingsEntry(title = "Export data", description = "Export application database") {
|
||||
settingsScreenController.onClickOpenDirectory()
|
||||
}
|
||||
SettingsEntry(title = "Import data", description = "Import application database") {
|
||||
settingsScreenController.onClickImportDatabase()
|
||||
}
|
||||
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") { }
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.github.nacabaro.vbhelper.screens.settingsScreen
|
||||
|
||||
interface SettingsScreenController {
|
||||
fun onClickOpenDirectory()
|
||||
fun onClickImportDatabase()
|
||||
fun onClickImportApk()
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package com.github.nacabaro.vbhelper.screens.settingsScreen
|
||||
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||
import com.github.nacabaro.vbhelper.source.ApkSecretsImporter
|
||||
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 java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
|
||||
class SettingsScreenControllerImpl(
|
||||
private val context: ComponentActivity,
|
||||
): SettingsScreenController {
|
||||
private val roomDbName = "internalDb"
|
||||
private val filePickerLauncher: ActivityResultLauncher<String>
|
||||
private val filePickerOpenerLauncher: ActivityResultLauncher<Array<String>>
|
||||
private val filePickerApk: ActivityResultLauncher<Array<String>>
|
||||
private val secretsImporter: SecretsImporter = ApkSecretsImporter()
|
||||
private val application = context.applicationContext as VBHelper
|
||||
private val secretsRepository: SecretsRepository = application.container.dataStoreSecretsRepository
|
||||
|
||||
init {
|
||||
filePickerLauncher = context.registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/octet-stream")
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
exportDatabase(uri)
|
||||
} else {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "No destination selected", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filePickerOpenerLauncher = context.registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
importDatabase(uri)
|
||||
} else {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "No source selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filePickerApk = context.registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
importApk(uri)
|
||||
} else {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "APK import cancelled", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClickOpenDirectory() {
|
||||
filePickerLauncher.launch("My application data.vbhelper")
|
||||
}
|
||||
|
||||
override fun onClickImportDatabase() {
|
||||
filePickerOpenerLauncher.launch(arrayOf("application/octet-stream"))
|
||||
}
|
||||
|
||||
override fun onClickImportApk() {
|
||||
filePickerApk.launch(arrayOf("*/*"))
|
||||
}
|
||||
|
||||
private fun exportDatabase(destinationUri: Uri) {
|
||||
context.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val dbFile = File(context.getDatabasePath(roomDbName).absolutePath)
|
||||
if (!dbFile.exists()) {
|
||||
throw IllegalStateException("Database file does not exist!")
|
||||
}
|
||||
|
||||
application.container.db.close()
|
||||
|
||||
context.contentResolver.openOutputStream(destinationUri)?.use { outputStream ->
|
||||
dbFile.inputStream().use { inputStream ->
|
||||
copyFile(inputStream, outputStream)
|
||||
}
|
||||
} ?: throw IllegalArgumentException("Unable to open destination Uri for writing")
|
||||
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Database exported successfully!", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, "Closing application to avoid changes.", Toast.LENGTH_LONG).show()
|
||||
context.finishAffinity()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ScanScreenController", "Error exporting database $e")
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Error exporting database: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun importDatabase(sourceUri: Uri) {
|
||||
context.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (!getFileNameFromUri(sourceUri)!!.endsWith(".vbhelper")) {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Invalid file format", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
application.container.db.close()
|
||||
|
||||
val dbPath = context.getDatabasePath(roomDbName)
|
||||
val shmFile = File(dbPath.parent, "$roomDbName-shm")
|
||||
val walFile = File(dbPath.parent, "$roomDbName-wal")
|
||||
|
||||
// Delete existing database files
|
||||
if (dbPath.exists()) dbPath.delete()
|
||||
if (shmFile.exists()) shmFile.delete()
|
||||
if (walFile.exists()) walFile.delete()
|
||||
|
||||
val dbFile = File(dbPath.absolutePath)
|
||||
|
||||
context.contentResolver.openInputStream(sourceUri)?.use { inputStream ->
|
||||
dbFile.outputStream().use { outputStream ->
|
||||
copyFile(inputStream, outputStream)
|
||||
}
|
||||
} ?: throw IllegalArgumentException("Unable to open source Uri for reading")
|
||||
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Database imported successfully!", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, "Reopen the app to finish import process!", Toast.LENGTH_LONG).show()
|
||||
context.finishAffinity()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ScanScreenController", "Error importing database $e")
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Error importing database: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileNameFromUri(uri: Uri): String? {
|
||||
var fileName: String? = null
|
||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val nameIndex = it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
|
||||
fileName = it.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
|
||||
val buffer = ByteArray(1024)
|
||||
var bytesRead: Int
|
||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
}
|
||||
outputStream.flush()
|
||||
}
|
||||
|
||||
private fun importApk(uri: Uri) {
|
||||
context.lifecycleScope.launch(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(uri).use {
|
||||
if(it == null) {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Selected file is empty!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
val secrets: Secrets?
|
||||
try {
|
||||
secrets = secretsImporter.importSecrets(it)
|
||||
} catch (e: Exception) {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Secrets import failed. Please only select the official Vital Arena App 2.1.0 APK.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
context.lifecycleScope.launch(Dispatchers.IO) {
|
||||
secretsRepository.updateSecrets(secrets)
|
||||
}.invokeOnCompletion {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Secrets successfully imported. Connections with devices are now possible.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import com.github.nacabaro.vbhelper.database.AppDatabase
|
||||
import com.github.nacabaro.vbhelper.domain.Character
|
||||
import com.github.nacabaro.vbhelper.domain.Dim
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Card
|
||||
import com.github.nacabaro.vbhelper.dtos.CardDtos
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
|
||||
class DexRepository (
|
||||
private val db: AppDatabase
|
||||
) {
|
||||
suspend fun getAllDims(): List<Dim> {
|
||||
return db.dimDao().getAllDims()
|
||||
suspend fun getAllDims(): List<CardDtos.CardProgress> {
|
||||
return db.dexDao().getCardsWithProgress()
|
||||
}
|
||||
|
||||
suspend fun getCharactersByDimId(dimId: Int): List<Character> {
|
||||
return db.characterDao().getCharacterByDimId(dimId)
|
||||
suspend fun getCharactersByDimId(cardId: Long): List<CharacterDtos.CardProgress> {
|
||||
return db.dexDao().getSingleCardProgress(cardId)
|
||||
}
|
||||
}
|
||||
@@ -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,99 @@
|
||||
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.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
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.github.nacabaro.vbhelper.domain
|
||||
package com.github.nacabaro.vbhelper.utils
|
||||
|
||||
enum class DeviceType {
|
||||
VBDevice,
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user