A app/src/main/java/com/github/nacabaro/vbhelper/ActivityLifecycleListener.kt => app/src/main/java/com/github/nacabaro/vbhelper/ActivityLifecycleListener.kt +16 -0
@@ 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() {}
+ }
+ }
+ }
+}<
\ No newline at end of file
M app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt => app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt +52 -107
@@ 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)))
- )
- }
-
- // EXTRACTED DIRECTLY FROM EXAMPLE APP
- private fun showWirelessSettings() {
- Toast.makeText(this, "NFC must be enabled", Toast.LENGTH_SHORT).show()
- startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
- }
-
- // EXTRACTED DIRECTLY FROM EXAMPLE APP
- private fun buildOnReadTag(handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
- return { tag->
- val nfcData = NfcA.get(tag)
- if (nfcData == null) {
- runOnUiThread {
- Toast.makeText(this, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
- }
- }
- nfcData.connect()
- nfcData.use {
- val tagCommunicator = TagCommunicator.getInstance(nfcData, deviceToCryptographicTransformers)
- val successText = handlerFunc(tagCommunicator)
- runOnUiThread {
- Toast.makeText(this, successText, Toast.LENGTH_SHORT).show()
- }
- }
- }
- }
+ private fun handleReceivedNfcCharacter(character: NfcCharacter): String {
+ nfcCharacter.value = character
- // 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
- )
- }
- }
+ val importStatus = addCharacterScannedIntoDatabase()
- // EXTRACTED DIRECTLY FROM EXAMPLE APP
- override fun onPause() {
- super.onPause()
- if (nfcAdapter.isEnabled) {
- nfcAdapter.disableReaderMode(this)
- }
+ return importStatus
}
//
M app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt => app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt +8 -7
@@ 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
)
}
M app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt +15 -6
@@ 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,
A app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt +71 -0
@@ 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()
+ }
+ }
+ }
+ }
+ }
+ }
+}<
\ No newline at end of file
R app/src/main/java/com/github/nacabaro/vbhelper/screens/ScanScreen.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreen.kt +65 -10
@@ 1,5 1,6 @@
-package com.github.nacabaro.vbhelper.screens
+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
@@ 10,28 11,63 @@ 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.screens.scanScreen.ReadingCharacterScreen
+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,
- onClickRead: () -> Unit,
- isDoneReadingCharacter: Boolean
+ 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
@@ 39,12 75,21 @@ fun ScanScreen(
}
if (readingScreen) {
- ReadingCharacterScreen { readingScreen = false }
+ ReadingCharacterScreen {
+ readingScreen = false
+ scanScreenController.cancelRead()
+ }
} else {
+ val context = LocalContext.current
ChooseConnectOption(
onClickRead = {
- readingScreen = true
- 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
+ }
},
)
}
@@ 66,7 111,7 @@ private fun ChooseConnectOption(
) {
ScanButton(
text = "Vital Bracelet to App",
- onClick = onClickRead
+ onClick = onClickRead,
)
Spacer(modifier = Modifier.height(16.dp))
ScanButton(
@@ 102,7 147,17 @@ fun ScanButton(
fun ScanScreenPreview() {
ScanScreen(
navController = rememberNavController(),
- onClickRead = { },
- isDoneReadingCharacter = false
+ 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() {}
+ }
)
}=
\ No newline at end of file
A app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt +14 -0
@@ 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)
+}<
\ No newline at end of file
A app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt +117 -0
@@ 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))
+ }
+}<
\ No newline at end of file
M app/src/main/java/com/github/nacabaro/vbhelper/source/DataStoreSecretsRepository.kt => app/src/main/java/com/github/nacabaro/vbhelper/source/DataStoreSecretsRepository.kt +13 -0
@@ 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
}=
\ No newline at end of file