Integrate SecretsRepository and ApkImporter with app

Refactor methods out of MainActivity into Controllers for ScanScreen and SettingsScreen
This commit is contained in:
Christopher O'Grady 2025-01-10 13:21:53 -05:00
parent c456d455ef
commit 4529925906
10 changed files with 469 additions and 228 deletions

View File

@ -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() {}
}
}
}
}

View File

@ -1,11 +1,8 @@
package com.github.nacabaro.vbhelper
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.NfcA
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@ -13,17 +10,10 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vb.dim.card.DimReader
import com.github.nacabaro.vbhelper.navigation.AppNavigation
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.Dim
@ -32,39 +22,71 @@ import com.github.nacabaro.vbhelper.domain.Character
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.SettingsScreenController
import com.github.nacabaro.vbhelper.source.ApkSecretsImporter
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private lateinit var nfcAdapter: NfcAdapter
private lateinit var deviceToCryptographicTransformers: Map<UShort, CryptographicTransformer>
private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null)
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
// EXTRACTED DIRECTLY FROM EXAMPLE APP
override fun onCreate(savedInstanceState: Bundle?) {
deviceToCryptographicTransformers = getMapOfCryptographicTransformers()
private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()
private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
if( onActivityLifecycleListeners[key] != null) {
throw IllegalStateException("Key is already in use")
}
onActivityLifecycleListeners[key] = activityLifecycleListener
}
private fun unregisterActivityLifecycleListener(key: String) {
onActivityLifecycleListeners.remove(key)
}
override fun onCreate(savedInstanceState: Bundle?) {
registerFileActivityResult()
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (maybeNfcAdapter == null) {
Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show()
finish()
return
}
nfcAdapter = maybeNfcAdapter
val application = applicationContext as VBHelper
val settingsScreenController = SettingsScreenController.Factory(this, ApkSecretsImporter(), application.container.dataStoreSecretsRepository)
.buildSettingScreenHandlers()
val scanScreenController = ScanScreenControllerImpl(
application.container.dataStoreSecretsRepository.secretsFlow,
this::handleReceivedNfcCharacter,
this,
this::registerActivityLifecycleListener,
this::unregisterActivityLifecycleListener)
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
VBHelperTheme {
MainApplication()
MainApplication(settingsScreenController, scanScreenController)
}
}
Log.i("MainActivity", "Activity onCreated")
}
override fun onPause() {
super.onPause()
Log.i("MainActivity", "onPause")
for(activityListener in onActivityLifecycleListeners) {
activityListener.value.onPause()
}
}
override fun onResume() {
super.onResume()
Log.i("MainActivity", "Resume")
for(activityListener in onActivityLifecycleListeners) {
activityListener.value.onResume()
}
}
private fun registerFileActivityResult() {
@ -154,26 +176,10 @@ class MainActivity : ComponentActivity() {
}
@Composable
private fun MainApplication() {
var isDoneReadingCharacter by remember { mutableStateOf(false) }
private fun MainApplication(settingsScreenController: SettingsScreenController, scanScreenController: ScanScreenControllerImpl) {
AppNavigation(
isDoneReadingCharacter = isDoneReadingCharacter,
onClickRead = {
handleTag {
val character = it.receiveCharacter()
nfcCharacter.value = character
val importStatus = addCharacterScannedIntoDatabase()
isDoneReadingCharacter = true
importStatus
}
},
onClickScan = {
isDoneReadingCharacter = false
},
applicationNavigationHandlers = AppNavigationHandlers(settingsScreenController, scanScreenController),
onClickImportCard = {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
@ -184,73 +190,12 @@ class MainActivity : ComponentActivity() {
)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun getMapOfCryptographicTransformers(): Map<UShort, CryptographicTransformer> {
return mapOf(
Pair(DeviceType.VitalBraceletBEDeviceType,
CryptographicTransformer(readableHmacKey1 = resources.getString(R.string.password1),
readableHmacKey2 = resources.getString(R.string.password2),
aesKey = resources.getString(R.string.decryptionKey),
substitutionCipher = resources.getIntArray(R.array.substitutionArray))),
// Pair(DeviceType.VitalSeriesDeviceType,
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
// hmacKey2 = resources.getString(R.string.password2),
// decryptionKey = resources.getString(R.string.decryptionKey),
// substitutionCipher = resources.getIntArray(R.array.substitutionArray))),
// Pair(DeviceType.VitalCharactersDeviceType,
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
// hmacKey2 = resources.getString(R.string.password2),
// decryptionKey = resources.getString(R.string.decryptionKey),
// substitutionCipher = resources.getIntArray(R.array.substitutionArray)))
)
}
private fun handleReceivedNfcCharacter(character: NfcCharacter): String {
nfcCharacter.value = character
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun showWirelessSettings() {
Toast.makeText(this, "NFC must be enabled", Toast.LENGTH_SHORT).show()
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
val importStatus = addCharacterScannedIntoDatabase()
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun buildOnReadTag(handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
return { tag->
val nfcData = NfcA.get(tag)
if (nfcData == null) {
runOnUiThread {
Toast.makeText(this, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
}
}
nfcData.connect()
nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, deviceToCryptographicTransformers)
val successText = handlerFunc(tagCommunicator)
runOnUiThread {
Toast.makeText(this, successText, Toast.LENGTH_SHORT).show()
}
}
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun handleTag(handlerFunc: (TagCommunicator)->String) {
if (!nfcAdapter.isEnabled) {
showWirelessSettings()
} else {
val options = Bundle()
// Work around for some broken Nfc firmware implementations that poll the card too fast
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
nfcAdapter.enableReaderMode(this, buildOnReadTag(handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
options
)
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
override fun onPause() {
super.onPause()
if (nfcAdapter.isEnabled) {
nfcAdapter.disableReaderMode(this)
}
return importStatus
}
//

View File

@ -12,17 +12,19 @@ import com.github.nacabaro.vbhelper.screens.BattlesScreen
import com.github.nacabaro.vbhelper.screens.DexScreen
import com.github.nacabaro.vbhelper.screens.DiMScreen
import com.github.nacabaro.vbhelper.screens.HomeScreen
import com.github.nacabaro.vbhelper.screens.ScanScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.SettingsScreen
import com.github.nacabaro.vbhelper.screens.SettingsScreenController
import com.github.nacabaro.vbhelper.screens.SpriteViewer
import com.github.nacabaro.vbhelper.screens.StorageScreen
data class AppNavigationHandlers(val settingsScreenController: SettingsScreenController, val scanScreenController: ScanScreenControllerImpl)
@Composable
fun AppNavigation(
onClickRead: () -> Unit,
onClickScan: () -> Unit,
applicationNavigationHandlers: AppNavigationHandlers,
onClickImportCard: () -> Unit,
isDoneReadingCharacter: Boolean
) {
val navController = rememberNavController()
@ -49,11 +51,9 @@ fun AppNavigation(
StorageScreen()
}
composable(BottomNavItem.Scan.route) {
onClickScan()
ScanScreen(
navController = navController,
onClickRead = onClickRead,
isDoneReadingCharacter = isDoneReadingCharacter
scanScreenController = applicationNavigationHandlers.scanScreenController,
)
}
composable(BottomNavItem.Dex.route) {
@ -64,6 +64,7 @@ fun AppNavigation(
composable(BottomNavItem.Settings.route) {
SettingsScreen(
navController = navController,
settingsScreenController = applicationNavigationHandlers.settingsScreenController,
onClickImportCard = onClickImportCard
)
}

View File

@ -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
)
}

View File

@ -1,5 +1,9 @@
package com.github.nacabaro.vbhelper.screens
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -22,6 +26,7 @@ import com.github.nacabaro.vbhelper.components.TopBanner
@Composable
fun SettingsScreen(
navController: NavController,
settingsScreenController: SettingsScreenController,
onClickImportCard: () -> Unit
) {
Scaffold (
@ -42,12 +47,10 @@ fun SettingsScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
SettingsSection("General")
SettingsEntry(title = "Import VB key", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import VB Characters key", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import VB BE key", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import transform functions", description = "Import standard vital bracelet keys") { }
SettingsEntry(title = "Import decryption key", description = "Import standard vital bracelet keys") { }
SettingsSection("NFC Communication")
SettingsEntry(title = "Import APK", description = "Import Secrets From Vital Arean 2.1.0 APK") {
settingsScreenController.apkFilePickLauncher.launch(arrayOf("*/*"))
}
SettingsSection("DiM/BEm management")
SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
SettingsEntry(title = "Rename DiM/BEm", description = "Set card name") { }
@ -58,6 +61,12 @@ fun SettingsScreen(
}
}
fun buildFilePickLauncher(activity: ComponentActivity, onItemPicked: (Uri?) -> Unit): ActivityResultLauncher<Array<String>> {
return activity.registerForActivityResult(ActivityResultContracts.OpenDocument()) {
onItemPicked.invoke(it)
}
}
@Composable
fun SettingsEntry(
title: String,

View File

@ -0,0 +1,71 @@
package com.github.nacabaro.vbhelper.screens
import android.net.Uri
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import com.github.nacabaro.vbhelper.source.SecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsRepository
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
data class SettingsScreenController(val apkFilePickLauncher: ActivityResultLauncher<Array<String>>) {
class Factory(private val componentActivity: ComponentActivity, private val secretsImporter: SecretsImporter, private val secretsRepository: SecretsRepository) {
fun buildSettingScreenHandlers(): SettingsScreenController {
return SettingsScreenController(
apkFilePickLauncher = buildFilePickerActivityLauncher(this::importApk)
)
}
private fun buildFilePickerActivityLauncher(onResult : (Uri?) ->Unit): ActivityResultLauncher<Array<String>> {
return componentActivity.registerForActivityResult(ActivityResultContracts.OpenDocument()) {
onResult.invoke(it)
}
}
private fun importApk(uri: Uri?) {
if(uri == null) {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "APK Import Cancelled", Toast.LENGTH_SHORT)
.show()
}
return
}
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
componentActivity.contentResolver.openInputStream(uri).use {
if(it == null) {
componentActivity.runOnUiThread {
Toast.makeText(
componentActivity,
"Selected file is empty!",
Toast.LENGTH_SHORT
).show()
}
return@launch
}
var secrets: Secrets? = null
try {
secrets = secretsImporter.importSecrets(it)
} catch (e: Exception) {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "Secrets import failed. Please only select the official Vital Arena App 2.1.0 APK.", Toast.LENGTH_SHORT).show()
}
return@launch
}
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
secretsRepository.updateSecrets(secrets)
}.invokeOnCompletion {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "Secrets successfully imported. Connections with devices are now possible.", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
}

View File

@ -0,0 +1,163 @@
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.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.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.navigation.BottomNavItem
import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.flow.MutableStateFlow
const val SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER = "SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER"
@Composable
fun ScanScreen(
navController: NavController,
scanScreenController: ScanScreenController,
) {
val secrets by scanScreenController.secretsFlow.collectAsState(null)
var readingScreen by remember { mutableStateOf(false) }
var isDoneReadingCharacter by remember { mutableStateOf(false) }
DisposableEffect(readingScreen) {
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
}
}
onDispose {
if(readingScreen) {
scanScreenController.unregisterActivityLifecycleListener(SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER)
scanScreenController.cancelRead()
}
}
}
if (isDoneReadingCharacter) {
readingScreen = false
navController.navigate(BottomNavItem.Home.route)
}
if (readingScreen) {
ReadingCharacterScreen {
readingScreen = false
scanScreenController.cancelRead()
}
} else {
val context = LocalContext.current
ChooseConnectOption(
onClickRead = {
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
}
},
)
}
}
@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(),
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 cancelRead() {}
}
)
}

View File

@ -0,0 +1,14 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
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 cancelRead()
fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener)
fun unregisterActivityLifecycleListener(key: String)
}

View File

@ -0,0 +1,117 @@
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.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()
}
}
}
}
// 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))
}
}

View File

@ -43,4 +43,17 @@ fun Secrets.getCryptographicTransformerMap(): Map<UShort, CryptographicTransform
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
}