From 4529925906edb40dde62029561d85cd9f5481ded Mon Sep 17 00:00:00 2001 From: Christopher O'Grady Date: Fri, 10 Jan 2025 13:21:53 -0500 Subject: [PATCH] Integrate SecretsRepository and ApkImporter with app Refactor methods out of MainActivity into Controllers for ScanScreen and SettingsScreen --- .../vbhelper/ActivityLifecycleListener.kt | 16 ++ .../github/nacabaro/vbhelper/MainActivity.kt | 159 ++++++----------- .../vbhelper/navigation/AppNavigation.kt | 15 +- .../nacabaro/vbhelper/screens/ScanScreen.kt | 108 ------------ .../vbhelper/screens/SettingsScreen.kt | 21 ++- .../screens/SettingsScreenController.kt | 71 ++++++++ .../vbhelper/screens/scanScreen/ScanScreen.kt | 163 ++++++++++++++++++ .../scanScreen/ScanScreenController.kt | 14 ++ .../scanScreen/ScanScreenControllerImpl.kt | 117 +++++++++++++ .../source/DataStoreSecretsRepository.kt | 13 ++ 10 files changed, 469 insertions(+), 228 deletions(-) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/ActivityLifecycleListener.kt delete mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/ScanScreen.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreen.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/ActivityLifecycleListener.kt b/app/src/main/java/com/github/nacabaro/vbhelper/ActivityLifecycleListener.kt new file mode 100644 index 0000000..2c30dd0 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/ActivityLifecycleListener.kt @@ -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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt index 8104869..7250006 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt @@ -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 private var nfcCharacter = MutableStateFlow(null) private lateinit var activityResultLauncher: ActivityResultLauncher - // EXTRACTED DIRECTLY FROM EXAMPLE APP - override fun onCreate(savedInstanceState: Bundle?) { - deviceToCryptographicTransformers = getMapOfCryptographicTransformers() + private val onActivityLifecycleListeners = HashMap() + 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 { - 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 } // diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt index 9b8e89e..727b5fc 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt @@ -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 ) } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/ScanScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/ScanScreen.kt deleted file mode 100644 index 98f5bba..0000000 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/ScanScreen.kt +++ /dev/null @@ -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 - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt index 37356b9..104cded 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreen.kt @@ -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> { + return activity.registerForActivityResult(ActivityResultContracts.OpenDocument()) { + onItemPicked.invoke(it) + } +} + @Composable fun SettingsEntry( title: String, diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt new file mode 100644 index 0000000..4afb3d1 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/SettingsScreenController.kt @@ -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>) { + + 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> { + 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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreen.kt new file mode 100644 index 0000000..9b545e9 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreen.kt @@ -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.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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt new file mode 100644 index 0000000..061e737 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenController.kt @@ -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 + fun onClickRead(secrets: Secrets, onComplete: ()->Unit) + fun cancelRead() + + fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) + fun unregisterActivityLifecycleListener(key: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt new file mode 100644 index 0000000..c31061b --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt @@ -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, + 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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/DataStoreSecretsRepository.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/DataStoreSecretsRepository.kt index 8d9ef96..2e4e4da 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/source/DataStoreSecretsRepository.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/DataStoreSecretsRepository.kt @@ -43,4 +43,17 @@ fun Secrets.getCryptographicTransformerMap(): Map