Compare commits

..
Author SHA1 Message Date
Nacho 660dbaca19 Forgot to include the settingsscreencontroller interface my bad 2025-01-18 16:01:42 +01:00
Nacho 23e233227a Finished refactoring the old SettingsScreenController.kt 2025-01-18 12:05:10 +01:00
Nacho 542072c238 Forgot to close DB before importing process
This will clear the database lock file before closing the application, ensuring that during the second startup the application does not see anything weird with the data. Not closing the database before import can lead to weird behavior.
2025-01-18 01:05:45 +01:00
Nacho 42dd87f0c4 Data export working
During export the database will be closed, otherwise the WAL will ruin the export. data will be saved as a .vbhelper file, then the app will be closed to avoid making any changes to the DB
For import, we check if the extension is .vbhelper, otherwise we can corrupt the database, IMPORTANT. Next we delete everything related to RoomDB and we swap it with the new files. Finally the app will be closed to ensure RoomDB is running on the new DB.

Finally, I still have to reintegrate the importApk functionality with the NewSettingsScreenController and get rid of the old one.
2025-01-18 00:45:16 +01:00
Nacho 88163684ca Dex stuff
- Added dex domain model, and rearranged class
- Added dex functionality to the dex screen, and updated the corresponding controller
2025-01-17 21:12:41 +01:00
nacabaroandGitHub 586d6c01ef Merge pull request #18 from nacabaro/ui/home_screen
A lot more things
2025-01-16 01:05:29 +01:00
nacabaroandGitHub 5d996edc1e Merge branch 'main' into ui/home_screen 2025-01-16 01:05:21 +01:00
nacabaroandGitHub 074ced1082 Merge pull request #17 from cfogrady/ObscuredDex
Algorithm to obscure undiscovered digimon
2025-01-16 01:04:32 +01:00
Nacho 5a1d52aa1d A lot more things
- Moved scan button to inside the home screen
- Added items screen
- Scanning functionality updated to work with BEms
- Different home screen layouts depending on the configuration (BE with BEm, BE with DIm, since they don't have the same data, looking at you, special training)
- It is now possible to import BEms
- Character size is now more accurate, (no more big babies)
- Once sent to the watch, characters are deleted from the app, and it shouldn't fail
- Updated domain model to support evolution history.

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

(Also added some more icons)

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

Originally this was also going to cover the home screen, since my idea was to have marked as active (the one shown in the home screen) be the one sent to the watch, but for testing I have added a "send to bracelet" button on the pop-up on the storage screen.
2025-01-12 00:57:34 +01:00
nacabaroandGitHub 7f22650601 Merge pull request #13 from cfogrady/TransparentBackground
Make background on characters and icons transparent.
2025-01-10 23:10:04 +01:00
Christopher O'Grady 1088e5259a Make background on characters and icons transparent. 2025-01-10 14:24:12 -05:00
nacabaroandGitHub eabf8770cc Merge pull request #12 from cfogrady/IntegrateSecretsWithApp
Integrate secrets with app
2025-01-10 19:38:26 +01:00
nacabaroandGitHub da5e6c6628 Merge pull request #11 from cfogrady/SecretsRepo
Add Secrets Repo Using a Proto DataStore
2025-01-10 19:38:13 +01:00
nacabaroandGitHub 4c0e2fafd0 Merge pull request #10 from cfogrady/APK_Import
APK Import
2025-01-10 19:38:01 +01:00
Christopher O'Grady 4529925906 Integrate SecretsRepository and ApkImporter with app
Refactor methods out of MainActivity into Controllers for ScanScreen and SettingsScreen
2025-01-10 13:21:53 -05:00
Christopher O'Grady c456d455ef Implement Secrets Repository
Add proto plugin and dependencies
Create Secrets Proto
Create Secrets Proto DataStore
Replace old secrets with proto secrets
Fix importers and tests to use new proto secrets.
2025-01-09 15:24:10 -05:00
Christopher O'Grady 9871f0420f Verify Secret correctness as part of loading.
Improve tests
2025-01-08 22:12:03 -05:00
Christopher O'Grady f4974c8705 Import APK in tests 2025-01-08 21:01:37 -05:00
nacabaroandGitHub 5d6e3743f0 Merge pull request #9 from nacabaro/nfc/upload_mon_new_model
Removed unused imports that'd cause compile issues.
2025-01-08 02:37:01 +01:00
Nacho b91da136df Removed unused imports that'd cause compile issues. 2025-01-08 02:34:46 +01:00
nacabaroandGitHub e4366ce15c Merge pull request #8 from nacabaro/nfc/upload_mon_new_model
Nfc/upload mon new model
2025-01-08 02:33:22 +01:00
Nacho 7582f944b5 Forgot to actually remove the files, my bad 2025-01-08 02:32:13 +01:00
Nacho a028611d8a More things:
- Improved upload function to support VB (still to do to add VB)
- Adapted to the latest version of the vb-nfc-reader library
- Added foreign keys with uploaded characters and existing dim database
- Removed the tempoorary objects
- Handled errors uploading (dim not existing for now and VB not supported yet)
- Improved bitmap conversion by moving it to it's own composable
  - Also added object to move bitmap data around
- Added DTO to read sprite data and character data simultaneously

Good night!
2025-01-08 02:31:36 +01:00
nacabaroandGitHub 5edd753da1 Merge pull request #7 from nacabaro/dim/read_cards
Read cards
2025-01-05 22:24:31 +01:00
Nacho 5691f998f2 Cleaned settings screen 2025-01-05 22:18:39 +01:00
Nacho 4e2b9eb541 Fixed flickering and banner overlapping 2025-01-05 18:13:41 +01:00
Nacho 19fbed0ef2 More things!
- Started slowly implementing the original model.
- Names are bitmaps, so we use that.
- Sprites are stored in RoomDB as bitmap.
- Can now import DiMs, need to implement BEms.
- Can also view all the sprites for all the DiMs and now you can view DiMs individually (these two are temporary).

Things to do next
- Use the new model when uploading characters from the watch.
- Figure out why my implementation of ABGR to BGRA isn't working (or something like that)
- Improve data retrieval from the database, maybe using flows and properly implemented view models. As of now the interface is quite flickery.
- Improve code that handles DiMs and BEms.

As for bugs
- Interface sometimes likes flickering a lot, need to figure out why
- I cannot for the life of me create transparent bitmaps... help!
- Something else I might be forgetting...

Going to continue working in this branch.
2025-01-05 17:38:12 +01:00
nacabaroandGitHub 82292823d3 Merge pull request #6 from cfogrady/dim-reader-library
Import vb-dim-reader library
2025-01-05 10:50:04 +01:00
nacabaroandGitHub 01e82a6a44 Merge pull request #5 from cfogrady/nfc-reader-from-maven
Pull Nfc Reader Library from dependency repo instead of as second module.
2025-01-05 10:49:39 +01:00
Christopher O'Grady 1e73f52da5 Add dim-reader library
Update readme to reflect
Add commented out sample code.
2025-01-04 23:46:13 -05:00
Christopher O'Grady d9e492eb0e Add readme describing how to setup
Fix readme
2025-01-04 23:07:07 -05:00
Christopher O'Grady 5bd5fc3f74 Use nfc library from maven local instead of copied module. 2025-01-04 23:03:01 -05:00
Christopher O'Grady 3019560a26 Ignore keys file for now. 2025-01-04 22:47:13 -05:00
Christopher O'Grady a01d0eba47 Ignore generated project files. 2025-01-04 22:45:35 -05:00
nacabaroandGitHub bc02689850 Merge pull request #4 from nacabaro/ui/get_character_details
Few things, again!
2025-01-05 02:12:44 +01:00
Nacho e8f441ba2b Few things, again!
- From "Battles" to "Battle"
- Added a settings screen, to import keys and cards
- Added a way to access the settings from home
- Get character details
2025-01-05 01:58:08 +01:00
Nacho 59033e6459 Forgot to update some foreign key references... sorry! 2025-01-04 19:02:13 +01:00
nacabaroandGitHub 4736202838 Merge pull request #2 from nacabaro/database/initialise
Database/initialise
2025-01-04 18:51:17 +01:00
nacabaroandGitHub aa0f4241ea Merge pull request #1 from nacabaro/ui/main_screens
Added dummy screens plus icons
2025-01-04 16:12:07 +01:00
129 changed files with 3581 additions and 2543 deletions
+12
View File
@@ -0,0 +1,12 @@
.gradle/
.idea/
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
+38
View File
@@ -0,0 +1,38 @@
# VBHelper
## Developer Setup
1. Clone vb-nfc-reader (https://github.com/cfogrady/lib-vb-nfc)
2. Run vb-nfc-reader/publishToMavenLocal gradle task in the lib-vb-nfc project.
3. Clone vb-dim-reader (https://github.com/cfogrady/vb-dim-reader)
4. Run publishToMavenLocal gradle task in the vb-dim-reader project.
5. Create res/values/keys.xml within the app module.
6. Populate with:
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="password1">beHmacKey1</string>
<string name="password2">beHmacKey2</string>
<string name="decryptionKey">aesKey</string>
<integer-array name="substitutionArray">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
<item>14</item>
<item>15</item>
</integer-array>
</resources>
```
7. Replace the values in the keys.xml file with those extracted from the original app.
8. Run
+28 -1
View File
@@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp") version "2.0.21-1.0.27" id("com.google.devtools.ksp") version "2.0.21-1.0.27"
id("com.google.protobuf")
} }
android { android {
@@ -38,11 +39,35 @@ android {
buildFeatures { buildFeatures {
compose = true 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 { dependencies {
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
implementation(project(":vb-nfc-reader")) implementation(libs.vb.nfc.reader)
implementation(libs.dim.reader)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
annotationProcessor(libs.androidx.room.compiler) annotationProcessor(libs.androidx.room.compiler)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
@@ -50,6 +75,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.datastore)
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
@@ -63,5 +89,6 @@ dependencies {
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
implementation("androidx.navigation:navigation-compose:2.7.0") implementation("androidx.navigation:navigation-compose:2.7.0")
implementation("com.google.android.material:material:1.2.0") implementation("com.google.android.material:material:1.2.0")
implementation(libs.protobuf.javalite)
implementation("androidx.compose.material:material") 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,175 +1,252 @@
package com.github.nacabaro.vbhelper package com.github.nacabaro.vbhelper
import android.content.Intent import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.NfcA
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.lifecycle.lifecycleScope
import androidx.compose.runtime.mutableStateOf import com.github.cfogrady.vb.dim.card.BemCard
import androidx.compose.runtime.remember import com.github.cfogrady.vb.dim.card.DimReader
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import com.github.nacabaro.vbhelper.navigation.AppNavigation import com.github.nacabaro.vbhelper.navigation.AppNavigation
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.R
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.be.BENfcCharacter 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.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.di.VBHelper import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryBECharacterData import com.github.nacabaro.vbhelper.domain.characters.Card
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryCharacterData import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHistory 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.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.ui.theme.VBHelperTheme
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.util.GregorianCalendar
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var nfcAdapter: NfcAdapter
private lateinit var deviceToCryptographicTransformers: Map<UShort, CryptographicTransformer>
private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null) private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null)
// EXTRACTED DIRECTLY FROM EXAMPLE APP private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
deviceToCryptographicTransformers = getMapOfCryptographicTransformers()
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this) private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()
if (maybeNfcAdapter == null) {
Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show() private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
finish() if( onActivityLifecycleListeners[key] != null) {
return throw IllegalStateException("Key is already in use")
} }
nfcAdapter = maybeNfcAdapter onActivityLifecycleListeners[key] = activityLifecycleListener
}
private fun unregisterActivityLifecycleListener(key: String) {
onActivityLifecycleListeners.remove(key)
}
override fun onCreate(savedInstanceState: Bundle?) {
registerFileActivityResult()
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) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
VBHelperTheme { 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() {
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
lifecycleScope.launch {
val application = applicationContext as VBHelper
val storageRepository = application.container.db
if (it.resultCode != RESULT_OK) {
Toast.makeText(applicationContext, "Import operation cancelled.", Toast.LENGTH_SHORT).show()
}
val contentResolver = applicationContext.contentResolver
val inputStream = contentResolver.openInputStream(it.data!!.data!!)
inputStream.use { fileReader ->
val dimReader = DimReader()
val card = dimReader.readCard(fileReader, false)
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,
isBEm = card is BemCard
)
val dimId = storageRepository
.dimDao()
.insertNewDim(cardModel)
val characters = card.characterStats.characterEntries
var spriteCounter = when (card is BemCard) {
true -> 55
false -> 10
}
val domainCharacters = mutableListOf<Character>()
for (index in 0 until characters.size) {
domainCharacters.add(
Character(
dimId = dimId,
monIndex = index,
name = card.spriteData.sprites[spriteCounter].pixelData,
stage = characters[index].stage,
attribute = characters[index].attribute,
baseHp = characters[index].hp,
baseBp = characters[index].dp,
baseAp = characters[index].ap,
sprite1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
sprite2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
nameWidth = card.spriteData.sprites[spriteCounter].width,
nameHeight = card.spriteData.sprites[spriteCounter].height,
spritesWidth = card.spriteData.sprites[spriteCounter + 1].width,
spritesHeight = card.spriteData.sprites[spriteCounter + 1].height
)
)
// TODO: Improve this
if (card is BemCard) {
spriteCounter += 14
} else {
when (index) {
0 -> spriteCounter += 6
1 -> spriteCounter += 7
else -> spriteCounter += 14
}
}
}
storageRepository
.characterDao()
.insertCharacter(*domainCharacters.toTypedArray())
val sprites = card.spriteData.sprites.map { sprite ->
Sprites(
id = 0,
sprite = sprite.pixelData,
width = sprite.width,
height = sprite.height
)
}
storageRepository
.characterDao()
.insertSprite(*sprites.toTypedArray())
}
inputStream?.close()
Toast.makeText(applicationContext, "Import successful!", Toast.LENGTH_SHORT).show()
} }
} }
} }
@Composable @Composable
private fun MainApplication() { private fun MainApplication(
var isDoneReadingCharacter by remember { mutableStateOf(false) } scanScreenController: ScanScreenControllerImpl,
val application = LocalContext.current.applicationContext as VBHelper settingsScreenController: SettingsScreenControllerImpl
val storageRepository = application.container.db ) {
AppNavigation( AppNavigation(
isDoneReadingCharacter = isDoneReadingCharacter, applicationNavigationHandlers = AppNavigationHandlers(
onClickRead = { settingsScreenController,
handleTag { scanScreenController,
val character = it.receiveCharacter() ),
onClickImportCard = {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
activityResultLauncher.launch(intent)
}
)
}
private fun handleReceivedNfcCharacter(character: NfcCharacter): String {
nfcCharacter.value = character nfcCharacter.value = character
addCharacterScannedIntoDatabase()
isDoneReadingCharacter = true
"Done reading character"
}
},
onClickScan = {
isDoneReadingCharacter = false
}
)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP val importStatus = addCharacterScannedIntoDatabase()
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)))
)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP return importStatus
private fun showWirelessSettings() {
Toast.makeText(this, "NFC must be enabled", Toast.LENGTH_SHORT).show()
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun buildOnReadTag(handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
return { tag->
val nfcData = NfcA.get(tag)
if (nfcData == null) {
runOnUiThread {
Toast.makeText(this, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
}
}
nfcData.connect()
nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, deviceToCryptographicTransformers)
val successText = handlerFunc(tagCommunicator)
runOnUiThread {
Toast.makeText(this, successText, Toast.LENGTH_SHORT).show()
}
}
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun handleTag(handlerFunc: (TagCommunicator)->String) {
if (!nfcAdapter.isEnabled) {
showWirelessSettings()
} else {
val options = Bundle()
// Work around for some broken Nfc firmware implementations that poll the card too fast
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
nfcAdapter.enableReaderMode(this, buildOnReadTag(handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
options
)
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
override fun onPause() {
super.onPause()
if (nfcAdapter.isEnabled) {
nfcAdapter.disableReaderMode(this)
}
} }
// //
/* /*
TODO: TODO:
- Make it able to detect the different model of watches
- Support for regular VB - Support for regular VB
The good news is that the theory behind inserting to the database should be working The good news is that the theory behind inserting to the database should be working
now, it's a matter of implementing the functionality to parse dim/bem cards and use my now, it's a matter of implementing the functionality to parse dim/bem cards and use my
domain model. domain model.
*/ */
private fun addCharacterScannedIntoDatabase() { private fun addCharacterScannedIntoDatabase(): String {
val beCharacter = nfcCharacter as MutableStateFlow<BENfcCharacter?> val application = applicationContext as VBHelper
val temporaryCharacterData = TemporaryCharacterData( val storageRepository = application.container.db
dimId = nfcCharacter.value!!.dimId.toInt(),
charIndex = nfcCharacter.value!!.charIndex.toInt(), val dimData = storageRepository
.dimDao()
.getDimById(nfcCharacter.value!!.dimId.toInt())
if (dimData == null) return "Card not found"
val cardCharData = storageRepository
.characterDao()
.getCharacterByMonIndex(nfcCharacter.value!!.charIndex.toInt(), dimData.id)
val characterData = UserCharacter(
charId = cardCharData.id,
stage = nfcCharacter.value!!.stage.toInt(), stage = nfcCharacter.value!!.stage.toInt(),
attribute = nfcCharacter.value!!.attribute, attribute = nfcCharacter.value!!.attribute,
ageInDays = nfcCharacter.value!!.ageInDays.toInt(), ageInDays = nfcCharacter.value!!.ageInDays.toInt(),
nextAdventureMissionStage = nfcCharacter.value!!.nextAdventureMissionStage.toInt(), nextAdventureMissionStage = nfcCharacter.value!!.nextAdventureMissionStage.toInt(),
mood = nfcCharacter.value!!.mood.toInt(), mood = nfcCharacter.value!!.mood.toInt(),
vitalPoints = nfcCharacter.value!!.vitalPoints.toInt(), vitalPoints = nfcCharacter.value!!.vitalPoints.toInt(),
transformationCountdown = nfcCharacter.value!!.transformationCountdown.toInt(), transformationCountdown = nfcCharacter.value!!.transformationCountdownInMinutes.toInt(),
injuryStatus = nfcCharacter.value!!.injuryStatus, injuryStatus = nfcCharacter.value!!.injuryStatus,
trophies = nfcCharacter.value!!.trophies.toInt(), trophies = nfcCharacter.value!!.trophies.toInt(),
currentPhaseBattlesWon = nfcCharacter.value!!.currentPhaseBattlesWon.toInt(), currentPhaseBattlesWon = nfcCharacter.value!!.currentPhaseBattlesWon.toInt(),
@@ -177,58 +254,74 @@ class MainActivity : ComponentActivity() {
totalBattlesWon = nfcCharacter.value!!.totalBattlesWon.toInt(), totalBattlesWon = nfcCharacter.value!!.totalBattlesWon.toInt(),
totalBattlesLost = nfcCharacter.value!!.totalBattlesLost.toInt(), totalBattlesLost = nfcCharacter.value!!.totalBattlesLost.toInt(),
activityLevel = nfcCharacter.value!!.activityLevel.toInt(), activityLevel = nfcCharacter.value!!.activityLevel.toInt(),
heartRateCurrent = nfcCharacter.value!!.heartRateCurrent.toInt() heartRateCurrent = nfcCharacter.value!!.heartRateCurrent.toInt(),
characterType = when (nfcCharacter.value) {
is BENfcCharacter -> DeviceType.BEDevice
else -> DeviceType.VBDevice
},
isActive = true
) )
val application = applicationContext as VBHelper storageRepository
val storageRepository = application.container.db .userCharacterDao()
val characterId = storageRepository .clearActiveCharacter()
.temporaryMonsterDao()
.insertCharacterData(temporaryCharacterData)
val temporaryBECharacterData = TemporaryBECharacterData( val characterId: Long = storageRepository
.userCharacterDao()
.insertCharacterData(characterData)
if (nfcCharacter.value is BENfcCharacter) {
val beCharacter = nfcCharacter.value as BENfcCharacter
val extraCharacterData = BECharacterData(
id = characterId, id = characterId,
trainingHp = beCharacter.value!!.trainingHp.toInt(), trainingHp = beCharacter.trainingHp.toInt(),
trainingAp = beCharacter.value!!.trainingAp.toInt(), trainingAp = beCharacter.trainingAp.toInt(),
trainingBp = beCharacter.value!!.trainingBp.toInt(), trainingBp = beCharacter.trainingBp.toInt(),
remainingTrainingTimeInMinutes = beCharacter.value!!.remainingTrainingTimeInMinutes.toInt(), remainingTrainingTimeInMinutes = beCharacter.remainingTrainingTimeInMinutes.toInt(),
itemEffectActivityLevelValue = beCharacter.value!!.itemEffectActivityLevelValue.toInt(), itemEffectActivityLevelValue = beCharacter.itemEffectActivityLevelValue.toInt(),
itemEffectMentalStateValue = beCharacter.value!!.itemEffectMentalStateValue.toInt(), itemEffectMentalStateValue = beCharacter.itemEffectMentalStateValue.toInt(),
itemEffectMentalStateMinutesRemaining = beCharacter.value!!.itemEffectMentalStateMinutesRemaining.toInt(), itemEffectMentalStateMinutesRemaining = beCharacter.itemEffectMentalStateMinutesRemaining.toInt(),
itemEffectActivityLevelMinutesRemaining = beCharacter.value!!.itemEffectActivityLevelMinutesRemaining.toInt(), itemEffectActivityLevelMinutesRemaining = beCharacter.itemEffectActivityLevelMinutesRemaining.toInt(),
itemEffectVitalPointsChangeValue = beCharacter.value!!.itemEffectVitalPointsChangeValue.toInt(), itemEffectVitalPointsChangeValue = beCharacter.itemEffectVitalPointsChangeValue.toInt(),
itemEffectVitalPointsChangeMinutesRemaining = beCharacter.value!!.itemEffectVitalPointsChangeMinutesRemaining.toInt(), itemEffectVitalPointsChangeMinutesRemaining = beCharacter.itemEffectVitalPointsChangeMinutesRemaining.toInt(),
abilityRarity = beCharacter.value!!.abilityRarity, abilityRarity = beCharacter.abilityRarity,
abilityType = beCharacter.value!!.abilityType.toInt(), abilityType = beCharacter.abilityType.toInt(),
abilityBranch = beCharacter.value!!.abilityBranch.toInt(), abilityBranch = beCharacter.abilityBranch.toInt(),
abilityReset = beCharacter.value!!.abilityReset.toInt(), abilityReset = beCharacter.abilityReset.toInt(),
rank = beCharacter.value!!.abilityReset.toInt(), rank = beCharacter.abilityReset.toInt(),
itemType = beCharacter.value!!.itemType.toInt(), itemType = beCharacter.itemType.toInt(),
itemMultiplier = beCharacter.value!!.itemMultiplier.toInt(), itemMultiplier = beCharacter.itemMultiplier.toInt(),
itemRemainingTime = beCharacter.value!!.itemRemainingTime.toInt(), itemRemainingTime = beCharacter.itemRemainingTime.toInt(),
otp0 = "", //beCharacter.value!!.otp0.toString(), otp0 = "", //beCharacter.value!!.otp0.toString(),
otp1 = "", //beCharacter.value!!.otp1.toString(), otp1 = "", //beCharacter.value!!.otp1.toString(),
minorVersion = beCharacter.value!!.characterCreationFirmwareVersion.minorVersion.toInt(), minorVersion = beCharacter.characterCreationFirmwareVersion.minorVersion.toInt(),
majorVersion = beCharacter.value!!.characterCreationFirmwareVersion.majorVersion.toInt(), majorVersion = beCharacter.characterCreationFirmwareVersion.majorVersion.toInt(),
) )
storageRepository storageRepository
.temporaryMonsterDao() .userCharacterDao()
.insertBECharacterData(temporaryBECharacterData) .insertBECharacterData(extraCharacterData)
val transformationHistoryWatch = beCharacter.value!!.transformationHistory val transformationHistoryWatch = beCharacter.transformationHistory
val domainTransformationHistory = transformationHistoryWatch.map { item -> transformationHistoryWatch.map { item ->
TemporaryTransformationHistory( if (item.toCharIndex.toInt() != 255) {
monId = characterId, val date = GregorianCalendar(item.year.toInt(), item.month.toInt(), item.day.toInt())
toCharIndex = item.toCharIndex.toInt(), .time
yearsSince1988 = item.yearsSince1988.toInt(), .time
month = item.month.toInt(),
day = item.day.toInt()
)
}
storageRepository storageRepository
.temporaryMonsterDao() .characterDao()
.insertTransformationHistory(*domainTransformationHistory.toTypedArray()) .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"
}
return "Done reading character!"
} }
} }
@@ -0,0 +1,116 @@
package com.github.nacabaro.vbhelper.components
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) {
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 = shape,
onClick = onClick,
modifier = modifier
.aspectRatio(1f)
.padding(8.dp)
) {
Box(
contentAlignment = Alignment.BottomCenter,
modifier = Modifier
.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
)
}
}
}
@@ -0,0 +1,73 @@
package com.github.nacabaro.vbhelper.components
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
import androidx.compose.material3.Card
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.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.utils.BitmapData
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) { 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,
modifier = modifier,
onClick = onClick
) {
Row (
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(8.dp)
) {
Image (
bitmap = imageBitmap,
contentDescription = name,
filterQuality = FilterQuality.None,
modifier = Modifier
.padding(8.dp)
.size(dpSize)
)
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
)
}
}
}
}
@@ -1,25 +1,78 @@
package com.github.nacabaro.vbhelper.components package com.github.nacabaro.vbhelper.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.github.nacabaro.vbhelper.R
@Composable @Composable
fun TopBanner( fun TopBanner(
text: String, text: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
onGearClick: (() -> Unit)? = null,
onBackClick: (() -> Unit)? = null,
onScanClick: (() -> Unit)? = null
) {
Box( // Use Box to overlay elements
modifier = modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth()
.padding(16.dp)
) { ) {
Text( Text(
text = text, text = text,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontSize = 24.sp, fontSize = 24.sp,
modifier = modifier modifier = Modifier
.fillMaxWidth() .align(Alignment.Center) // Center the text
.padding(16.dp) )
if (onGearClick != null) {
IconButton(
onClick = onGearClick,
modifier = Modifier
.align(Alignment.CenterEnd) // Place gear icon at the end
) {
Icon(
painter = painterResource(R.drawable.baseline_settings_24), // Use a gear icon
contentDescription = "Settings"
) )
} }
}
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
.align(Alignment.CenterStart) // Place gear icon at the end
) {
Icon(
painter = painterResource(R.drawable.baseline_arrow_back_24), // Use a gear icon
contentDescription = "Settings"
)
}
}
}
}
@@ -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)
)
}
}
@@ -0,0 +1,51 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
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 {
@Insert
suspend fun insertCharacter(vararg characterData: Character)
@Query("SELECT * FROM Character")
suspend fun getAllCharacters(): List<Character>
@Query("SELECT * FROM Character WHERE dimId = :dimId")
suspend fun getCharacterByDimId(dimId: Int): List<Character>
@Query("SELECT * FROM Character WHERE monIndex = :monIndex AND dimId = :dimId LIMIT 1")
fun getCharacterByMonIndex(monIndex: Int, dimId: Long): Character
@Insert
suspend fun insertSprite(vararg sprite: Sprites)
@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>
}
@@ -2,15 +2,18 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.Dim import com.github.nacabaro.vbhelper.domain.characters.Card
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface DiMDao { interface DiMDao {
@Insert @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertNewDim(dim: Dim) suspend fun insertNewDim(card: Card): Long
@Query("SELECT * FROM Dim") @Query("SELECT * FROM Card")
fun getAllDims(): Flow<List<Dim>> suspend fun getAllDims(): List<Card>
@Query("SELECT * FROM Card WHERE dimId = :id")
fun getDimById(id: Int): Card?
} }
@@ -0,0 +1,83 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
@Dao
interface UserCharacterDao {
@Insert
fun insertCharacterData(characterData: UserCharacter): Long
@Insert
fun insertBECharacterData(characterData: BECharacterData)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertTransformationHistory(vararg transformationHistory: 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,
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)
}
@@ -1,15 +0,0 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.UserMonsters
@Dao
interface UserMonstersDao {
@Insert
fun insertUserMonsters(userMonsters: UserMonsters)
@Query("SELECT * FROM UserMonsters WHERE userId = :userId")
fun getUserMonsters(userId: Int): List<UserMonsters>
}
@@ -1,11 +0,0 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHistory
@Dao
interface UserMonstersTransformationHistoryDao {
@Insert
fun insertTransformations(vararg transformations: TemporaryTransformationHistory)
}
@@ -2,20 +2,33 @@ package com.github.nacabaro.vbhelper.database
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.github.nacabaro.vbhelper.temporary_daos.TemporaryMonsterDao import com.github.nacabaro.vbhelper.daos.CharacterDao
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryBECharacterData import com.github.nacabaro.vbhelper.daos.DexDao
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryCharacterData import com.github.nacabaro.vbhelper.daos.DiMDao
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHistory import com.github.nacabaro.vbhelper.daos.UserCharacterDao
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
@Database( @Database(
version = 1, version = 1,
entities = [ entities = [
TemporaryCharacterData::class, Card::class,
TemporaryBECharacterData::class, Character::class,
TemporaryTransformationHistory::class Sprites::class,
UserCharacter::class,
BECharacterData::class,
TransformationHistory::class,
Dex::class
] ]
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun temporaryMonsterDao(): TemporaryMonsterDao 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 package com.github.nacabaro.vbhelper.di
import com.github.nacabaro.vbhelper.database.AppDatabase import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
interface AppContainer { interface AppContainer {
val db: AppDatabase val db: AppDatabase
val dataStoreSecretsRepository: DataStoreSecretsRepository
} }
@@ -1,9 +1,22 @@
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import androidx.room.Room import androidx.room.Room
import com.github.nacabaro.vbhelper.database.AppDatabase import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.AppContainer 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 { class DefaultAppContainer(private val context: Context) : AppContainer {
override val db: AppDatabase by lazy { override val db: AppDatabase by lazy {
Room.databaseBuilder( Room.databaseBuilder(
context = context, context = context,
@@ -11,4 +24,8 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
"internalDb" "internalDb"
).build() ).build()
} }
override val dataStoreSecretsRepository = DataStoreSecretsRepository(context.secretsStore)
} }
@@ -1,12 +0,0 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Dim(
@PrimaryKey(autoGenerate = true)
val id: Int,
val name: String,
val stageCount: Int
)
@@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.github.nacabaro.vbhelper.domain.characters.Card
@Entity( @Entity(
foreignKeys = [ foreignKeys = [
@@ -13,7 +14,7 @@ import androidx.room.PrimaryKey
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
), ),
ForeignKey( ForeignKey(
entity = Dim::class, entity = Card::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["dimId"], childColumns = ["dimId"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
@@ -3,17 +3,18 @@ package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.github.nacabaro.vbhelper.domain.characters.Character
@Entity( @Entity(
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = Mon::class, entity = Character::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["monId"], childColumns = ["monId"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
), ),
ForeignKey( ForeignKey(
entity = Mon::class, entity = Character::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["nextMon"], childColumns = ["nextMon"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
@@ -0,0 +1,12 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Sprites(
@PrimaryKey(autoGenerate = true) val id : Int,
val sprite: ByteArray,
val width: Int,
val height: Int
)
@@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.github.nacabaro.vbhelper.domain.characters.Character
@Entity( @Entity(
foreignKeys = [ foreignKeys = [
@@ -13,7 +14,7 @@ import androidx.room.PrimaryKey
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
), ),
ForeignKey( ForeignKey(
entity = Mon::class, entity = Character::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["monId"], childColumns = ["monId"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
@@ -0,0 +1,17 @@
package com.github.nacabaro.vbhelper.domain.characters
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Card(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val dimId: Int,
val logo: ByteArray,
val logoWidth: Int,
val logoHeight: Int,
val name: String,
val stageCount: Int,
val isBEm: Boolean
)
@@ -1,4 +1,4 @@
package com.github.nacabaro.vbhelper.domain package com.github.nacabaro.vbhelper.domain.characters
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@@ -7,22 +7,27 @@ import androidx.room.ForeignKey
@Entity( @Entity(
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = Dim::class, entity = Card::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["dimId"], childColumns = ["dimId"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
) )
] ]
) )
data class Mon ( data class Character (
@PrimaryKey val id: Int, @PrimaryKey(autoGenerate = true) val id: Long = 0,
val dimId: Int, val dimId: Long,
val monIndex: Int, val monIndex: Int,
val name: String, val name: ByteArray,
val stage: Int, // These should be replaced with enums val stage: Int, // These should be replaced with enums
val attribute: Int, // This one too val attribute: Int, // This one too
val baseHp: Int, val baseHp: Int,
val baseBp: Int, val baseBp: Int,
val baseAp: Int, val baseAp: Int,
val evoTime: Int, // In minutes val sprite1: ByteArray,
val sprite2: ByteArray,
val nameWidth: Int,
val nameHeight: Int,
val spritesWidth: Int,
val spritesHeight: Int
) )
@@ -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
)
@@ -1,24 +1,22 @@
package com.github.nacabaro.vbhelper.temporary_domain package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.be.FirmwareVersion
import com.github.cfogrady.vbnfc.data.NfcCharacter import com.github.cfogrady.vbnfc.data.NfcCharacter
@Entity( @Entity(
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = TemporaryCharacterData::class, entity = UserCharacter::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["id"], childColumns = ["id"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
) )
] ]
) )
data class TemporaryBECharacterData ( data class BECharacterData (
@PrimaryKey(autoGenerate = true) val id: Int, @PrimaryKey(autoGenerate = true) val id: Long,
val trainingHp: Int, val trainingHp: Int,
val trainingAp: Int, val trainingAp: Int,
val trainingBp: Int, val trainingBp: Int,
@@ -0,0 +1,29 @@
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 = [
ForeignKey(
entity = UserCharacter::class,
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 stageId: Long,
val transformationDate: Long
)
@@ -1,14 +1,25 @@
package com.github.nacabaro.vbhelper.temporary_domain package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.utils.DeviceType
import com.github.nacabaro.vbhelper.domain.characters.Character
@Entity @Entity(
data class TemporaryCharacterData ( foreignKeys = [
@PrimaryKey(autoGenerate = true) val id: Int = 0, ForeignKey(
val dimId: Int, entity = Character::class,
var charIndex: Int, parentColumns = ["id"],
childColumns = ["charId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class UserCharacter (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
var charId: Long,
var stage: Int, var stage: Int,
var attribute: NfcCharacter.Attribute, var attribute: NfcCharacter.Attribute,
var ageInDays: Int, var ageInDays: Int,
@@ -24,4 +35,6 @@ data class TemporaryCharacterData (
var totalBattlesLost: Int, var totalBattlesLost: Int,
var activityLevel: Int, var activityLevel: Int,
var heartRateCurrent: Int, var heartRateCurrent: Int,
var characterType: DeviceType,
var isActive: Boolean
) )
@@ -0,0 +1,4 @@
package com.github.nacabaro.vbhelper.domain.device_data
class VBCharacterData {
}
@@ -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,
)
}
@@ -0,0 +1,54 @@
package com.github.nacabaro.vbhelper.dtos
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.utils.DeviceType
object CharacterDtos {
data class CharacterWithSprites(
var id: Long = 0,
var charId: Long,
var stage: Int,
var attribute: NfcCharacter.Attribute,
var ageInDays: Int,
var nextAdventureMissionStage: Int, // next adventure mission stage on the character's dim
var mood: Int,
var vitalPoints: Int,
var transformationCountdown: Int,
var injuryStatus: NfcCharacter.InjuryStatus,
var trophies: Int,
var currentPhaseBattlesWon: Int,
var currentPhaseBattlesLost: Int,
var totalBattlesWon: Int,
var totalBattlesLost: Int,
var activityLevel: Int,
var heartRateCurrent: Int,
var characterType: DeviceType,
val spriteIdle: ByteArray,
val spriteWidth: 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,4 +0,0 @@
package com.github.nacabaro.vbhelper.dtos
class MonsterDataCombined {
}
@@ -9,15 +9,25 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.github.nacabaro.vbhelper.screens.BattlesScreen import com.github.nacabaro.vbhelper.screens.BattlesScreen
import com.github.nacabaro.vbhelper.screens.DexScreen import com.github.nacabaro.vbhelper.screens.DexScreen
import com.github.nacabaro.vbhelper.screens.HomeScreen import com.github.nacabaro.vbhelper.screens.DiMScreen
import com.github.nacabaro.vbhelper.screens.ScanScreen import com.github.nacabaro.vbhelper.screens.homeScreens.HomeScreen
import com.github.nacabaro.vbhelper.screens.ItemsScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreen
import com.github.nacabaro.vbhelper.screens.SpriteViewer
import com.github.nacabaro.vbhelper.screens.StorageScreen 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 @Composable
fun AppNavigation( fun AppNavigation(
onClickRead: () -> Unit, applicationNavigationHandlers: AppNavigationHandlers,
onClickScan: () -> Unit, onClickImportCard: () -> Unit,
isDoneReadingCharacter: Boolean
) { ) {
val navController = rememberNavController() val navController = rememberNavController()
@@ -28,29 +38,63 @@ fun AppNavigation(
) { contentPadding -> ) { contentPadding ->
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = BottomNavItem.Home.route, startDestination = NavigationItems.Home.route,
modifier = Modifier modifier = Modifier
.padding(contentPadding) .padding(contentPadding)
) { ) {
composable(BottomNavItem.Battles.route) { composable(NavigationItems.Battles.route) {
BattlesScreen() BattlesScreen()
} }
composable(BottomNavItem.Home.route) { composable(NavigationItems.Home.route) {
HomeScreen() HomeScreen(
} navController = navController
composable(BottomNavItem.Storage.route) {
StorageScreen()
}
composable(BottomNavItem.Scan.route) {
onClickScan()
ScanScreen(
navController = navController,
onClickRead = onClickRead,
isDoneReadingCharacter = isDoneReadingCharacter
) )
} }
composable(BottomNavItem.Dex.route) { composable(NavigationItems.Storage.route) {
DexScreen() StorageScreen(
navController = navController
)
}
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(NavigationItems.Settings.route) {
SettingsScreen(
navController = navController,
settingsScreenController = applicationNavigationHandlers.settingsScreenController,
onClickImportCard = onClickImportCard,
)
}
composable(NavigationItems.Viewer.route) {
SpriteViewer(
navController = navController
)
}
composable(NavigationItems.CardView.route) {
val cardId = it.arguments?.getString("cardId")
if (cardId != null) {
DiMScreen(
navController = navController,
dimId = cardId.toLong()
)
}
}
composable(NavigationItems.Items.route) {
ItemsScreen(
navController = navController
)
} }
} }
} }
@@ -1,15 +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("Battles", R.drawable.baseline_swords_24, "Battles")
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")
}
@@ -13,11 +13,11 @@ import androidx.navigation.compose.currentBackStackEntryAsState
@Composable @Composable
fun BottomNavigationBar(navController: NavController) { fun BottomNavigationBar(navController: NavController) {
val items = listOf( val items = listOf(
BottomNavItem.Scan, NavigationItems.Items,
BottomNavItem.Battles, NavigationItems.Battles,
BottomNavItem.Home, NavigationItems.Home,
BottomNavItem.Dex, NavigationItems.Dex,
BottomNavItem.Storage, NavigationItems.Storage,
) )
NavigationBar { NavigationBar {
val currentBackStackEntry = navController.currentBackStackEntryAsState() 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")
}
@@ -1,80 +1,84 @@
package com.github.nacabaro.vbhelper.screens package com.github.nacabaro.vbhelper.screens
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Card import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.R import androidx.navigation.NavController
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.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
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
@Composable @Composable
fun DexScreen() { fun DexScreen(
navController: NavController
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val dexRepository = DexRepository(application.container.db)
val cardList = remember { mutableStateOf<List<CardDtos.CardProgress>>(emptyList()) }
LaunchedEffect(dexRepository) {
coroutineScope.launch {
val newDimList = dexRepository.getAllDims()
cardList.value = newDimList // Replace the entire list atomically
}
}
Scaffold ( Scaffold (
topBar = { TopBanner("Discovered Digimon") } topBar = {
TopBanner(
text = "Discovered characters",
onGearClick = {
navController.navigate(NavigationItems.Viewer.route)
}
)
}
) { contentPadding -> ) { contentPadding ->
LazyColumn ( LazyColumn (
modifier = Modifier modifier = Modifier
.padding(top = contentPadding.calculateTopPadding()) .padding(top = contentPadding.calculateTopPadding())
) { ) {
items(100) { i -> items(cardList.value) {
DexDiMEntry( DexDiMEntry(
name = "Digimon $i", name = it.cardName,
icon = R.drawable.baseline_egg_24, logo = BitmapData(
onClick = {}, bitmap = it.cardLogo,
width = it.logoWidth,
height = it.logoHeight
),
onClick = {
navController
.navigate(
NavigationItems
.CardView.route
.replace("{cardId}", "${it.cardId}")
)
},
obtainedCharacters = it.obtainedCharacters,
totalCharacters = it.totalCharacters,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding( .padding(8.dp)
vertical = 8.dp,
horizontal = 16.dp
)
) )
} }
} }
} }
} }
@Composable
fun DexDiMEntry(
name: String,
icon: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card (
shape = MaterialTheme.shapes.medium,
modifier = modifier,
onClick = onClick
) {
Row (
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(8.dp)
) {
Image (
painter = painterResource(id = icon),
contentDescription = name,
modifier = Modifier
.padding(8.dp)
.size(64.dp)
)
Text(
text = name,
modifier = Modifier
.padding(8.dp)
)
}
}
}
@@ -0,0 +1,68 @@
package com.github.nacabaro.vbhelper.screens
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
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.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: Long
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val dexRepository = DexRepository(application.container.db)
val characterList = remember { mutableStateOf<List<CharacterDtos.CardProgress>>(emptyList()) }
LaunchedEffect(dexRepository) {
coroutineScope.launch {
val newCharacterList = dexRepository.getCharactersByDimId(dimId)
characterList.value = newCharacterList
}
}
Scaffold (
topBar = {
TopBanner(
text = "Discovered characters",
onBackClick = {
navController.popBackStack()
}
)
}
) { contentPadding ->
LazyVerticalGrid(
columns = GridCells.Fixed(3),
contentPadding = contentPadding
) {
items(characterList.value) { character ->
CharacterEntry(
onClick = { },
obscure = character.discoveredOn == null,
icon = BitmapData(
bitmap = character.spriteIdle,
width = character.spriteWidth,
height = character.spriteHeight,
),
)
}
}
}
}
@@ -2,8 +2,11 @@ package com.github.nacabaro.vbhelper.screens
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.navigation.NavController
@Composable @Composable
fun HomeScreen() { fun ItemsScreen(
Text("Home Screen") 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
)
}
@@ -0,0 +1,90 @@
package com.github.nacabaro.vbhelper.screens
import android.graphics.Bitmap
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.source.SpriteRepo
import kotlinx.coroutines.launch
import java.nio.ByteBuffer
@Composable
fun SpriteViewer(
navController: NavController
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val db = application.container.db
val spriteRepo = SpriteRepo(db)
val spriteList = remember { mutableStateListOf<Sprites>() }
Log.d("SpriteViewer", "spriteList: $spriteList")
LaunchedEffect(spriteRepo) {
coroutineScope.launch {
spriteList.clear()
spriteList.addAll(spriteRepo.getAllSprites())
}
}
Scaffold (
topBar = {
TopBanner(
text = "Sprite viewer",
onBackClick = {
navController.popBackStack()
}
)
},
modifier = Modifier
.fillMaxSize()
) { contentPadding ->
LazyColumn (
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
items(spriteList) { sprite ->
val bitmap = remember (sprite.sprite) {
Log.d("SpriteViewer", "sprite: $sprite")
Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.sprite))
}
}
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
Image(
bitmap = imageBitmap,
contentDescription = "Sprite",
modifier = Modifier
.size(256.dp)
.padding(8.dp)
)
}
}
}
}
@@ -1,10 +1,11 @@
package com.github.nacabaro.vbhelper.screens package com.github.nacabaro.vbhelper.screens
import android.util.Log import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -12,60 +13,76 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState 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.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.R 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.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper 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.source.StorageRepository
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryCharacterData import com.github.nacabaro.vbhelper.utils.BitmapData
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun StorageScreen() { fun StorageScreen(
navController: NavController
) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db) val storageRepository = StorageRepository(application.container.db)
val monList = remember { mutableStateListOf<TemporaryCharacterData>() } val monList = remember { mutableStateOf<List<CharacterDtos.CharacterWithSprites>>(emptyList()) }
var selectedCharacter by remember { mutableStateOf<Long?>(null) }
LaunchedEffect(storageRepository) { LaunchedEffect(storageRepository) {
coroutineScope.launch { coroutineScope.launch {
monList.clear() val characterList = storageRepository.getAllCharacters()
monList.addAll(storageRepository.getAllCharacters()) monList.value = characterList
Log.d("StorageScreen", "Updated data: $monList")
} }
} }
Log.d("StorageScreen", "monList: $monList")
Scaffold ( Scaffold (
topBar = { TopBanner(text = "My Digimon") } topBar = { TopBanner(text = "My Digimon") }
) { contentPadding -> ) { contentPadding ->
if (monList.isEmpty()) { if (monList.value.isEmpty()) {
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
) {
Text( Text(
text = "Nothing to see here", text = "Nothing to see here",
textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.padding(8.dp)
) )
} }
}
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(3), columns = GridCells.Fixed(3),
@@ -73,47 +90,107 @@ fun StorageScreen() {
.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical) .scrollable(state = rememberScrollState(), orientation = Orientation.Vertical)
.padding(top = contentPadding.calculateTopPadding()) .padding(top = contentPadding.calculateTopPadding())
) { ) {
items(monList) { index -> items(monList.value) { index ->
StorageEntry( CharacterEntry(
name = index.dimId.toString() + " - " + index.charIndex.toString(), icon = BitmapData(
icon = R.drawable.ic_launcher_foreground, bitmap = index.spriteIdle,
modifier = Modifier width = index.spriteWidth,
.padding(8.dp) height = index.spriteHeight
),
onClick = {
selectedCharacter = index.id
}
) )
if (selectedCharacter != null) {
StorageDialog(
characterId = selectedCharacter!!,
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()
)
)
}
)
}
} }
} }
} }
} }
@Composable @Composable
fun StorageEntry( fun StorageDialog(
name: String, characterId: Long,
icon: Int, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier onSendToBracelet: () -> Unit,
onClickSetActive: () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
val character = remember { mutableStateOf<UserCharacter?>(null) }
LaunchedEffect(storageRepository) {
coroutineScope.launch {
character.value = storageRepository.getSingleCharacter(characterId)
}
}
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) { ) {
Card( Card(
shape = MaterialTheme.shapes.medium, shape = RoundedCornerShape(16.dp)
modifier = modifier
.padding(8.dp)
) { ) {
Column ( Column (
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.fillMaxSize() .padding(16.dp)
) { ) {
Image( if (character.value != null) {
painter = painterResource(id = icon),
contentDescription = name,
modifier = Modifier
.padding(8.dp)
.size(64.dp)
)
Text( Text(
text = name, text = character.value?.toString() ?: "Loading...",
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
) )
} }
Row (
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
Button(
onClick = onSendToBracelet
) {
Text(text = "Send to bracelet")
}
Button(
onClick = onClickSetActive
) {
Text(text = "Set active")
}
Button(
onClick = onDismissRequest
) {
Text(text = "Close")
}
}
}
}
} }
} }
@@ -0,0 +1,201 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.ItemDisplay
import com.github.nacabaro.vbhelper.components.TransformationHistoryCard
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.utils.BitmapData
import java.util.Locale
@Composable
fun BEBEmHomeScreen(
activeMon: CharacterDtos.CharacterWithSprites,
beData: BECharacterData,
transformationHistory: List<CharacterDtos.TransformationHistory>,
contentPadding: PaddingValues
) {
Column(
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.verticalScroll(state = rememberScrollState())
) {
Row (
modifier = Modifier
.fillMaxWidth()
) {
CharacterEntry(
icon = BitmapData(
bitmap = activeMon.spriteIdle,
width = activeMon.spriteWidth,
height = activeMon.spriteHeight
),
multiplier = 8,
shape = androidx.compose.material.MaterialTheme.shapes.small,
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
)
Column (
modifier = Modifier
.weight(0.5f)
.aspectRatio(0.5f)
) {
ItemDisplay(
icon = R.drawable.baseline_vitals_24,
textValue = activeMon.vitalPoints.toString(),
definition = "Vitals",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_trophy_24,
textValue = activeMon.trophies.toString(),
definition = "Trophies",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
.padding(8.dp)
)
}
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
ItemDisplay(
icon = R.drawable.baseline_mood_24,
textValue = activeMon.mood.toString(),
definition = "Mood",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
val timeInHours = (beData.remainingTrainingTimeInMinutes / 60)
ItemDisplay(
icon = R.drawable.baseline_timer_24,
textValue = "$timeInHours h",
definition = "Training limit",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_rank_24,
textValue = beData.rank.toString(),
definition = "Rank",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
val transformationCountdownInHours = activeMon.transformationCountdown / 60
ItemDisplay(
icon = R.drawable.baseline_next_24,
textValue = when (transformationCountdownInHours) {
0 -> "${activeMon.transformationCountdown} m"
else -> "$transformationCountdownInHours h"
},
definition = "Next timer",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_swords_24,
textValue = when {
activeMon.totalBattlesLost == 0 -> "0.00 %"
else -> {
val battleWinPercentage = activeMon.totalBattlesWon.toFloat() / (activeMon.totalBattlesWon + activeMon.totalBattlesLost).toFloat()
String.format(Locale.getDefault(), "%.2f", battleWinPercentage * 100) + " %" // Specify locale
}
},
definition = "Total battle win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_swords_24,
textValue = when {
activeMon.totalBattlesLost == 0 -> "0.00 %"
else -> {
val battleWinPercentage = activeMon.currentPhaseBattlesWon.toFloat() / (activeMon.currentPhaseBattlesWon + activeMon.currentPhaseBattlesLost).toFloat()
String.format(Locale.getDefault(), "%.2f", battleWinPercentage * 100) + " %" // Specify locale
}
},
definition = "Current phase win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
TransformationHistoryCard(
transformationHistory = transformationHistory,
modifier = Modifier
.weight(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
ItemDisplay(
icon = R.drawable.baseline_health_24,
textValue = "+${beData.trainingHp}",
definition = "Training HP",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_agility_24,
textValue = "+${beData.trainingBp}",
definition = "Training BP",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_attack_24,
textValue = "+${beData.trainingAp}",
definition = "Training AP",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
}
}
@@ -0,0 +1,171 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import java.util.Locale
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.ItemDisplay
import com.github.nacabaro.vbhelper.components.TransformationHistoryCard
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.utils.BitmapData
import kotlin.text.format
@Composable
fun BEDiMHomeScreen(
activeMon: CharacterDtos.CharacterWithSprites,
beData: BECharacterData,
transformationHistory: List<CharacterDtos.TransformationHistory>,
contentPadding: PaddingValues
) {
Column(
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.verticalScroll(state = rememberScrollState())
) {
Row (
modifier = Modifier
.fillMaxWidth()
) {
CharacterEntry(
icon = BitmapData(
bitmap = activeMon.spriteIdle,
width = activeMon.spriteWidth,
height = activeMon.spriteHeight
),
multiplier = 8,
shape = androidx.compose.material.MaterialTheme.shapes.small,
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
)
Column (
modifier = Modifier
.weight(0.5f)
.aspectRatio(0.5f)
) {
ItemDisplay(
icon = R.drawable.baseline_vitals_24,
textValue = activeMon.vitalPoints.toString(),
definition = "Vitals",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_trophy_24,
textValue = activeMon.trophies.toString(),
definition = "Trophies",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
.padding(8.dp)
)
}
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
ItemDisplay(
icon = R.drawable.baseline_mood_24,
textValue = activeMon.mood.toString(),
definition = "Mood",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
val timeInHours = (beData.remainingTrainingTimeInMinutes / 60)
ItemDisplay(
icon = R.drawable.baseline_timer_24,
textValue = "$timeInHours h",
definition = "Training limit",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
// Maybe get rid of this?
ItemDisplay(
icon = R.drawable.baseline_rank_24,
textValue = beData.rank.toString(),
definition = "Rank",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
val transformationCountdownInHours = activeMon.transformationCountdown / 60
ItemDisplay(
icon = R.drawable.baseline_next_24,
textValue = when (transformationCountdownInHours) {
0 -> "${activeMon.transformationCountdown} m"
else -> "$transformationCountdownInHours h"
},
definition = "Next timer",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_swords_24,
textValue = when {
activeMon.totalBattlesLost == 0 -> "0.00 %"
else -> {
val battleWinPercentage = activeMon.totalBattlesWon.toFloat() / (activeMon.totalBattlesWon + activeMon.totalBattlesLost).toFloat()
String.format(Locale.getDefault(), "%.2f", battleWinPercentage * 100) + " %" // Specify locale
}
},
definition = "Total battle win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
ItemDisplay(
icon = R.drawable.baseline_swords_24,
textValue = when {
activeMon.totalBattlesLost == 0 -> "0.00 %"
else -> {
val battleWinPercentage = activeMon.currentPhaseBattlesWon.toFloat() / (activeMon.currentPhaseBattlesWon + activeMon.currentPhaseBattlesLost).toFloat()
String.format(Locale.getDefault(), "%.2f", battleWinPercentage * 100) + " %" // Specify locale
}
},
definition = "Current phase win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(8.dp)
)
}
Row (
modifier = Modifier
.fillMaxWidth()
) {
TransformationHistoryCard(
transformationHistory = transformationHistory,
modifier = Modifier
.weight(1f)
.padding(8.dp)
)
}
}
}
@@ -0,0 +1,102 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.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")
}
@@ -15,11 +15,12 @@ import com.github.nacabaro.vbhelper.components.TopBanner
@Composable @Composable
fun ReadingCharacterScreen( fun ReadingCharacterScreen(
topBannerText: String,
onClickCancel: () -> Unit, onClickCancel: () -> Unit,
) { ) {
Scaffold ( Scaffold (
topBar = { topBar = {
TopBanner("Reading Character") TopBanner(topBannerText)
} }
) { innerPadding -> ) { innerPadding ->
Column ( Column (
@@ -0,0 +1,265 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
import com.github.nacabaro.vbhelper.utils.characterToNfc
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
const val SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER = "SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER"
@Composable
fun ScanScreen(
navController: NavController,
characterId: Long?,
scanScreenController: ScanScreenController,
) {
val secrets by scanScreenController.secretsFlow.collectAsState(null)
var readingScreen by remember { mutableStateOf(false) }
var writingScreen by remember { mutableStateOf(false) }
var isDoneReadingCharacter by remember { mutableStateOf(false) }
var isDoneSendingCard by remember { mutableStateOf(false) }
var isDoneWritingCharacter by remember { mutableStateOf(false) }
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
var nfcCharacter by remember { mutableStateOf<NfcCharacter?>(null) }
val context = LocalContext.current
LaunchedEffect(storageRepository) {
withContext(Dispatchers.IO) {
if(characterId != null && nfcCharacter == null) {
nfcCharacter = characterToNfc(context, characterId)
}
}
}
DisposableEffect(readingScreen || writingScreen, isDoneSendingCard) {
if(readingScreen) {
scanScreenController.registerActivityLifecycleListener(SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER, object: ActivityLifecycleListener {
override fun onPause() {
scanScreenController.cancelRead()
}
override fun onResume() {
scanScreenController.onClickRead(secrets!!) {
isDoneReadingCharacter = true
}
}
})
scanScreenController.onClickRead(secrets!!) {
isDoneReadingCharacter = true
}
} else if (writingScreen) {
scanScreenController.registerActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER,
object : ActivityLifecycleListener {
override fun onPause() {
scanScreenController.cancelRead()
}
override fun onResume() {
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter!!) {
isDoneSendingCard = true
}
} else if (!isDoneWritingCharacter) {
scanScreenController.onClickWrite(secrets!!, nfcCharacter!!) {
isDoneWritingCharacter = true
}
}
}
}
)
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter!!) {
isDoneSendingCard = true
}
} else if (!isDoneWritingCharacter) {
scanScreenController.onClickWrite(secrets!!, nfcCharacter!!) {
isDoneWritingCharacter = true
}
}
}
onDispose {
if(readingScreen || writingScreen) {
scanScreenController.unregisterActivityLifecycleListener(SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER)
scanScreenController.cancelRead()
}
}
}
if (isDoneReadingCharacter) {
readingScreen = false
navController.navigate(NavigationItems.Home.route)
} else if (isDoneSendingCard && isDoneWritingCharacter) {
writingScreen = false
navController.navigate(NavigationItems.Home.route)
LaunchedEffect(storageRepository) {
withContext(Dispatchers.IO) {
storageRepository
.deleteCharacter(characterId!!)
}
}
}
if (readingScreen) {
ReadingCharacterScreen("Reading character") {
readingScreen = false
scanScreenController.cancelRead()
}
} else if (writingScreen) {
if (!isDoneSendingCard) {
ReadingCharacterScreen("Sending card") {
writingScreen = false
scanScreenController.cancelRead()
}
} else if (!isDoneWritingCharacter) {
ReadingCharacterScreen("Writing character") {
isDoneSendingCard = false
writingScreen = false
scanScreenController.cancelRead()
}
}
} else {
ChooseConnectOption(
onClickRead = when {
characterId != null -> null
else -> {
{
if(secrets == null) {
Toast.makeText(context, "Secrets is not yet initialized. Try again.", Toast.LENGTH_SHORT).show()
} else if(secrets?.isMissingSecrets() == true) {
Toast.makeText(context, "Secrets not yet imported. Go to Settings and Import APK", Toast.LENGTH_SHORT).show()
} else {
readingScreen = true // kicks off nfc adapter in DisposableEffect
}
}
}
},
onClickWrite = when {
nfcCharacter == null -> null
else -> {
{
if(secrets == null) {
Toast.makeText(context, "Secrets is not yet initialized. Try again.", Toast.LENGTH_SHORT).show()
} else if(secrets?.isMissingSecrets() == true) {
Toast.makeText(context, "Secrets not yet imported. Go to Settings and Import APK", Toast.LENGTH_SHORT).show()
} else {
writingScreen = true // kicks off nfc adapter in DisposableEffect
}
}
}
}
)
}
}
@Composable
fun ChooseConnectOption(
onClickRead: (() -> Unit)? = null,
onClickWrite: (() -> Unit)? = null,
) {
Scaffold(
topBar = { TopBanner(text = "Scan a Vital Bracelet") }
) { contentPadding ->
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
ScanButton(
text = "Vital Bracelet to App",
disabled = onClickRead == null,
onClick = onClickRead?: { },
)
Spacer(modifier = Modifier.height(16.dp))
ScanButton(
text = "App to Vital Bracelet",
disabled = onClickWrite == null,
onClick = onClickWrite?: { },
)
}
}
}
@Composable
fun ScanButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
disabled: Boolean = false,
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = !disabled,
) {
Text(
text = text,
fontSize = 16.sp,
modifier = Modifier
.padding(4.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun ScanScreenPreview() {
ScanScreen(
navController = rememberNavController(),
scanScreenController = object: ScanScreenController {
override val secretsFlow = MutableStateFlow<Secrets>(Secrets.getDefaultInstance())
override fun unregisterActivityLifecycleListener(key: String) { }
override fun registerActivityLifecycleListener(
key: String,
activityLifecycleListener: ActivityLifecycleListener
) {
}
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit) {}
override fun onClickCheckCard(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit) {}
override fun onClickWrite(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit) {}
override fun cancelRead() {}
},
characterId = null
)
}
@@ -0,0 +1,18 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.flow.Flow
interface ScanScreenController {
val secretsFlow: Flow<Secrets>
fun onClickRead(secrets: Secrets, onComplete: ()->Unit)
fun onClickCheckCard(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit)
fun onClickWrite(secrets: Secrets, nfcCharacter: NfcCharacter, onComplete: () -> Unit)
fun cancelRead()
fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener)
fun unregisterActivityLifecycleListener(key: String)
}
@@ -0,0 +1,147 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.NfcA
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.source.getCryptographicTransformerMap
import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class ScanScreenControllerImpl(
override val secretsFlow: Flow<Secrets>,
private val nfcHandler: (NfcCharacter)->String,
private val context: ComponentActivity,
private val registerActivityLifecycleListener: (String, ActivityLifecycleListener)->Unit,
private val unregisterActivityLifecycleListener: (String)->Unit,
): ScanScreenController {
private val nfcAdapter: NfcAdapter
init {
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(context)
if (maybeNfcAdapter == null) {
Toast.makeText(context, "No NFC on device!", Toast.LENGTH_SHORT).show()
}
nfcAdapter = maybeNfcAdapter
checkSecrets()
}
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit) {
handleTag(secrets) { tagCommunicator ->
val character = tagCommunicator.receiveCharacter()
val resultMessage = nfcHandler(character)
onComplete.invoke()
resultMessage
}
}
override fun cancelRead() {
if(nfcAdapter.isEnabled) {
nfcAdapter.disableReaderMode(context)
}
}
override fun registerActivityLifecycleListener(
key: String,
activityLifecycleListener: ActivityLifecycleListener
) {
registerActivityLifecycleListener.invoke(key, activityLifecycleListener)
}
override fun unregisterActivityLifecycleListener(key: String) {
unregisterActivityLifecycleListener.invoke(key)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun handleTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String) {
if (!nfcAdapter.isEnabled) {
showWirelessSettings()
} else {
val options = Bundle()
// Work around for some broken Nfc firmware implementations that poll the card too fast
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
nfcAdapter.enableReaderMode(context, buildOnReadTag(secrets, handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
options
)
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun buildOnReadTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
return { tag->
val nfcData = NfcA.get(tag)
if (nfcData == null) {
context.runOnUiThread {
Toast.makeText(context, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
}
}
nfcData.connect()
nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
val successText = handlerFunc(tagCommunicator)
context.runOnUiThread {
Toast.makeText(context, successText, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun checkSecrets() {
context.lifecycleScope.launch(Dispatchers.IO) {
if(secretsFlow.stateIn(context.lifecycleScope).value.isMissingSecrets()) {
context.runOnUiThread {
Toast.makeText(context, "Missing Secrets. Go to settings and import Vital Arena APK", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onClickWrite(
secrets: Secrets,
nfcCharacter: NfcCharacter,
onComplete: () -> Unit
) {
handleTag(secrets) { tagCommunicator ->
try {
tagCommunicator.sendCharacter(nfcCharacter)
onComplete.invoke()
"Sent character successfully!"
} catch (e: Throwable) {
Log.e("TAG", e.stackTraceToString())
"Whoops"
}
}
}
override fun onClickCheckCard(
secrets: Secrets,
nfcCharacter: NfcCharacter,
onComplete: () -> Unit
) {
handleTag(secrets) { tagCommunicator ->
tagCommunicator.prepareDIMForCharacter(nfcCharacter.dimId)
onComplete.invoke()
"Sent DIM successfully!"
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun showWirelessSettings() {
Toast.makeText(context, "NFC must be enabled", Toast.LENGTH_SHORT).show()
context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
}
@@ -0,0 +1,103 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
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.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
@Composable
fun SettingsScreen(
navController: NavController,
settingsScreenController: SettingsScreenControllerImpl,
onClickImportCard: () -> Unit
) {
Scaffold (
topBar = {
TopBanner(
text = "Settings",
onBackClick = {
navController.popBackStack()
}
)
},
modifier = Modifier
.fillMaxSize()
) { contentPadding ->
Column (
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
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") { }
SettingsSection("About and credits")
SettingsEntry(title = "Credits", description = "Credits") { }
SettingsEntry(title = "About", description = "About") { }
}
}
}
@Composable
fun SettingsEntry(
title: String,
description: String,
onClick: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(16.dp)
) {
Text(text = title)
Text(
text = description,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline
)
}
}
@Composable
fun SettingsSection(
title: String
) {
Box (
modifier = Modifier
.padding(start = 16.dp)
) {
Text(
text = title,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
}
@@ -0,0 +1,7 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen
interface SettingsScreenController {
fun onClickOpenDirectory()
fun onClickImportDatabase()
fun onClickImportApk()
}
@@ -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
}
@@ -0,0 +1,18 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.database.AppDatabase
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<CardDtos.CardProgress> {
return db.dexDao().getCardsWithProgress()
}
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)
}
}
@@ -0,0 +1,12 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.Sprites
class SpriteRepo (
private val db: AppDatabase
) {
suspend fun getAllSprites(): List<Sprites> {
return db.characterDao().getAllSprites()
}
}
@@ -1,12 +1,44 @@
package com.github.nacabaro.vbhelper.source package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.database.AppDatabase import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryCharacterData 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
class StorageRepository ( class StorageRepository (
private val db: AppDatabase private val db: AppDatabase
) { ) {
suspend fun getAllCharacters(): List<TemporaryCharacterData> { suspend fun getAllCharacters(): List<CharacterDtos.CharacterWithSprites> {
return db.temporaryMonsterDao().getAllCharacters() return db.userCharacterDao().getAllCharacters()
}
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,27 +0,0 @@
package com.github.nacabaro.vbhelper.temporary_daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryBECharacterData
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryCharacterData
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHistory
@Dao
interface TemporaryMonsterDao {
@Insert
fun insertCharacterData(temporaryCharacterData: TemporaryCharacterData): Int
@Insert
fun insertBECharacterData(temporaryBECharacterData: TemporaryBECharacterData)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertTransformationHistory(vararg transformationHistory: TemporaryTransformationHistory)
@Query("SELECT * FROM TemporaryTransformationHistory WHERE monId = :monId")
fun getTransformationHistory(monId: Int): List<TemporaryTransformationHistory>
@Query("SELECT * FROM TemporaryCharacterData")
suspend fun getAllCharacters(): List<TemporaryCharacterData>
}
@@ -1,25 +0,0 @@
package com.github.nacabaro.vbhelper.temporary_domain
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = TemporaryCharacterData::class,
parentColumns = ["id"],
childColumns = ["monId"],
onDelete = ForeignKey.CASCADE
)
]
)
// Bit lazy, will correct later...
data class TemporaryTransformationHistory (
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val monId: Int,
val toCharIndex: Int,
val yearsSince1988: Int,
val month: Int,
val day: Int
)
@@ -0,0 +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
}
@@ -0,0 +1,6 @@
package com.github.nacabaro.vbhelper.utils
enum class DeviceType {
VBDevice,
BEDevice
}
@@ -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
}
@@ -1,2 +0,0 @@
package com.github.nacabaro.vbhelper.vm
@@ -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,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</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,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</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,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M360,120v-80h240v80L360,120ZM440,560h80v-240h-80v240ZM480,880q-74,0 -139.5,-28.5T226,774q-49,-49 -77.5,-114.5T120,520q0,-74 28.5,-139.5T226,266q49,-49 114.5,-77.5T480,160q62,0 119,20t107,58l56,-56 56,56 -56,56q38,50 58,107t20,119q0,74 -28.5,139.5T734,774q-49,49 -114.5,77.5T480,880ZM480,800q116,0 198,-82t82,-198q0,-116 -82,-198t-198,-82q-116,0 -198,82t-82,198q0,116 82,198t198,82ZM480,520Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M280,840v-80h160v-124q-49,-11 -87.5,-41.5T296,518q-75,-9 -125.5,-65.5T120,320v-40q0,-33 23.5,-56.5T200,200h80v-80h400v80h80q33,0 56.5,23.5T840,280v40q0,76 -50.5,132.5T664,518q-18,46 -56.5,76.5T520,636v124h160v80L280,840ZM280,432v-152h-80v40q0,38 22,68.5t58,43.5ZM480,560q50,0 85,-35t35,-85v-240L360,200v240q0,50 35,85t85,35ZM680,432q36,-13 58,-43.5t22,-68.5v-40h-80v152ZM480,380Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M156,447q-11,-12 -11,-28.5t11,-28.5l112,-112 -43,-43 -12,12q-12,12 -28.5,12T156,247q-11,-11 -11,-28t11,-28l80,-80q12,-12 28.5,-12t28.5,12q11,11 11,28t-11,28l-12,12 43,43 112,-112q12,-12 28.5,-12t28.5,12q12,12 12,28.5T493,167l-27,26 295,295q23,23 23,56.5T761,601l-28,29 189,188L808,818L676,686l-28,29q-23,23 -56.5,23T535,715L240,420l-27,27q-12,11 -28.5,11T156,447ZM296,364 L591,659 704,545 644,484 588,540q-12,11 -28.5,11.5T532,541q-12,-12 -12,-28.5t12,-28.5l56,-56 -60,-60 -56,56q-12,11 -28.5,11T415,424q-11,-12 -11,-28.5t11,-28.5l56,-56 -61,-61 -114,114ZM296,364 L410,250 296,364Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M600,880q-17,0 -28.5,-11.5T560,840v-43q-23,-4 -43.5,-11.5T478,765l-40,40q-12,11 -28.5,11.5T381,805q-12,-12 -12,-28.5t12,-28.5l41,-41q-3,-5 -6,-10.5t-6,-10.5l-27,-53 -49,49q-12,11 -28,11.5T278,682q-12,-12 -12,-28t12,-28l49,-50 -53,-26q-5,-2 -9,-4.5t-9,-5.5l-36,36q-12,11 -28.5,11.5T163,576q-12,-12 -12,-28t12,-28l35,-35q-14,-19 -22.5,-40.5T163,400h-43q-17,0 -28.5,-11.5T80,360q0,-17 11.5,-28.5T120,320h45q5,-19 12,-36t18,-33l-35,-35q-12,-12 -12,-28t12,-28q12,-12 28,-12t28,12l35,35q16,-11 33,-18t36,-12v-45q0,-17 11.5,-28.5T360,80q17,0 28.5,11.5T400,120v43q24,4 45.5,13t40.5,23l35,-35q12,-12 28.5,-12t28.5,12q12,12 12,28t-12,28l-37,37q2,4 4.5,8t4.5,9l25,50 46,-46q12,-12 28.5,-12t28.5,12q12,12 12,28.5T678,335l-48,47 56,28q6,3 12.5,6.5T710,424l40,-40q12,-12 28,-12t28,12q12,12 12,28.5T806,441l-40,39q12,18 19.5,38t11.5,42h43q17,0 28.5,11.5T880,600q0,17 -11.5,28.5T840,640h-45q-5,19 -12,35.5T765,708l34,34q12,12 12,28.5T799,799q-12,11 -28.5,11.5T742,799l-33,-34q-16,11 -33,18t-36,12v45q0,17 -11.5,28.5T600,880ZM594,720q58,0 95.5,-44T718,574q-5,-30 -22.5,-54T650,482l-66,-34q-23,-12 -41.5,-30.5T512,376l-34,-66q-16,-32 -46,-51t-66,-19q-58,0 -95.5,44T242,386q5,30 22.5,54t45.5,38l66,34q23,12 41.5,30.5T448,584l34,66q16,32 46,51t66,19ZM380,420q25,0 42.5,-17.5T440,360q0,-25 -17.5,-42.5T380,300q-25,0 -42.5,17.5T320,360q0,25 17.5,42.5T380,420ZM580,670q21,0 35.5,-14.5T630,620q0,-21 -14.5,-35.5T580,570q-21,0 -35.5,14.5T530,620q0,21 14.5,35.5T580,670ZM480,480Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M300,120q52,0 99,22t81,62q34,-40 81,-62t99,-22q94,0 157,63t63,157q0,5 -0.5,10t-0.5,10h-80q1,-5 1,-10v-10q0,-60 -40,-100t-100,-40q-47,0 -87,26.5T518,294h-76q-15,-41 -55,-67.5T300,200q-60,0 -100,40t-40,100v10q0,5 1,10L81,360q0,-5 -0.5,-10t-0.5,-10q0,-94 63,-157t157,-63ZM212,600h112q32,31 70,67t86,79q48,-43 86,-79t70,-67h113q-38,42 -90,91T538,802l-58,52 -58,-52q-69,-62 -120.5,-111T212,600ZM442,640q13,0 22.5,-7.5T478,613l54,-163 35,52q5,8 14,13t19,5h320v-80L623,440l-69,-102q-6,-9 -15.5,-13.5T518,320q-13,0 -22.5,7.5T482,347l-54,162 -34,-51q-5,-8 -14,-13t-19,-5L40,440v80h297l69,102q6,9 15.5,13.5T442,640ZM480,473Z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,76 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.source.proto.Secrets
import org.junit.Assert
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class ApkSecretsImporterTest {
@Test
fun testThatRealImportSecretsPasses() {
val apkFileSecretsImporter = ApkSecretsImporter()
val url = getAndAssertApkFile()
val file = File(url.path)
file.inputStream().use {
val secrets = apkFileSecretsImporter.importSecrets(it)
Assert.assertEquals("Cipher size isn't correct", 16, secrets.vbCipherCount)
Assert.assertEquals("BE Cipher size isn't correct", 16, secrets.beCipherCount)
Assert.assertFalse("AES Key is empty", secrets.aesKey.isEmpty())
assertHmacKeysArePopulated("VBDM", secrets.vbdmHmacKeys)
assertHmacKeysArePopulated("VBC", secrets.vbcHmacKeys)
assertHmacKeysArePopulated("BE", secrets.beHmacKeys)
}
}
@Test
fun testThatApkSecretsImporterCallsDexSecretImporterOnDexFile() {
val expectedDexContents = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1)
var foundFile = false
val apkFileSecretsImporter = ApkSecretsImporter {
val inputStreamContents = it.readAllBytes()
Assert.assertTrue("Unexpected file contents received by DexSecretsImporter", inputStreamContents.contentEquals(expectedDexContents))
foundFile = true
Secrets.newBuilder().build()
}
val apkBytes = constructTestApk(expectedDexContents)
ByteArrayInputStream(apkBytes).use {
apkFileSecretsImporter.importSecrets(it)
}
Assert.assertTrue("${ApkSecretsImporter.DEX_FILE} not found", foundFile)
}
fun constructTestApk(dexContents: ByteArray): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
ZipOutputStream(byteArrayOutputStream).use { zipOutputStream ->
zipOutputStream.putNextEntry(ZipEntry("dummy.txt"))
zipOutputStream.write("This is a text file".toByteArray(StandardCharsets.UTF_8))
zipOutputStream.putNextEntry(ZipEntry("AndroidManifest.xml"))
zipOutputStream.write("Malformed xml!".toByteArray(StandardCharsets.UTF_8))
zipOutputStream.putNextEntry(ZipEntry("assets/"))
zipOutputStream.putNextEntry(ZipEntry("assets/bad.assets"))
zipOutputStream.write("Malformed asset!".toByteArray(StandardCharsets.UTF_8))
zipOutputStream.putNextEntry(ZipEntry(ApkSecretsImporter.DEX_FILE))
zipOutputStream.write(dexContents)
}
return byteArrayOutputStream.toByteArray()
}
private fun getAndAssertApkFile(): URL {
val url = javaClass.getResource("com.bandai.vitalbraceletarena.apk")
if(url == null) {
Assert.assertTrue("""
Create `resources\com\github\nacabaro\vbhelper\source` within the src/test directory.
Add com.bandai.vitalbraceletarena.apk (the official apk) in the above directory. It
should never be checked in and should be on the .gitignore.
""".trimIndent(), false)
}
return url!!
}
}
@@ -0,0 +1,82 @@
package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.source.proto.Secrets.HmacKeys
import org.junit.Assert
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.File
import java.net.URL
import java.nio.ByteOrder
import java.security.InvalidKeyException
fun assertHmacKeysArePopulated(msg: String, hmacKeys: HmacKeys) {
Assert.assertFalse("$msg hmacKey1 is empty", hmacKeys.hmacKey1.isEmpty())
Assert.assertFalse("$msg hmacKey2 is empty", hmacKeys.hmacKey2.isEmpty())
}
class DexFileSecretsImporterTest {
@Test
fun testThatImportSecretsIsPopulated() {
val dexFileSecretsImporter = DexFileSecretsImporter()
val url = getAndAssertClassesDexFile()
val file = File(url.path)
file.inputStream().use {
val secrets = dexFileSecretsImporter.importSecrets(it)
Assert.assertEquals("Cipher size isn't correct", 16, secrets.vbCipherCount)
Assert.assertEquals("BE Cipher size isn't correct", 16, secrets.beCipherCount)
Assert.assertFalse("AES Key is empty", secrets.aesKey.isEmpty())
assertHmacKeysArePopulated("VBDM", secrets.vbdmHmacKeys)
assertHmacKeysArePopulated("VBC", secrets.vbcHmacKeys)
assertHmacKeysArePopulated("BE", secrets.beHmacKeys)
}
}
@Test
fun testThatImportWrongSecretsThrows() {
val dexFileSecretsImporter = DexFileSecretsImporter()
val url = getAndAssertClassesDexFile()
val file = File(url.path)
val content = file.readBytes()
val badCipher = intArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15).toByteArray(ByteOrder.BIG_ENDIAN)
badCipher.copyInto(content, DexFileSecretsImporter.BE_SUBSTITUTION_CIPHER_IDX)
ByteArrayInputStream(content).use {
Assert.assertThrows("Secrets are validated", InvalidKeyException::class.java) {
dexFileSecretsImporter.importSecrets(it)
}
}
}
private fun getAndAssertClassesDexFile(): URL {
val url = javaClass.getResource("classes.dex")
if(url == null) {
Assert.assertTrue("""
Create `resources\com\github\nacabaro\vbhelper\source` within the src/test directory.
Add classes.dex from the official apk in the above directory. It should never be
checked in and should be on the .gitignore.
""".trimIndent(), false)
}
return url!!
}
}
fun IntArray.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray {
val byteArray = ByteArray(this.size*4)
for(i in this.indices) {
val byteArrayIndex = i*4
this[i].toByteArray(byteArray, byteArrayIndex, byteOrder)
}
return byteArray
}
fun Int.toByteArray(bytes: ByteArray, dstIndex: Int, byteOrder: ByteOrder = ByteOrder.nativeOrder()) {
val asUInt = this.toUInt()
for(i in 0 until 4) {
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
bytes[i+dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
} else {
bytes[(3-i) + dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
}
}
}
+6
View File
@@ -4,3 +4,9 @@ plugins {
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
} }
buildscript {
dependencies {
classpath(libs.protobuf.gradle.plugin)
}
}
+10
View File
@@ -1,5 +1,6 @@
[versions] [versions]
agp = "8.7.3" agp = "8.7.3"
datastore = "1.1.1"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.15.0" coreKtx = "1.15.0"
junit = "4.13.2" junit = "4.13.2"
@@ -8,10 +9,15 @@ espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7" lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3" activityCompose = "1.9.3"
composeBom = "2024.04.01" composeBom = "2024.04.01"
protobufGradlePlugin = "0.9.4"
protobufJavalite = "4.27.0"
roomRuntime = "2.6.1" roomRuntime = "2.6.1"
vbNfcReader = "0.1.0"
dimReader = "2.1.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -27,6 +33,10 @@ 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-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } 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" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
+1 -1
View File
@@ -14,6 +14,7 @@ pluginManagement {
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
mavenLocal()
google() google()
mavenCentral() mavenCentral()
} }
@@ -21,4 +22,3 @@ dependencyResolutionManagement {
rootProject.name = "VBHelper" rootProject.name = "VBHelper"
include(":app") include(":app")
include(":vb-nfc-reader")
-1
View File
@@ -1 +0,0 @@
/build
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
-340
View File
@@ -1,340 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceStreaming">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
<option name="codename" value="OP573DL1" />
<option name="id" value="OP573DL1" />
<option name="manufacturer" value="OPPO" />
<option name="name" value="CPH2557" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-21" />
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
-44
View File
@@ -1,44 +0,0 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.github.cfogrady.vbnfc"
compileSdk = 34
defaultConfig {
minSdk = 28
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
testImplementation("io.mockk:mockk-android:1.13.14")
testImplementation("io.mockk:mockk-agent:1.13.14")
}

Some files were not shown because too many files have changed in this diff Show More