~cytrogen/vbhelper

eabf8770cc87a771444b515c3f0373f88d10d754 — nacabaro 1 year, 3 months ago da5e6c6 + 4529925
Merge pull request #12 from cfogrady/IntegrateSecretsWithApp

Integrate secrets with app
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